diff --git a/.env b/.env index 65ae7a67..e04a54a2 100644 --- a/.env +++ b/.env @@ -28,7 +28,7 @@ LOCAL_INDEX_INTERVAL=3600 # 30 minutes VERSION_INDEX_INTERVAL=1800 -GITHUB_CLIENT_ID=3acffb2e808d16d4b226 +GITHUB_CLIENT_ID=none GITHUB_CLIENT_SECRET=none RATE_LIMIT_IGNORE_IPS='[]' \ No newline at end of file diff --git a/.idea/labrinth.iml b/.idea/labrinth.iml index c254557e..3f4390b5 100644 --- a/.idea/labrinth.iml +++ b/.idea/labrinth.iml @@ -3,6 +3,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Cargo.lock b/Cargo.lock index f17f90ff..cf1f9680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ dependencies = [ "log", "once_cell", "parking_lot", - "pin-project 0.4.27", + "pin-project 0.4.28", "smallvec", - "tokio 0.2.24", + "tokio 0.2.25", "tokio-util 0.3.1", "trust-dns-proto", "trust-dns-resolver", @@ -38,8 +38,8 @@ dependencies = [ "futures-core", "futures-sink", "log", - "pin-project 0.4.27", - "tokio 0.2.24", + "pin-project 0.4.28", + "tokio 0.2.25", "tokio-util 0.3.1", ] @@ -64,21 +64,23 @@ dependencies = [ [[package]] name = "actix-cors" -version = "0.4.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e5c769e4d332bfad27f11b8139b5818c4bbddb02c385b8f16344d93ff1a8eb" +checksum = "36b133d8026a9f209a9aeeeacd028e7451bcca975f592881b305d37983f303d7" dependencies = [ - "actix-service", "actix-web", "derive_more", "futures-util", + "log", + "once_cell", + "tinyvec", ] [[package]] name = "actix-files" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d031468a7859f71674e5531bd05137e0ea5de05ec9a917314330b88c582e2e0a" +checksum = "c51e8a9146c12fce92a6e4c24b8c4d9b05268130bfd8d61bc587e822c32ce689" dependencies = [ "actix-service", "actix-web", @@ -131,7 +133,7 @@ dependencies = [ "log", "mime", "percent-encoding", - "pin-project 1.0.4", + "pin-project 1.0.7", "rand 0.7.3", "regex", "serde", @@ -139,7 +141,7 @@ dependencies = [ "serde_urlencoded", "sha-1", "slab", - "time 0.2.25", + "time 0.2.26", ] [[package]] @@ -190,9 +192,9 @@ dependencies = [ [[package]] name = "actix-router" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8be584b3b6c705a18eabc11c4059cf83b255bdd8511673d1d569f4ce40c69de" +checksum = "2ad299af73649e1fc893e333ccf86f377751eb95ff875d095131574c6f43452c" dependencies = [ "bytestring", "http", @@ -213,7 +215,7 @@ dependencies = [ "futures-channel", "futures-util", "smallvec", - "tokio 0.2.24", + "tokio 0.2.25", ] [[package]] @@ -233,7 +235,7 @@ dependencies = [ "mio-uds", "num_cpus", "slab", - "socket2", + "socket2 0.3.19", ] [[package]] @@ -243,7 +245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0052435d581b5be835d11f4eb3bce417c8af18d87ddf8ace99f8e67e595882bb" dependencies = [ "futures-util", - "pin-project 0.4.27", + "pin-project 0.4.28", ] [[package]] @@ -257,7 +259,7 @@ dependencies = [ "actix-server", "actix-service", "log", - "socket2", + "socket2 0.3.19", ] [[package]] @@ -303,7 +305,7 @@ dependencies = [ "futures-sink", "futures-util", "log", - "pin-project 0.4.27", + "pin-project 0.4.28", "slab", ] @@ -335,13 +337,13 @@ dependencies = [ "fxhash", "log", "mime", - "pin-project 1.0.4", + "pin-project 1.0.7", "regex", "serde", "serde_json", "serde_urlencoded", - "socket2", - "time 0.2.25", + "socket2 0.3.19", + "time 0.2.26", "tinyvec", "url", ] @@ -370,18 +372,18 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.14.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +checksum = "03345e98af8f3d786b6d9f656ccfa6ac316d954e92bc4841f0bba20789d5fb5a" dependencies = [ "gimli", ] [[package]] name = "adler" -version = "0.2.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" @@ -401,26 +403,20 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "796540673305a66d127804eef19ad696f1f204b8c1025aaca4958c17eab32877" dependencies = [ - "getrandom 0.2.2", + "getrandom 0.2.3", "once_cell", - "version_check 0.9.2", + "version_check 0.9.3", ] [[package]] name = "aho-corasick" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - [[package]] name = "arrayvec" version = "0.5.2" @@ -435,9 +431,9 @@ checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" [[package]] name = "async-channel" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59740d83946db6a5af71ae25ddf9562c2b176b2ca42cf99a455f09f4a220d6b9" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" dependencies = [ "concurrent-queue", "event-listener", @@ -446,16 +442,16 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb877970c7b440ead138f6321a3b5395d6061183af779340b65e20c0fede9146" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "once_cell", - "vec-arena", + "slab", ] [[package]] @@ -476,29 +472,29 @@ dependencies = [ [[package]] name = "async-io" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9315f8f07556761c3e48fec2e6b276004acf426e6dc068b2c2251854d65ee0fd" +checksum = "4bbfd5cf2794b1e908ea8457e6c45f8f8f1f6ec5f74617bf4662623f47503c3b" dependencies = [ "concurrent-queue", "fastrand", "futures-lite", "libc", "log", - "nb-connect", "once_cell", "parking", "polling", - "vec-arena", + "slab", + "socket2 0.4.0", "waker-fn", "winapi 0.3.9", ] [[package]] name = "async-lock" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1996609732bde4a9988bc42125f55f2af5f3c36370e27c778d5191a4a1b63bfb" +checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" dependencies = [ "event-listener", ] @@ -522,7 +518,7 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.4", "futures-channel", "futures-core", "futures-io", @@ -533,33 +529,12 @@ dependencies = [ "memchr", "num_cpus", "once_cell", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "pin-utils", "slab", "wasm-bindgen-futures", ] -[[package]] -name = "async-stream" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3670df70cbc01729f901f94c887814b3c68db038aad1329a418bae178bc5295c" -dependencies = [ - "async-stream-impl", - "futures-core", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3548b8efc9f8e8a5a0a2808c5bd8451a9031b9e5b879a79590304ae928b0a70" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "async-task" version = "4.0.3" @@ -568,9 +543,9 @@ checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" [[package]] name = "async-trait" -version = "0.1.42" +version = "0.1.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" +checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" dependencies = [ "proc-macro2", "quote", @@ -685,11 +660,12 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.56" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" dependencies = [ "addr2line", + "cc", "cfg-if 1.0.0", "libc", "miniz_oxide", @@ -723,9 +699,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "bitvec" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81" +checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" dependencies = [ "funty", "radium", @@ -733,17 +709,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "blake2b_simd" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - [[package]] name = "block-buffer" version = "0.9.0" @@ -789,30 +754,30 @@ dependencies = [ [[package]] name = "buf-min" -version = "0.2.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881e704e61d0fb41d7c6c9ae2bd790eb8c13dc974ae102fb98c788b4fdea4349" +checksum = "fa17aa1cf56bdd6bb30518767d00e58019d326f3f05d8c3e0730b549d332ea83" dependencies = [ - "bytes 0.6.0", + "bytes 0.5.6", ] [[package]] name = "build_const" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" +checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7" [[package]] name = "bumpalo" -version = "3.5.0" +version = "3.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f07aa6688c702439a1be0307b6a94dffe1168569e45b9500c1372bc580740d59" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" [[package]] name = "byteorder" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" @@ -820,12 +785,6 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" -[[package]] -name = "bytes" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0dcbc35f504eb6fc275a6d20e4ebcda18cf50d40ba6fabff8c711fa16cb3b16" - [[package]] name = "bytes" version = "1.0.1" @@ -841,12 +800,67 @@ dependencies = [ "bytes 1.0.1", ] +[[package]] +name = "bzip2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.10+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fa3d1ac1ca21c5c4e36a97f3c3eb25084576f6fc47bf0139c1123434216c6c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cache-padded" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" +[[package]] +name = "cached" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2afe73808fbaac302e39c9754bfc3c4b4d0f99c9c240b9f4e4efc841ad1b74" +dependencies = [ + "async-mutex", + "async-trait", + "cached_proc_macro", + "cached_proc_macro_types", + "futures", + "hashbrown 0.9.1", + "once_cell", +] + +[[package]] +name = "cached_proc_macro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf857ae42d910aede5c5186e62684b0d7a597ce2fe3bd14448ab8f7ef439848c" +dependencies = [ + "async-mutex", + "cached_proc_macro_types", + "darling 0.10.2", + "quote", + "syn", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4f925191b4367301851c6d99b09890311d74b0d43f274c0b34c86d308a3663" + [[package]] name = "cargo-platform" version = "0.1.1" @@ -871,9 +885,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" [[package]] name = "cfg-if" @@ -925,25 +939,25 @@ dependencies = [ [[package]] name = "const_fn" -version = "0.4.5" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" +checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7" [[package]] -name = "constant_time_eq" -version = "0.1.5" +name = "convert_case" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cookie" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" +checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" dependencies = [ "percent-encoding", - "time 0.2.25", - "version_check 0.9.2", + "time 0.2.26", + "version_check 0.9.3", ] [[package]] @@ -969,10 +983,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" [[package]] -name = "cpuid-bool" -version = "0.1.2" +name = "cpufeatures" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" +checksum = "ed00c67cb5d0a7d64a44f6ad2668db7e7530311dd53ea79bcd4fb022c64911c8" +dependencies = [ + "libc", +] [[package]] name = "crc" @@ -1004,12 +1021,12 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.4", ] [[package]] @@ -1019,7 +1036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f6cb3c7f5b8e51bc3ebb73a2327ad4abdbd119dc13223f14f961d2f38486756" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.4", ] [[package]] @@ -1035,9 +1052,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" dependencies = [ "autocfg", "cfg-if 1.0.0", @@ -1066,9 +1083,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.1.18" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10bcb9d7dcbf7002aaffbb53eac22906b64cdcc127971dcc387d8eb7c95d5560" +checksum = "5e98e2ad1a782e33928b96fc3948e7c355e5af34ba4de7670fe8bac2a3b2006d" dependencies = [ "quote", "syn", @@ -1076,24 +1093,24 @@ dependencies = [ [[package]] name = "curl" -version = "0.4.34" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e268162af1a5fe89917ae25ba3b0a77c8da752bdc58e7dbb4f15b91fbd33756e" +checksum = "adba2012502267c1fdde381e0e3acc44bf8be96b2f7d455089dcc2c8ad2f67bd" dependencies = [ "curl-sys", "libc", "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.0", "winapi 0.3.9", ] [[package]] name = "curl-sys" -version = "0.4.40+curl-7.75.0" +version = "0.4.43+curl-7.76.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffafc1c35958318bd7fdd0582995ce4c72f4f461a8e70499ccee83a619fd562" +checksum = "a802c7a9828b7d139efaed1bc92471ab6681ed86d19a105605f5eadcb0837d11" dependencies = [ "cc", "libc", @@ -1111,8 +1128,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + +[[package]] +name = "darling" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2c43f534ea4b0b049015d00269734195e6d3f0f6635cb692251aca6f9f8b3c" +dependencies = [ + "darling_core 0.12.4", + "darling_macro 0.12.4", ] [[package]] @@ -1125,7 +1152,21 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e91455b86830a1c21799d94524df0845183fa55bafd9aa137b01c7d1065fa36" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", "syn", ] @@ -1135,7 +1176,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ - "darling_core", + "darling_core 0.10.2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b5acf0dea37a7f66f7b25d2c5e93fd46f8f6968b1a5d7a3e02e97768afc95a" +dependencies = [ + "darling_core 0.12.4", "quote", "syn", ] @@ -1162,10 +1214,11 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.11" +version = "0.99.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c" +checksum = "5cc7b9cef1e351660e5443924e4f43ab25fbbed3e9a5f052df3677deb4d6b320" dependencies = [ + "convert_case", "proc-macro2", "quote", "syn", @@ -1182,18 +1235,18 @@ dependencies = [ [[package]] name = "dirs" -version = "3.0.1" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" dependencies = [ "libc", "redox_users", @@ -1208,11 +1261,11 @@ checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" [[package]] name = "dlv-list" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b391911b9a786312a10cb9d2b3d0735adfd5a8113eb3648de26a75e91b0826c" +checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" dependencies = [ - "rand 0.7.3", + "rand 0.8.3", ] [[package]] @@ -1223,9 +1276,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "dtoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d7ed2934d741c6b37e33e3832298e8850b53fd2d2bea03873375596c7cea4e" +checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" [[package]] name = "either" @@ -1238,9 +1291,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.26" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" dependencies = [ "cfg-if 1.0.0", ] @@ -1259,9 +1312,9 @@ dependencies = [ [[package]] name = "enum_dispatch" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8946e241a7774d5327d92749c50806f275f57d031d2229ecbfd65469a8ad338e" +checksum = "bd53b3fde38a39a06b2e66dc282f3e86191e53bd04cc499929c15742beae3df8" dependencies = [ "once_cell", "proc-macro2", @@ -1271,9 +1324,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26ecb66b4bdca6c1409b40fb255eefc2bd4f6d135dab3c3124f80ffa2a9661e" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" dependencies = [ "atty", "humantime", @@ -1312,18 +1365,18 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3" +checksum = "77b705829d1e87f762c2df6da140b26af5839e1033aa84aa5f56bb688e4e1bdb" dependencies = [ "instant", ] [[package]] name = "flate2" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7411863d55df97a419aa64cb4d2f167103ea9d767e2c54a1868b7ac3f6b47129" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" dependencies = [ "cfg-if 1.0.0", "crc32fast", @@ -1331,18 +1384,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "flume" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531a685ab99b8f60a271b44d5dd1a76e55124a8c9fa0407b7a8e9cd172d5b588" -dependencies = [ - "futures-core", - "futures-sink", - "pin-project 1.0.4", - "spinning_top", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1366,9 +1407,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ "matches", "percent-encoding", @@ -1398,9 +1439,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" [[package]] name = "futures" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9052a1a50244d8d5aa9bf55cbc2fb6f357c86cc52e46c62ed390a7180cf150" +checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" dependencies = [ "futures-channel", "futures-core", @@ -1413,9 +1454,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d31b7ec7efab6eefc7c57233bb10b847986139d88cc2f5a02a1ae6871a1846" +checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" dependencies = [ "futures-core", "futures-sink", @@ -1423,15 +1464,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e5145dde8da7d1b3892dad07a9c98fc04bc39892b1ecc9692cf53e2b780a65" +checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" [[package]] name = "futures-executor" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e59fdc009a4b3096bf94f740a0f2424c082521f20a9b08c5c07c48d90fd9b9" +checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" dependencies = [ "futures-core", "futures-task", @@ -1440,9 +1481,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28be053525281ad8259d47e4de5de657b25e7bac113458555bb4b70bc6870500" +checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" [[package]] name = "futures-lite" @@ -1455,16 +1496,17 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "waker-fn", ] [[package]] name = "futures-macro" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c287d25add322d9f9abdcdc5927ca398917996600182178774032e9f8258fedd" +checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" dependencies = [ + "autocfg", "proc-macro-hack", "proc-macro2", "quote", @@ -1473,18 +1515,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf5c69029bda2e743fddd0582d1083951d65cc9539aebf8812f36c3491342d6" +checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" [[package]] name = "futures-task" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13de07eb8ea81ae445aca7b69f5f7bf15d7bf4912d8ca37d6645c77ae8a58d86" -dependencies = [ - "once_cell", -] +checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" [[package]] name = "futures-timer" @@ -1494,10 +1533,11 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.12" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632a8cd0f2a4b3fdea1657f08bde063848c3bd00f9bbf6e256b8be78802e624b" +checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" dependencies = [ + "autocfg", "futures-channel", "futures-core", "futures-io", @@ -1505,7 +1545,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "pin-utils", "proc-macro-hack", "proc-macro-nested", @@ -1528,7 +1568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" dependencies = [ "typenum", - "version_check 0.9.2", + "version_check 0.9.3", ] [[package]] @@ -1544,20 +1584,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.1+wasi-snapshot-preview1", + "wasi 0.10.2+wasi-snapshot-preview1", ] [[package]] name = "gimli" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" [[package]] name = "gloo-timers" @@ -1606,7 +1646,7 @@ dependencies = [ "http", "indexmap", "slab", - "tokio 0.2.24", + "tokio 0.2.25", "tokio-util 0.3.1", "tracing", "tracing-futures", @@ -1614,9 +1654,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.0" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b67e66362108efccd8ac053abafc8b7a8d86a37e6e48fc4f6f7485eb5e9e6a5" +checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" dependencies = [ "bytes 1.0.1", "fnv", @@ -1626,10 +1666,9 @@ dependencies = [ "http", "indexmap", "slab", - "tokio 1.1.0", - "tokio-util 0.6.2", + "tokio 1.6.0", + "tokio-util 0.6.7", "tracing", - "tracing-futures", ] [[package]] @@ -1680,9 +1719,9 @@ dependencies = [ [[package]] name = "hex" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" @@ -1717,9 +1756,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7245cd7449cc792608c3c8a9eaf69bd4eabbabf802713748fd739c98b82f0747" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" dependencies = [ "bytes 1.0.1", "fnv", @@ -1738,19 +1777,20 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2861bd27ee074e5ee891e8b539837a9430012e249d7f0ca2d795650f579c1994" +checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" dependencies = [ "bytes 1.0.1", "http", + "pin-project-lite 0.2.6", ] [[package]] name = "httparse" -version = "1.3.4" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" [[package]] name = "httpdate" @@ -1758,6 +1798,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +[[package]] +name = "httpdate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05842d0d43232b23ccb7060ecb0f0626922c21f30012e97b767b30afd4a5d4b9" + [[package]] name = "humantime" version = "2.1.0" @@ -1766,9 +1812,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.13.9" +version = "0.13.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ad767baac13b44d4529fcf58ba2cd0995e36e7b435bc5b039de6f47e880dbf" +checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" dependencies = [ "bytes 0.5.6", "futures-channel", @@ -1778,11 +1824,11 @@ dependencies = [ "http", "http-body 0.3.1", "httparse", - "httpdate", + "httpdate 0.3.2", "itoa", - "pin-project 1.0.4", - "socket2", - "tokio 0.2.24", + "pin-project 1.0.7", + "socket2 0.3.19", + "tokio 0.2.25", "tower-service", "tracing", "want", @@ -1790,23 +1836,23 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.2" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12219dc884514cb4a6a03737f4413c0e01c23a1b059b0156004b23f1e19dccbe" +checksum = "1e5f105c494081baa3bf9e200b279e27ec1623895cd504c7dbef8d0b080fcf54" dependencies = [ "bytes 1.0.1", "futures-channel", "futures-core", "futures-util", - "h2 0.3.0", + "h2 0.3.3", "http", - "http-body 0.4.0", + "http-body 0.4.2", "httparse", - "httpdate", + "httpdate 1.0.0", "itoa", - "pin-project 1.0.4", - "socket2", - "tokio 1.1.0", + "pin-project 1.0.7", + "socket2 0.4.0", + "tokio 1.6.0", "tower-service", "tracing", "want", @@ -1819,9 +1865,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" dependencies = [ "bytes 0.5.6", - "hyper 0.13.9", + "hyper 0.13.10", "native-tls", - "tokio 0.2.24", + "tokio 0.2.25", "tokio-tls", ] @@ -1832,9 +1878,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes 1.0.1", - "hyper 0.14.2", + "hyper 0.14.7", "native-tls", - "tokio 1.1.0", + "tokio 1.6.0", "tokio-native-tls", ] @@ -1846,9 +1892,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ "matches", "unicode-bidi", @@ -1856,10 +1902,16 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.6.1" +name = "if_chain" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1fa934250de4de8aef298d81c729a7d33d8c239daa3a7575e6b92bfc7313b" +checksum = "1f7280c75fb2e2fc47080ec80ccc481376923acb04501957fc38f935c3de5088" + +[[package]] +name = "indexmap" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" dependencies = [ "autocfg", "hashbrown 0.9.1", @@ -1889,7 +1941,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" dependencies = [ - "socket2", + "socket2 0.3.19", "widestring", "winapi 0.3.9", "winreg 0.6.2", @@ -1903,20 +1955,22 @@ checksum = "47be2f14c678be2fdcab04ab1171db51b2762ce6f0a8ee87c8dd4a04ed216135" [[package]] name = "isahc" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3d0a62435883f745c825ec06a03a38d24bf5fa65c43e2c083b6a60ce0058ae" +checksum = "81c01404730bb4574bbacb59ca0855f969f8eabd688ca22866f2cc333f1a4f69" dependencies = [ - "crossbeam-utils 0.8.1", + "async-channel", + "crossbeam-utils 0.8.4", "curl", "curl-sys", "encoding_rs", - "flume", + "event-listener", "futures-lite", "http", "log", "mime", "once_cell", + "polling", "slab", "sluice", "tracing", @@ -1933,9 +1987,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "js-sys" -version = "0.3.47" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cfb73131c35423a367daf8cbd24100af0d077668c8c2943f0e7dd775fef0f65" +checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" dependencies = [ "wasm-bindgen", ] @@ -1961,7 +2015,7 @@ dependencies = [ [[package]] name = "labrinth" -version = "0.1.0" +version = "0.2.0" dependencies = [ "actix-cors", "actix-files", @@ -1972,6 +2026,7 @@ dependencies = [ "async-trait", "base64 0.13.0", "bitflags", + "cached", "chrono", "dotenv", "env_logger", @@ -1982,6 +2037,7 @@ dependencies = [ "log", "meilisearch-sdk", "rand 0.7.3", + "regex", "reqwest 0.10.10", "rust-s3", "sentry", @@ -1993,9 +2049,11 @@ dependencies = [ "sha2", "sqlx", "thiserror", + "validator", "xml-rs", "yaserde", "yaserde_derive", + "zip", ] [[package]] @@ -2012,22 +2070,22 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lexical-core" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ "arrayvec", "bitflags", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "ryu", "static_assertions", ] [[package]] name = "libc" -version = "0.2.82" +version = "0.2.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" +checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "libnghttp2-sys" @@ -2041,9 +2099,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" +checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" dependencies = [ "cc", "libc", @@ -2059,9 +2117,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lock_api" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" +checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" dependencies = [ "scopeguard", ] @@ -2157,9 +2215,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" [[package]] name = "mime" @@ -2179,9 +2237,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", "autocfg", @@ -2208,13 +2266,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.7.7" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7" +checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" dependencies = [ "libc", "log", - "miow 0.3.6", + "miow 0.3.7", "ntapi", "winapi 0.3.9", ] @@ -2244,11 +2302,10 @@ dependencies = [ [[package]] name = "miow" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a33c1b55807fbed163481b5ba66db4b2fa6cde694a5027be10fb724206c5897" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" dependencies = [ - "socket2", "winapi 0.3.9", ] @@ -2270,16 +2327,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nb-connect" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8123a81538e457d44b933a02faf885d3fe8408806b23fa700e8f01c6c3a98998" -dependencies = [ - "libc", - "winapi 0.3.9", -] - [[package]] name = "net2" version = "0.2.37" @@ -2303,14 +2350,15 @@ dependencies = [ [[package]] name = "nom" -version = "6.1.0" +version = "6.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6f70b46d6325aa300f1c7bb3d470127dfc27806d8ea6bf294ee0ce643ce2b1" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" dependencies = [ "bitvec", + "funty", "lexical-core", "memchr", - "version_check 0.9.2", + "version_check 0.9.3", ] [[package]] @@ -2353,15 +2401,15 @@ dependencies = [ [[package]] name = "object" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" [[package]] name = "once_cell" -version = "1.5.2" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "opaque-debug" @@ -2371,29 +2419,29 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.32" +version = "0.10.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70" +checksum = "6d7830286ad6a3973c0f1d9b73738f69c76b739301d0229c4b96501695cbe4c8" dependencies = [ "bitflags", "cfg-if 1.0.0", "foreign-types", - "lazy_static", "libc", + "once_cell", "openssl-sys", ] [[package]] name = "openssl-probe" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.60" +version = "0.9.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6" +checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98" dependencies = [ "autocfg", "cc", @@ -2431,14 +2479,14 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.1.57", + "redox_syscall", "smallvec", "winapi 0.3.9", ] @@ -2460,27 +2508,27 @@ dependencies = [ [[package]] name = "pin-project" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffbc8e94b38ea3d2d8ba92aea2983b503cd75d0888d75b86bb37970b5698e15" +checksum = "918192b5c59119d51e0cd221f4d49dde9112824ba717369e903c97d076083d0f" dependencies = [ - "pin-project-internal 0.4.27", + "pin-project-internal 0.4.28", ] [[package]] name = "pin-project" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b70b68509f17aa2857863b6fa00bf21fc93674c7a8893de2f469f6aa7ca2f2" +checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" dependencies = [ - "pin-project-internal 1.0.4", + "pin-project-internal 1.0.7", ] [[package]] name = "pin-project-internal" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895" +checksum = "3be26700300be6d9d23264c73211d8190e755b6b5ca7a1b28230025511b52a5e" dependencies = [ "proc-macro2", "quote", @@ -2489,9 +2537,9 @@ dependencies = [ [[package]] name = "pin-project-internal" -version = "1.0.4" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa25a6393f22ce819b0f50e0be89287292fda8d425be38ee0ca14c4931d9e71" +checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" dependencies = [ "proc-macro2", "quote", @@ -2500,15 +2548,15 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827" +checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" [[package]] name = "pin-utils" @@ -2524,11 +2572,11 @@ checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" [[package]] name = "polling" -version = "2.0.2" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a7bc6b2a29e632e45451c941832803a18cce6781db04de8a04696cdca8bde4" +checksum = "4fc12d774e799ee9ebae13f4076ca003b40d18a11ac0f3641e6f899618580b7b" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", "log", "wepoll-sys", @@ -2541,6 +2589,30 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check 0.9.3", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check 0.9.3", +] + [[package]] name = "proc-macro-hack" version = "0.5.19" @@ -2555,9 +2627,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" dependencies = [ "unicode-xid", ] @@ -2570,9 +2642,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ "proc-macro2", ] @@ -2625,7 +2697,7 @@ checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" dependencies = [ "libc", "rand_chacha 0.3.0", - "rand_core 0.6.1", + "rand_core 0.6.2", "rand_hc 0.3.0", ] @@ -2646,7 +2718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" dependencies = [ "ppv-lite86", - "rand_core 0.6.1", + "rand_core 0.6.2", ] [[package]] @@ -2660,11 +2732,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5" +checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" dependencies = [ - "getrandom 0.2.2", + "getrandom 0.2.3", ] [[package]] @@ -2682,7 +2754,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" dependencies = [ - "rand_core 0.6.1", + "rand_core 0.6.2", ] [[package]] @@ -2698,56 +2770,48 @@ dependencies = [ "futures-util", "itoa", "percent-encoding", - "pin-project-lite 0.1.11", + "pin-project-lite 0.1.12", "sha1", - "tokio 0.2.24", + "tokio 0.2.25", "tokio-util 0.2.0", "url", ] [[package]] name = "redox_syscall" -version = "0.1.57" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - -[[package]] -name = "redox_syscall" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ec8ca9416c5ea37062b502703cd7fcb207736bc294f6e0cf367ac6fc234570" +checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.3.5" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ - "getrandom 0.1.16", - "redox_syscall 0.1.57", - "rust-argon2", + "getrandom 0.2.3", + "redox_syscall", ] [[package]] name = "regex" -version = "1.4.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.22" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remove_dir_all" @@ -2771,7 +2835,7 @@ dependencies = [ "futures-util", "http", "http-body 0.3.1", - "hyper 0.13.9", + "hyper 0.13.10", "hyper-tls 0.4.3", "ipnet", "js-sys", @@ -2781,11 +2845,11 @@ dependencies = [ "mime_guess", "native-tls", "percent-encoding", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "serde", "serde_json", "serde_urlencoded", - "tokio 0.2.24", + "tokio 0.2.25", "tokio-tls", "url", "wasm-bindgen", @@ -2796,9 +2860,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd281b1030aa675fb90aa994d07187645bb3c8fc756ca766e7c3070b439de9de" +checksum = "2296f2fac53979e8ccbc4a1136b25dcefd37be9ed7e4a1f6b05a6029c84ff124" dependencies = [ "base64 0.13.0", "bytes 1.0.1", @@ -2806,8 +2870,8 @@ dependencies = [ "futures-core", "futures-util", "http", - "http-body 0.4.0", - "hyper 0.14.2", + "http-body 0.4.2", + "hyper 0.14.7", "hyper-tls 0.5.0", "ipnet", "js-sys", @@ -2816,11 +2880,11 @@ dependencies = [ "mime", "native-tls", "percent-encoding", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "serde", "serde_json", "serde_urlencoded", - "tokio 1.1.0", + "tokio 1.6.0", "tokio-native-tls", "url", "wasm-bindgen", @@ -2841,9 +2905,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.19" +version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ "cc", "libc", @@ -2854,18 +2918,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "rust-argon2" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" -dependencies = [ - "base64 0.13.0", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils 0.8.1", -] - [[package]] name = "rust-ini" version = "0.15.3" @@ -2901,16 +2953,16 @@ dependencies = [ "serde_derive", "sha2", "simpl", - "tokio 0.2.24", + "tokio 0.2.25", "url", "uuid", ] [[package]] name = "rustc-demangle" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" +checksum = "410f7acf3cb3a44527c5d9546bad4bf4e6c460915d5f9f2fc524498bfe8f70ce" [[package]] name = "rustc_version" @@ -2943,6 +2995,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + [[package]] name = "ryu" version = "1.0.5" @@ -2976,9 +3034,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sct" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", "untrusted", @@ -2986,9 +3044,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84" dependencies = [ "bitflags", "core-foundation", @@ -2999,9 +3057,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.0.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339" dependencies = [ "core-foundation-sys", "libc", @@ -3047,8 +3105,8 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27c425b07c7186018e2ef9ac3a25b01dae78b05a7ef604d07f216b9f59b42b4" dependencies = [ - "httpdate", - "reqwest 0.11.0", + "httpdate 0.3.2", + "reqwest 0.11.3", "sentry-backtrace", "sentry-contexts", "sentry-core", @@ -3144,9 +3202,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.123" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" dependencies = [ "serde_derive", ] @@ -3165,9 +3223,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.123" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" dependencies = [ "proc-macro2", "quote", @@ -3176,9 +3234,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.61" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" dependencies = [ "indexmap", "itoa", @@ -3200,21 +3258,22 @@ dependencies = [ [[package]] name = "serde_with" -version = "1.6.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "012c1e1750318ba7d775e4104e34a4eca896b0016e6b90370f12381a28fb29f0" +checksum = "edeeaecd5445109b937a3a335dc52780ca7779c4b4b7374cc6340dedfe44cfca" dependencies = [ + "rustversion", "serde", "serde_with_macros", ] [[package]] name = "serde_with_macros" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1197ff7de45494f290c1e3e1a6f80e108974681984c87a3e480991ef3d0f1950" +checksum = "e48b35457e9d855d3dc05ef32a73e0df1e2c0fd72c38796a4ee909160c8eeec2" dependencies = [ - "darling", + "darling 0.12.4", "proc-macro2", "quote", "syn", @@ -3222,13 +3281,13 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3cdf1b5e620a498ee6f2a171885ac7e22f0e12089ec4b3d22b84921792507c" +checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16" dependencies = [ "block-buffer", "cfg-if 1.0.0", - "cpuid-bool", + "cpufeatures", "digest", "opaque-debug", ] @@ -3241,13 +3300,13 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" [[package]] name = "sha2" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e7aab86fe2149bad8c507606bdb3f4ef5e7b2380eb92350f56122cca72a42a8" +checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" dependencies = [ "block-buffer", "cfg-if 1.0.0", - "cpuid-bool", + "cpufeatures", "digest", "opaque-debug", ] @@ -3269,9 +3328,9 @@ checksum = "2a30f10c911c0355f80f1c2faa8096efc4a58cdf8590b954d5b395efa071c711" [[package]] name = "slab" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" [[package]] name = "sluice" @@ -3301,30 +3360,31 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "spinning_top" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e529d73e80d64b5f2631f9035113347c578a1c9c7774b83a2b880788459ab36" -dependencies = [ - "lock_api", -] - [[package]] name = "sqlformat" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c70f0235b9925cbb106c52af1a28b5ea4885a8b851e328b8562e257a389c2d" +checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c" dependencies = [ "lazy_static", "maplit", - "nom 6.1.0", + "nom 6.1.2", "regex", "unicode_categories", ] @@ -3353,9 +3413,9 @@ dependencies = [ "bytes 0.5.6", "chrono", "crc", - "crossbeam-channel 0.5.0", + "crossbeam-channel 0.5.1", "crossbeam-queue", - "crossbeam-utils 0.8.1", + "crossbeam-utils 0.8.4", "either", "futures-channel", "futures-core", @@ -3420,17 +3480,17 @@ dependencies = [ "actix-rt", "actix-threadpool", "once_cell", - "tokio 0.2.24", + "tokio 0.2.25", "tokio-rustls", ] [[package]] name = "standback" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" dependencies = [ - "version_check 0.9.2", + "version_check 0.9.3", ] [[package]] @@ -3504,6 +3564,12 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.0" @@ -3512,9 +3578,9 @@ checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "syn" -version = "1.0.60" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" dependencies = [ "proc-macro2", "quote", @@ -3535,9 +3601,9 @@ dependencies = [ [[package]] name = "tap" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" @@ -3548,7 +3614,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "rand 0.8.3", - "redox_syscall 0.2.4", + "redox_syscall", "remove_dir_all", "winapi 0.3.9", ] @@ -3564,33 +3630,24 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146" +checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1" +checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "thread_local" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8208a331e1cb318dd5bd76951d2b8fc48ca38a69f5f4e4af1b6a9f8c6236915" -dependencies = [ - "once_cell", -] - [[package]] name = "threadpool" version = "1.8.1" @@ -3612,16 +3669,16 @@ dependencies = [ [[package]] name = "time" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" +checksum = "08a8cbfbf47955132d0202d1662f49b2423ae35862aee471f3ba4b133358f372" dependencies = [ "const_fn", "libc", "standback", "stdweb", "time-macros", - "version_check 0.9.2", + "version_check 0.9.3", "winapi 0.3.9", ] @@ -3650,9 +3707,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023" +checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" dependencies = [ "tinyvec_macros", ] @@ -3665,9 +3722,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099837d3464c16a808060bb3f02263b412f6fafcb5d01c533d309985fbeebe48" +checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" dependencies = [ "bytes 0.5.6", "fnv", @@ -3679,7 +3736,7 @@ dependencies = [ "mio 0.6.23", "mio-uds", "num_cpus", - "pin-project-lite 0.1.11", + "pin-project-lite 0.1.12", "signal-hook-registry", "slab", "tokio-macros", @@ -3688,17 +3745,17 @@ dependencies = [ [[package]] name = "tokio" -version = "1.1.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8efab2086f17abcddb8f756117665c958feee6b2e39974c2f1600592ab3a4195" +checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37" dependencies = [ "autocfg", "bytes 1.0.1", "libc", "memchr", - "mio 0.7.7", + "mio 0.7.11", "num_cpus", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", ] [[package]] @@ -3719,7 +3776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" dependencies = [ "native-tls", - "tokio 1.1.0", + "tokio 1.6.0", ] [[package]] @@ -3730,21 +3787,10 @@ checksum = "e12831b255bcfa39dc0436b01e19fea231a37db570686c06ee72c423479f889a" dependencies = [ "futures-core", "rustls", - "tokio 0.2.24", + "tokio 0.2.25", "webpki", ] -[[package]] -name = "tokio-stream" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76066865172052eb8796c686f0b441a93df8b08d40a950b062ffb9a426f00edd" -dependencies = [ - "futures-core", - "pin-project-lite 0.2.4", - "tokio 1.1.0", -] - [[package]] name = "tokio-tls" version = "0.3.1" @@ -3752,7 +3798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" dependencies = [ "native-tls", - "tokio 0.2.24", + "tokio 0.2.25", ] [[package]] @@ -3765,8 +3811,8 @@ dependencies = [ "futures-core", "futures-sink", "log", - "pin-project-lite 0.1.11", - "tokio 0.2.24", + "pin-project-lite 0.1.12", + "tokio 0.2.25", ] [[package]] @@ -3780,24 +3826,22 @@ dependencies = [ "futures-io", "futures-sink", "log", - "pin-project-lite 0.1.11", - "tokio 0.2.24", + "pin-project-lite 0.1.12", + "tokio 0.2.25", ] [[package]] name = "tokio-util" -version = "0.6.2" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb971a26599ffd28066d387f109746df178eff14d5ea1e235015c5601967a4b" +checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" dependencies = [ - "async-stream", "bytes 1.0.1", "futures-core", "futures-sink", "log", - "pin-project-lite 0.2.4", - "tokio 1.1.0", - "tokio-stream", + "pin-project-lite 0.2.6", + "tokio 1.6.0", ] [[package]] @@ -3808,22 +3852,22 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.22" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.4", + "pin-project-lite 0.2.6", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" -version = "0.1.13" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a9bd1db7706f2373a190b0d067146caa39350c486f3d455b0e33b431f94c07" +checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" dependencies = [ "proc-macro2", "quote", @@ -3832,31 +3876,31 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" +checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" dependencies = [ "lazy_static", ] [[package]] name = "tracing-futures" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" dependencies = [ - "pin-project 0.4.27", + "pin-project 1.0.7", "tracing", ] [[package]] name = "trust-dns-proto" -version = "0.19.6" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53861fcb288a166aae4c508ae558ed18b53838db728d4d310aad08270a7d4c2b" +checksum = "1cad71a0c0d68ab9941d2fb6e82f8fb2e86d9945b94e1661dd0aaea2b88215a9" dependencies = [ "async-trait", - "backtrace", + "cfg-if 1.0.0", "enum-as-inner", "futures", "idna", @@ -3865,17 +3909,16 @@ dependencies = [ "rand 0.7.3", "smallvec", "thiserror", - "tokio 0.2.24", + "tokio 0.2.25", "url", ] [[package]] name = "trust-dns-resolver" -version = "0.19.6" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6759e8efc40465547b0dfce9500d733c65f969a4cbbfbe3ccf68daaa46ef179e" +checksum = "710f593b371175db53a26d0b38ed2978fafb9e9e8d3868b1acd753ea18df0ceb" dependencies = [ - "backtrace", "cfg-if 0.1.10", "futures", "ipconfig", @@ -3885,7 +3928,7 @@ dependencies = [ "resolv-conf", "smallvec", "thiserror", - "tokio 0.2.24", + "tokio 0.2.25", "trust-dns-proto", ] @@ -3897,9 +3940,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "twoway" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc" +checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47" dependencies = [ "memchr", "unchecked-index", @@ -3907,9 +3950,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "ucd-trie" @@ -3938,23 +3981,23 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" dependencies = [ - "version_check 0.9.2", + "version_check 0.9.3", ] [[package]] name = "unicode-bidi" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" dependencies = [ "matches", ] [[package]] name = "unicode-normalization" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13e63ab62dbe32aeee58d1c5408d35c36c392bba5d9d3142287219721afe606" +checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" dependencies = [ "tinyvec", ] @@ -3967,9 +4010,9 @@ checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" [[package]] name = "unicode-xid" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "unicode_categories" @@ -3994,9 +4037,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", "idna", @@ -4011,15 +4054,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.2", + "getrandom 0.2.3", "serde", ] [[package]] name = "v_escape" -version = "0.14.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccca9e73c678b882900cbaec16dae4d3662ace5a17774ac45af04e0f3988fafa" +checksum = "f3e0ab5fab1db278a9413d2ea794cb66f471f898c5b020c3c394f6447625d9d4" dependencies = [ "buf-min", "v_escape_derive", @@ -4039,34 +4082,68 @@ dependencies = [ [[package]] name = "v_htmlescape" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db00c903248abee8499af60bf20d242e7882335bbbffd2614915184cbb207402" +checksum = "1f9a8af610ad6f7fc9989c9d2590d9764bc61f294884e9ee93baa58795174572" dependencies = [ "cfg-if 1.0.0", "v_escape", ] [[package]] -name = "value-bag" -version = "1.0.0-alpha.6" +name = "validator" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b676010e055c99033117c2343b33a40a30b91fecd6c49055ac9cd2d6c305ab1" +checksum = "be110dc66fa015b8b1d2c4eae40c495a27fae55f82b9cae3efb8178241ed20eb" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", + "validator_types", +] + +[[package]] +name = "validator_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f14fe757e2894ce4271991901567be07fbc3eac6b24246122214e1d5a16554" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9680608df133af2c1ddd5eaf1ddce91d60d61b6bc51494ef326458365a470a" + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd320e1520f94261153e96f7534476ad869c14022aee1e59af7c778075d840ae" dependencies = [ "ctor", + "version_check 0.9.3", ] [[package]] name = "vcpkg" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" - -[[package]] -name = "vec-arena" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eafc1b9b2dfc6f5529177b62cf806484db55b32dc7c9658a118e11bbeb33061d" +checksum = "025ce40a007e1907e58d5bc1a594def78e5573bb0b1160bc389634e8f12e4faa" [[package]] name = "version_check" @@ -4076,9 +4153,9 @@ checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" [[package]] name = "void" @@ -4110,15 +4187,15 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" [[package]] name = "wasi" -version = "0.10.1+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c6c3420963c5c64bca373b25e77acb562081b9bb4dd5bb864187742186cea9" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.70" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55c0f7123de74f0dab9b7d00fd614e7b19349cd1e2f5252bbe9b1754b59433be" +checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" dependencies = [ "cfg-if 1.0.0", "serde", @@ -4128,9 +4205,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.70" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc45447f0d4573f3d65720f636bbcc3dd6ce920ed704670118650bcd47764c7" +checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" dependencies = [ "bumpalo", "lazy_static", @@ -4143,9 +4220,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.20" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3de431a2910c86679c34283a33f66f4e4abd7e0aec27b6669060148872aadf94" +checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -4155,9 +4232,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.70" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b8853882eef39593ad4174dd26fc9865a64e84026d223f63bb2c42affcbba2c" +checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4165,9 +4242,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.70" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4133b5e7f2a531fa413b3a1695e925038a05a71cf67e87dafa295cb645a01385" +checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" dependencies = [ "proc-macro2", "quote", @@ -4178,15 +4255,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.70" +version = "0.2.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4945e4943ae02d15c13962b38a5b1e81eadd4b71214eee75af64a4d6a4fd64" +checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" [[package]] name = "web-sys" -version = "0.3.47" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c40dc691fc48003eba817c38da7113c15698142da971298003cac3ef175680b3" +checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" dependencies = [ "js-sys", "wasm-bindgen", @@ -4204,9 +4281,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ "webpki", ] @@ -4222,9 +4299,9 @@ dependencies = [ [[package]] name = "whoami" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a921c0ad578a51c0b6c0bbb9b95f0ed11e90d61da506139e48a946edd11ee1e" +checksum = "4abacf325c958dfeaf1046931d37f2a901b6dfe0968ee965a29e94c6766b2af6" dependencies = [ "wasm-bindgen", "web-sys", @@ -4340,3 +4417,17 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zip" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c83dc9b784d252127720168abd71ea82bf8c3d96b17dc565b5e2a02854f2b27" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time 0.1.43", +] diff --git a/Cargo.toml b/Cargo.toml index 9e0d8d43..e6b6792f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "labrinth" -version = "0.1.0" +version = "0.2.0" #Team members, please add your emails and usernames authors = ["geometrically ", "Redblueflame ", "Aeledfyr ", "Charalampos Fanoulis ", "AppleTheGolden "] edition = "2018" @@ -12,11 +12,11 @@ path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -actix-web = "3.1.0" -actix-rt = "1.1.1" -actix-files = "0.4.0" +actix-web = "3.3.2" +actix-rt = "1.1.0" +actix-files = "0.5.0" actix-multipart = "0.3.0" -actix-cors = "0.4.1" +actix-cors = "0.5.4" actix-ratelimit = "0.3.0" meilisearch-sdk = "0.6.0" @@ -35,6 +35,10 @@ base64 = "0.13.0" sha1 = { version = "0.6.0", features = ["std"] } sha2 = "0.9.2" bitflags = "1.2.1" +zip = "0.5.12" + +validator = { version = "0.13", features = ["derive"] } +regex = "1.5.4" gumdrop = "0.8.0" dotenv = "0.15" @@ -52,3 +56,5 @@ sqlx = { version = "0.4.2", features = ["runtime-actix-rustls", "postgres", "chr sentry = { version = "0.22.0", features = ["log"] } sentry-actix = "0.22.0" + +cached = "0.23.0" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2a1d12d0..9fdd1f45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.45.1 as build +FROM rust:1.52.1 as build ENV PKG_CONFIG_ALLOW_CROSS=1 WORKDIR /usr/src/labrinth diff --git a/migrations/20210509010206_project_types.sql b/migrations/20210509010206_project_types.sql new file mode 100644 index 00000000..d772f0ae --- /dev/null +++ b/migrations/20210509010206_project_types.sql @@ -0,0 +1,31 @@ +ALTER TABLE users ADD CONSTRAINT username_unique UNIQUE (username); + +CREATE TABLE project_types ( + id serial PRIMARY KEY, + name varchar(64) UNIQUE NOT NULL +); + +INSERT INTO project_types (name) VALUES ('mod'); +INSERT INTO project_types (name) VALUES ('modpack'); + +CREATE TABLE loaders_project_types ( + joining_loader_id int REFERENCES loaders ON UPDATE CASCADE NOT NULL, + joining_project_type_id int REFERENCES project_types ON UPDATE CASCADE NOT NULL, + PRIMARY KEY (joining_loader_id, joining_project_type_id) +); + +ALTER TABLE mods + ADD COLUMN project_type integer REFERENCES project_types NOT NULL default 1; + +ALTER TABLE categories + ADD COLUMN project_type integer REFERENCES project_types NOT NULL default 1, + ADD COLUMN icon varchar(20000) NOT NULL default ''; + +ALTER TABLE loaders + ADD COLUMN icon varchar(20000) NOT NULL default ''; + +ALTER TABLE mods + ALTER COLUMN project_type DROP DEFAULT; + +ALTER TABLE categories + ALTER COLUMN project_type DROP DEFAULT; diff --git a/sqlx-data.json b/sqlx-data.json index 2e545c1b..35f2ced3 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -1,5 +1,44 @@ { "db": "PostgreSQL", + "002a7e9d3ccfb94348b1e7ad93b923aab9d2c377e41bc2cc22e3a94d799d44de": { + "query": "\n SELECT f.url url, f.id id, f.version_id version_id, v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE h.algorithm = $2 AND h.hash = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "project_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + } + }, "017c9fd0c8103c590489453a25b3317e6790a21f388bcf7ec8c93cd26255f368": { "query": "\n SELECT id, team_id, role, permissions, accepted\n FROM team_members\n WHERE (user_id = $1 AND accepted = TRUE)\n ", "describe": { @@ -56,6 +95,25 @@ "nullable": [] } }, + "03209c5bda2d704e688439919a7b3903db6ad7caebf7ddafb3ea52d312d47bfb": { + "query": "\n INSERT INTO users (\n id, github_id, username, name, email,\n avatar_url, bio, created\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz" + ] + }, + "nullable": [] + } + }, "03c196a6b0c287b9d913559442b1ea679c35634e33f94197f587532757cb7385": { "query": "\n DELETE FROM notifications_actions\n WHERE notification_id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", "describe": { @@ -182,6 +240,27 @@ "nullable": [] } }, + "0a1e0cbcdfe68e111326e2dc070e7903d4abb92180c7957f2408b4d6f810c2e5": { + "query": "\n SELECT v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE h.algorithm = $2 AND h.hash = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "0ca11a32b2860e4f5c3d20892a5be3cb419e084f42ba0f98e09b9995027fcc4e": { "query": "\n SELECT id FROM statuses\n WHERE status = $1\n ", "describe": { @@ -290,22 +369,64 @@ ] } }, - "1220d15a56dbf823eaa452fbafa17442ab0568bc81a31fa38e16e3df3278e5f9": { - "query": "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)", + "0fd612828feb009aab2f78fbbb271dcdd9ba2770b7d6f323415b958100cd3fb8": { + "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY n.id, n.user_id\n ORDER BY n.created DESC;\n ", "describe": { "columns": [ { "ordinal": 0, - "name": "exists", + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "text", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "link", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "created", + "type_info": "Timestamptz" + }, + { + "ordinal": 6, + "name": "read", "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "actions", + "type_info": "Text" } ], "parameters": { "Left": [ - "Int8" + "Int8Array" ] }, "nullable": [ + false, + false, + false, + false, + false, + false, + false, null ] } @@ -331,6 +452,51 @@ ] } }, + "155910d402d6cd3440a0fee53259ae3c397c6d1d98f97a38880078bd9192b6fa": { + "query": "\n SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE h.algorithm = $2 AND h.hash IN (SELECT * FROM UNNEST($1::bytea[]))\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "url", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "hash", + "type_info": "Bytea" + }, + { + "ordinal": 2, + "name": "algorithm", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "project_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "ByteaArray", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + } + }, "15b8ea323c2f6d03c2e385d9c46d7f13460764f2f106fd638226c42ae0217f75": { "query": "\n DELETE FROM notifications\n WHERE user_id = $1\n ", "describe": { @@ -499,8 +665,34 @@ ] } }, - "18f9188a2c89707ceea46b1ebc66c0cc09da21240dbf36c1c7c32736aed44a38": { - "query": "\n SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY m.id, s.id, cs.id, ss.id, l.id;\n ", + "19dc22c4d6d14222f8e8bace74c2961761c53b7375460ade15af921754d5d7da": { + "query": "\n UPDATE mods\n SET license = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, + "1ab781d26c93aa74bf90b78b74b99e50004d25d42d56b734e5e83f2333d0c0d2": { + "query": "\n UPDATE users\n SET avatar_url = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, + "1b6c38ac65be4d4f10decabf0e80fd45c8e4d15e4f916ac1bdc9348b188fc469": { + "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n rc.channel release_channel, v.featured featured,\n STRING_AGG(DISTINCT gv.version, ',') game_versions, STRING_AGG(DISTINCT l.loader, ',') loaders,\n STRING_AGG(DISTINCT f.id || ', ' || f.filename || ', ' || f.is_primary || ', ' || f.url, ' ,') files,\n STRING_AGG(DISTINCT h.algorithm || ', ' || encode(h.hash, 'escape') || ', ' || h.file_id, ' ,') hashes,\n STRING_AGG(DISTINCT d.dependency_id || ', ' || d.dependency_type, ' ,') dependencies\n FROM versions v\n INNER JOIN release_channels rc on v.release_channel = rc.id\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY v.id, rc.id\n ORDER BY v.date_published ASC;\n ", "describe": { "columns": [ { @@ -510,137 +702,77 @@ }, { "ordinal": 1, - "name": "title", - "type_info": "Varchar" + "name": "mod_id", + "type_info": "Int8" }, { "ordinal": 2, - "name": "description", - "type_info": "Varchar" + "name": "author_id", + "type_info": "Int8" }, { "ordinal": 3, - "name": "downloads", - "type_info": "Int4" + "name": "version_name", + "type_info": "Varchar" }, { "ordinal": 4, - "name": "follows", - "type_info": "Int4" + "name": "version_number", + "type_info": "Varchar" }, { "ordinal": 5, - "name": "icon_url", + "name": "changelog", "type_info": "Varchar" }, { "ordinal": 6, - "name": "body", + "name": "changelog_url", "type_info": "Varchar" }, { "ordinal": 7, - "name": "body_url", - "type_info": "Varchar" + "name": "date_published", + "type_info": "Timestamptz" }, { "ordinal": 8, - "name": "published", - "type_info": "Timestamptz" + "name": "downloads", + "type_info": "Int4" }, { "ordinal": 9, - "name": "updated", - "type_info": "Timestamptz" + "name": "release_channel", + "type_info": "Varchar" }, { "ordinal": 10, - "name": "status", - "type_info": "Int4" + "name": "featured", + "type_info": "Bool" }, { "ordinal": 11, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 17, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 18, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 19, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 20, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 21, - "name": "status_name", - "type_info": "Varchar" - }, - { - "ordinal": 22, - "name": "client_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 23, - "name": "server_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "short", - "type_info": "Varchar" - }, - { - "ordinal": 25, - "name": "license_name", - "type_info": "Varchar" - }, - { - "ordinal": 26, - "name": "categories", + "name": "game_versions", "type_info": "Text" }, { - "ordinal": 27, - "name": "versions", + "ordinal": 12, + "name": "loaders", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "files", + "type_info": "Text" + }, + { + "ordinal": 14, + "name": "hashes", + "type_info": "Text" + }, + { + "ordinal": 15, + "name": "dependencies", "type_info": "Text" } ], @@ -655,45 +787,20 @@ false, false, false, - true, false, true, false, false, false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - false, - false, - false, - false, false, null, + null, + null, + null, null ] } }, - "19dc22c4d6d14222f8e8bace74c2961761c53b7375460ade15af921754d5d7da": { - "query": "\n UPDATE mods\n SET license = $1\n WHERE (id = $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4", - "Int8" - ] - }, - "nullable": [] - } - }, "1c7b0eb4341af5a7942e52f632cf582561f10b4b6a41a082fb8a60f04ac17c6e": { "query": "SELECT EXISTS(SELECT 1 FROM states WHERE id=$1)", "describe": { @@ -848,6 +955,26 @@ ] } }, + "21ef50f46b7b3e62b91e7d067c1cb33806e14c33bb76d63c2711f822c44261f6": { + "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, "24e5daad907eec54505274f93952d5c20f4bbdd3f771eb0a2fdfa6324768df39": { "query": "\n SELECT short, name FROM licenses\n WHERE id = $1\n ", "describe": { @@ -1051,8 +1178,40 @@ "nullable": [] } }, - "2d5af30eec91c9086fe1beffdb648766986cb3e0ff00c0f3fc96f119f0a67c00": { - "query": "\n SELECT id, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "31853f131eaeb2aaedc2bcc27da387462408409af810337f6a8ef397f674fb44": { + "query": "\n SELECT DISTINCT loaders.loader FROM versions\n INNER JOIN loaders_versions lv ON lv.version_id = versions.id\n INNER JOIN loaders ON loaders.id = lv.loader_id\n WHERE versions.mod_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "loader", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, + "33fc96ac71cfa382991cfb153e89da1e9f43ebf5367c28b30c336b758222307b": { + "query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "365d2cdfdbac0b906580fc28cff69c9c27209c7e120da3d301737ef97ca4d535": { + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", "describe": { "columns": [ { @@ -1060,6 +1219,582 @@ "name": "id", "type_info": "Int8" }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 18, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 23, + "name": "client_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 24, + "name": "server_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 25, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 26, + "name": "license_name", + "type_info": "Varchar" + }, + { + "ordinal": 27, + "name": "project_type_name", + "type_info": "Varchar" + }, + { + "ordinal": 28, + "name": "categories", + "type_info": "Text" + }, + { + "ordinal": 29, + "name": "versions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + null, + null + ] + } + }, + "371048e45dd74c855b84cdb8a6a565ccbef5ad166ec9511ab20621c336446da6": { + "query": "\n UPDATE mods\n SET follows = follows - 1\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "381974f80a890a59f89c46b0c709e4511c0216eb8059ee47bb1e1456caf68fd7": { + "query": "\n INSERT INTO dependencies (dependent_id, dependency_id, dependency_type)\n VALUES ($1, $2, $3)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Varchar" + ] + }, + "nullable": [] + } + }, + "3831c1b321e47690f1f54597506a0d43362eda9540c56acb19c06532bba50b01": { + "query": "\n SELECT id, user_id, role, permissions, accepted\n FROM team_members\n WHERE team_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "role", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "permissions", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "accepted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + } + }, + "398ac436f5fe2f6a66544204b9ff01ae1ea1204edf03ffc16de657a861cfe0ba": { + "query": "\n DELETE FROM categories\n WHERE category = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, + "3b52d9f68ba23d1e3764f8df9f28bcaec0741101f6afd0c7c234b7f1b91054a4": { + "query": "\n UPDATE team_members\n SET accepted = TRUE\n WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Text" + ] + }, + "nullable": [] + } + }, + "3bdcbfa5abe43cc9b4f996f147277a7f6921cca00f82cad0ef5d85032c761a36": { + "query": "\n DELETE FROM mod_follows\n WHERE follower_id = $1 AND mod_id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [] + } + }, + "3d700aaeb0d5129ac8c297ee0542757435a50a35ec94582d9d6ce67aa5302291": { + "query": "\n UPDATE mods\n SET title = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Int8" + ] + }, + "nullable": [] + } + }, + "3f2f05653552ce8c1be95ce0a922ab41f52f40f8ff6c91c6621481102c8f35e3": { + "query": "\n INSERT INTO game_versions_versions (game_version_id, joining_version_id)\n VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, + "3fdece422b1c54bc0853fa3ddbb4c1d1a45ac7723c906544ea9b4f69e5b29dc1": { + "query": "\n SELECT name FROM side_types\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, + "413762398111e04074a2d8a1e4e03ed362b9167d397947f8d14e5ae330e3de0b": { + "query": "\n UPDATE versions\n SET downloads = downloads + 1\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, + "43660c74ef6a72b3fc68006a2f743737f1e4973788a5c954ffeaac151c16d0c1": { + "query": "\n SELECT status FROM statuses\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "status", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, + "43b793e2df30a6ace9e037e38bb4ea456656cfbe276c151e3a9e0a408d2c249f": { + "query": "\n UPDATE versions\n SET release_channel = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, + "449920c44d498adf8b771973d6034dc97e1c7f3ff4d9d23599af432f294ed564": { + "query": "\n INSERT INTO files (id, version_id, url, filename)\n VALUES ($1, $2, $3, $4)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Varchar", + "Varchar" + ] + }, + "nullable": [] + } + }, + "44bb1034872a80bbea122e04399470fd5f029b819c70cb6e0cb2db6d3193b97e": { + "query": "\n INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id)\n VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int4" + ] + }, + "nullable": [] + } + }, + "45f8a06abdd17fc437f5355ad109efcb5d7e247ef397b1a0cd98d7fb6bd9ce17": { + "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id)\n VALUES ($1, $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int4" + ] + }, + "nullable": [] + } + }, + "48294a4e0c594e80fff8d14a705aa7282f55e47cf3772e77f1d4bf4849008b60": { + "query": "\n SELECT follower_id FROM mod_follows\n WHERE mod_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "follower_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, + "49b2829b22f6ca82b3f62ea7962d8af22098cfa5a1fc1e06312bf1d3df382280": { + "query": "\n INSERT INTO categories (category, project_type, icon)\n VALUES ($1, $2, $3)\n ON CONFLICT (category, project_type, icon) DO NOTHING\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Int4", + "Varchar" + ] + }, + "nullable": [ + false + ] + } + }, + "4a4b4166248877eefcd63603945fdcd392f76812bdec7c70f8ffeb06ee7e737f": { + "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id\n WHERE tm.user_id = $1 AND tm.role = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + } + }, + "4a54d350b4695c32a802675506e85b0506fc62a63ca0ee5f38890824301d6515": { + "query": "\n UPDATE mods\n SET server_side = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, + "4b14b5c69f6a0ee4e06e41d7cea425c7c34d6db45895275a2ce8adfa28dc8f72": { + "query": "\n INSERT INTO project_types (name)\n VALUES ($1)\n ON CONFLICT (name) DO NOTHING\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Varchar" + ] + }, + "nullable": [ + false + ] + } + }, + "4b305fba5341b183cc07048aef48dc593c7a2fdf7abb82f7440e5a63786ebe7b": { + "query": "\n SELECT id, user_id, role, permissions, accepted\n FROM team_members\n WHERE (team_id = $1 AND user_id = $2 AND accepted = TRUE)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "role", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "permissions", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "accepted", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + } + }, + "4b74531cfe100a37f1ebfe0cee42926055f2c55fe8c8d5e6fff642c722dbb92f": { + "query": "\n SELECT project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "project_type", + "type_info": "Int4" + }, { "ordinal": 1, "name": "title", @@ -1163,7 +1898,7 @@ ], "parameters": { "Left": [ - "Int8Array" + "Int8" ] }, "nullable": [ @@ -1191,428 +1926,6 @@ ] } }, - "2fa070eef3fe8f708a1495104f78eda2bfa0fe19ada2bf66ac35fb2468631774": { - "query": "\n SELECT category FROM categories\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "category", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false - ] - } - }, - "3135db1c5309dac7580a731b2829397ae7bdd6c9a67b21e813f26a4f5aa251a9": { - "query": "\n SELECT status FROM statuses\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "status", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false - ] - } - }, - "31853f131eaeb2aaedc2bcc27da387462408409af810337f6a8ef397f674fb44": { - "query": "\n SELECT DISTINCT loaders.loader FROM versions\n INNER JOIN loaders_versions lv ON lv.version_id = versions.id\n INNER JOIN loaders ON loaders.id = lv.loader_id\n WHERE versions.mod_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "loader", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, - "33fc96ac71cfa382991cfb153e89da1e9f43ebf5367c28b30c336b758222307b": { - "query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, - "371048e45dd74c855b84cdb8a6a565ccbef5ad166ec9511ab20621c336446da6": { - "query": "\n UPDATE mods\n SET follows = follows - 1\n WHERE id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, - "381974f80a890a59f89c46b0c709e4511c0216eb8059ee47bb1e1456caf68fd7": { - "query": "\n INSERT INTO dependencies (dependent_id, dependency_id, dependency_type)\n VALUES ($1, $2, $3)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar" - ] - }, - "nullable": [] - } - }, - "3831c1b321e47690f1f54597506a0d43362eda9540c56acb19c06532bba50b01": { - "query": "\n SELECT id, user_id, role, permissions, accepted\n FROM team_members\n WHERE team_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "role", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "permissions", - "type_info": "Int8" - }, - { - "ordinal": 4, - "name": "accepted", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false - ] - } - }, - "389088b3ccff3a3c970aba3deef8831cca140b74ffc74e43a1162a9021428820": { - "query": "\n SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id mod_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE h.algorithm = $2 AND h.hash = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "version_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "filename", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "mod_id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Bytea", - "Text" - ] - }, - "nullable": [ - false, - false, - false, - false, - false - ] - } - }, - "398ac436f5fe2f6a66544204b9ff01ae1ea1204edf03ffc16de657a861cfe0ba": { - "query": "\n DELETE FROM categories\n WHERE category = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [] - } - }, - "3b52d9f68ba23d1e3764f8df9f28bcaec0741101f6afd0c7c234b7f1b91054a4": { - "query": "\n UPDATE team_members\n SET accepted = TRUE\n WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text" - ] - }, - "nullable": [] - } - }, - "3bdcbfa5abe43cc9b4f996f147277a7f6921cca00f82cad0ef5d85032c761a36": { - "query": "\n DELETE FROM mod_follows\n WHERE follower_id = $1 AND mod_id = $2\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [] - } - }, - "3d700aaeb0d5129ac8c297ee0542757435a50a35ec94582d9d6ce67aa5302291": { - "query": "\n UPDATE mods\n SET title = $1\n WHERE (id = $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - }, - "nullable": [] - } - }, - "3f2f05653552ce8c1be95ce0a922ab41f52f40f8ff6c91c6621481102c8f35e3": { - "query": "\n INSERT INTO game_versions_versions (game_version_id, joining_version_id)\n VALUES ($1, $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4", - "Int8" - ] - }, - "nullable": [] - } - }, - "413762398111e04074a2d8a1e4e03ed362b9167d397947f8d14e5ae330e3de0b": { - "query": "\n UPDATE versions\n SET downloads = downloads + 1\n WHERE id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, - "43b793e2df30a6ace9e037e38bb4ea456656cfbe276c151e3a9e0a408d2c249f": { - "query": "\n UPDATE versions\n SET release_channel = $1\n WHERE (id = $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4", - "Int8" - ] - }, - "nullable": [] - } - }, - "449920c44d498adf8b771973d6034dc97e1c7f3ff4d9d23599af432f294ed564": { - "query": "\n INSERT INTO files (id, version_id, url, filename)\n VALUES ($1, $2, $3, $4)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar", - "Varchar" - ] - }, - "nullable": [] - } - }, - "45f8a06abdd17fc437f5355ad109efcb5d7e247ef397b1a0cd98d7fb6bd9ce17": { - "query": "\n INSERT INTO mods_categories (joining_mod_id, joining_category_id)\n VALUES ($1, $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int4" - ] - }, - "nullable": [] - } - }, - "48294a4e0c594e80fff8d14a705aa7282f55e47cf3772e77f1d4bf4849008b60": { - "query": "\n SELECT follower_id FROM mod_follows\n WHERE mod_id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "follower_id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false - ] - } - }, - "49e36828e3a0214b48234435e34311735ae32e08d8be1270f8f0db4b27e708ba": { - "query": "\n INSERT INTO loaders (loader)\n VALUES ($1)\n ON CONFLICT (loader) DO NOTHING\n RETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Varchar" - ] - }, - "nullable": [ - false - ] - } - }, - "4a4b4166248877eefcd63603945fdcd392f76812bdec7c70f8ffeb06ee7e737f": { - "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id\n WHERE tm.user_id = $1 AND tm.role = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - }, - "nullable": [ - false - ] - } - }, - "4a54d350b4695c32a802675506e85b0506fc62a63ca0ee5f38890824301d6515": { - "query": "\n UPDATE mods\n SET server_side = $1\n WHERE (id = $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int4", - "Int8" - ] - }, - "nullable": [] - } - }, - "4b305fba5341b183cc07048aef48dc593c7a2fdf7abb82f7440e5a63786ebe7b": { - "query": "\n SELECT id, user_id, role, permissions, accepted\n FROM team_members\n WHERE (team_id = $1 AND user_id = $2 AND accepted = TRUE)\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "role", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "permissions", - "type_info": "Int8" - }, - { - "ordinal": 4, - "name": "accepted", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false - ] - } - }, "4c99c0840159d18e88cd6094a41117258f2337346c145d926b5b610c76b5125f": { "query": "\n SELECT c.category\n FROM mods_categories mc\n INNER JOIN categories c ON mc.joining_category_id=c.id\n WHERE mc.joining_mod_id = $1\n ", "describe": { @@ -1633,26 +1946,6 @@ ] } }, - "4c9e2190e2a68ffc093a69aaa1fc9384957138f57ac9cd85cbc6179613c13a08": { - "query": "SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "exists", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - } - }, "4e9f9eafbfd705dfc94571018cb747245a98ea61bad3fae4b3ce284229d99955": { "query": "\n UPDATE mods\n SET description = $1\n WHERE (id = $2)\n ", "describe": { @@ -1679,6 +1972,18 @@ "nullable": [] } }, + "4fa53dab6de86711825c032077fbe4985d5edf8aeec9003be900d162f46b3631": { + "query": "\n DELETE FROM loaders_project_types\n WHERE joining_loader_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [] + } + }, "4fb5bd341369b4beb6b4a88de296b608ea5441a96db9f7360fbdccceb4628202": { "query": "\n UPDATE mods\n SET slug = LOWER($1)\n WHERE (id = $2)\n ", "describe": { @@ -1720,116 +2025,6 @@ "nullable": [] } }, - "529f4c62eb8e98909ea7ec504f86647f105b1717990dd0c422919234060dc30f": { - "query": "\n SELECT v.id id, v.mod_id mod_id, v.author_id author_id, v.name version_name, v.version_number version_number,\n v.changelog changelog, v.changelog_url changelog_url, v.date_published date_published, v.downloads downloads,\n rc.channel release_channel, v.featured featured,\n STRING_AGG(DISTINCT gv.version, ',') game_versions, STRING_AGG(DISTINCT l.loader, ',') loaders,\n STRING_AGG(DISTINCT f.id || ', ' || f.filename || ', ' || f.is_primary || ', ' || f.url, ' ,') files,\n STRING_AGG(DISTINCT h.algorithm || ', ' || encode(h.hash, 'escape') || ', ' || h.file_id, ' ,') hashes,\n STRING_AGG(DISTINCT d.dependency_id || ', ' || d.dependency_type, ' ,') dependencies\n FROM versions v\n INNER JOIN release_channels rc on v.release_channel = rc.id\n LEFT OUTER JOIN game_versions_versions gvv on v.id = gvv.joining_version_id\n LEFT OUTER JOIN game_versions gv on gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv on v.id = lv.version_id\n LEFT OUTER JOIN loaders l on lv.loader_id = l.id\n LEFT OUTER JOIN files f on v.id = f.version_id\n LEFT OUTER JOIN hashes h on f.id = h.file_id\n LEFT OUTER JOIN dependencies d on v.id = d.dependent_id\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY v.id, rc.id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "author_id", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "version_name", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "changelog", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "changelog_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 9, - "name": "release_channel", - "type_info": "Varchar" - }, - { - "ordinal": 10, - "name": "featured", - "type_info": "Bool" - }, - { - "ordinal": 11, - "name": "game_versions", - "type_info": "Text" - }, - { - "ordinal": 12, - "name": "loaders", - "type_info": "Text" - }, - { - "ordinal": 13, - "name": "files", - "type_info": "Text" - }, - { - "ordinal": 14, - "name": "hashes", - "type_info": "Text" - }, - { - "ordinal": 15, - "name": "dependencies", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false, - null, - null, - null, - null, - null - ] - } - }, "53a8966ac345cc334ad65ea907be81af74e90b1217696c7eedcf8a8e3fca736e": { "query": "\n UPDATE versions\n SET version_number = $1\n WHERE (id = $2)\n ", "describe": { @@ -1907,26 +2102,6 @@ "nullable": [] } }, - "56578cb820533fbc17599ee8744b8f563cf4852e7c62a6a935765d3c60235e7b": { - "query": "\n SELECT version FROM game_versions\n WHERE type = $1\n ORDER BY created DESC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "version", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false - ] - } - }, "57bb3db92e6a8fb8606005be955e2379f13a04f101f91358322a591a860a7f9e": { "query": "\n SELECT id FROM reports\n ORDER BY created ASC\n LIMIT $1;\n ", "describe": { @@ -2015,6 +2190,26 @@ ] } }, + "5c3b340d278c356b6bc2cd7110e5093a7d1ad982ae0f468f8fff7c54e4e6603a": { + "query": "\n SELECT id FROM project_types\n WHERE name = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "5c4262689205aafdd97a74bee0003f39eef0a34c97f97a939c14fb8fe349f7eb": { "query": "\n UPDATE files\n SET is_primary = TRUE\n WHERE (id = $1)\n ", "describe": { @@ -2077,6 +2272,24 @@ "nullable": [] } }, + "5ee2dc5cda9bfc0395da5a4ebf234093e9b8135db5e4a0258b00fa16fb825faa": { + "query": "\n SELECT name FROM project_types\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false + ] + } + }, "5ff8fd471ff62f86aa95e52cee2723b31ec3d7fc53c3ef1454df40eef0ceff53": { "query": "\n SELECT version.id FROM (\n SELECT DISTINCT ON(v.id) v.id, v.date_published FROM versions v\n INNER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n INNER JOIN game_versions gv on gvv.game_version_id = gv.id AND (cardinality($2::varchar[]) = 0 OR gv.version = ANY($2::varchar[]))\n INNER JOIN loaders_versions lv ON lv.version_id = v.id\n INNER JOIN loaders l on lv.loader_id = l.id AND (cardinality($3::varchar[]) = 0 OR l.loader = ANY($3::varchar[]))\n WHERE v.mod_id = $1\n ) AS version\n ORDER BY version.date_published ASC\n ", "describe": { @@ -2129,188 +2342,6 @@ ] } }, - "6612afe698ec5ad3a9a98294d12fa5b04ccfeac4b31365b3821ac8e2ae6c5768": { - "query": "\n SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id = $1\n GROUP BY m.id, s.id, cs.id, ss.id, l.id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 5, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "body", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 8, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 11, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 16, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 17, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 18, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 19, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 20, - "name": "slug", - "type_info": "Varchar" - }, - { - "ordinal": 21, - "name": "status_name", - "type_info": "Varchar" - }, - { - "ordinal": 22, - "name": "client_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 23, - "name": "server_side_type", - "type_info": "Varchar" - }, - { - "ordinal": 24, - "name": "short", - "type_info": "Varchar" - }, - { - "ordinal": 25, - "name": "license_name", - "type_info": "Varchar" - }, - { - "ordinal": 26, - "name": "categories", - "type_info": "Text" - }, - { - "ordinal": 27, - "name": "versions", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true, - false, - false, - false, - false, - false, - null, - null - ] - } - }, "67d021f0776276081d3c50ca97afa6b78b98860bf929009e845e9c00a192e3b5": { "query": "\n SELECT id FROM report_types\n WHERE name = $1\n ", "describe": { @@ -2352,6 +2383,39 @@ ] } }, + "6ace87c4d5a960ba70eb057f1fb5672f4af6da433a420260a67ebd5ea2f4cb7f": { + "query": "\n SELECT h.hash hash, h.algorithm algorithm, f.version_id version_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n WHERE h.algorithm = $2 AND h.hash IN (SELECT * FROM UNNEST($1::bytea[]))\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "hash", + "type_info": "Bytea" + }, + { + "ordinal": 1, + "name": "algorithm", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "version_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "ByteaArray", + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + } + }, "6b28cb8b54ef57c9b6f03607611f688455f0e2b27eb5deda5a8cbc5b506b4602": { "query": "\n DELETE FROM mods\n WHERE id = $1\n ", "describe": { @@ -2404,6 +2468,42 @@ "nullable": [] } }, + "6dc7ec051df26915ab8ee824c3caa45dbac2bda5e2e55958e463cdc0f8754ce2": { + "query": "\n SELECT l.id id, l.loader loader, l.icon icon,\n STRING_AGG(DISTINCT pt.name, ',') project_types\n FROM loaders l\n LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id\n LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id\n GROUP BY l.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "loader", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "icon", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "project_types", + "type_info": "Text" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + null + ] + } + }, "6f1fb4c3269b2a8190f328df025be76241eae757d9c4f3e5eb1cc01b191837df": { "query": "\n DELETE FROM mods_categories\n WHERE joining_mod_id = $1\n ", "describe": { @@ -2433,6 +2533,86 @@ "nullable": [] } }, + "6fd610bfa35eaa849c64f5335dbde91159b22fbdb7bbfa4d41ef251e3a3b0b8e": { + "query": "\n SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog, v.changelog_url, v.date_published, v.downloads,\n v.release_channel, v.featured\n FROM versions v\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\n ORDER BY v.date_published ASC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "author_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "name", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "changelog", + "type_info": "Varchar" + }, + { + "ordinal": 6, + "name": "changelog_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "date_published", + "type_info": "Timestamptz" + }, + { + "ordinal": 8, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "release_channel", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "featured", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + false, + false + ] + } + }, "70cdf1b4a17405974909d89b1437a8425792d620f9ed67fd8e31e004e4609e83": { "query": "\n UPDATE users\n SET username = $1\n WHERE (id = $2)\n ", "describe": { @@ -2446,23 +2626,13 @@ "nullable": [] } }, - "71db1bc306ff6da3a92544e1585aa11c5627b50d95b15e794b2fa5dc838ea1a3": { - "query": "\n SELECT mod_id, version_number, author_id\n FROM versions\n WHERE id = $1\n ", + "72ad6f4be40d7620a0ec557e3806da41ce95335aeaa910fe35aca2ec7c3f09b6": { + "query": "\n SELECT id FROM users\n WHERE id = $1\n ", "describe": { "columns": [ { "ordinal": 0, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "author_id", + "name": "id", "type_info": "Int8" } ], @@ -2472,25 +2642,10 @@ ] }, "nullable": [ - false, - false, false ] } }, - "7269f523a289e9d0dfe710074492e2eb547359e62d42234abed698871905edd6": { - "query": "\n UPDATE users\n SET avatar_url = $1\n WHERE (id = $2)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - }, - "nullable": [] - } - }, "72c1e6de8f2c8d89be030454eeab6d5c9695164af2ebfb8d7e94b2deee4f130d": { "query": "\n SELECT c.category\n FROM mods_categories mc\n INNER JOIN categories c ON mc.joining_category_id=c.id\n WHERE mc.joining_mod_id = $1\n ", "describe": { @@ -2553,68 +2708,6 @@ ] } }, - "73ab32d116e2785332f9c671a61b0173cede5db03ed40b971162283c21b0e9b9": { - "query": "\n SELECT n.id, n.user_id, n.title, n.text, n.link, n.created, n.read,\n STRING_AGG(DISTINCT na.id || ', ' || na.title || ', ' || na.action_route || ', ' || na.action_route_method, ' ,') actions\n FROM notifications n\n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id\n WHERE n.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY n.id, n.user_id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "text", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "link", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "created", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "read", - "type_info": "Bool" - }, - { - "ordinal": 7, - "name": "actions", - "type_info": "Text" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - null - ] - } - }, "73bdd6c9e7cd8c1ed582261aebdee0f8fd2734e712ef288a2608564c918009cb": { "query": "\n DELETE FROM versions WHERE id = $1\n ", "describe": { @@ -2627,6 +2720,26 @@ "nullable": [] } }, + "75a860ca8087536a9fcf932846341c8bd322d314231bb8acac124d1cea93270b": { + "query": "\n SELECT mf.mod_id FROM mod_follows mf\n WHERE mf.follower_id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "mod_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + } + }, "76db1c204139e18002e5751c3dcefff79791a1dd852b62d34fcf008151e8945a": { "query": "\n SELECT id, short, name FROM donation_platforms\n ", "describe": { @@ -2657,26 +2770,6 @@ ] } }, - "7826a4f18add285afe556335581474071e601361775ef85c91074d70287392b9": { - "query": "\n SELECT version FROM game_versions\n WHERE major = $1\n ORDER BY created DESC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "version", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Bool" - ] - }, - "nullable": [ - false - ] - } - }, "796f057ea8eb5b01d3eedeee9840fb37464ea567f32871953fb07e14ed86af1c": { "query": "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", "describe": { @@ -2698,6 +2791,152 @@ ] } }, + "79aa918a1dfb8e7cc958c4b7a75172ddb38fc28a95c881e766ecbcef5fd2475a": { + "query": "\n SELECT id, project_type, title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 18, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "slug", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true + ] + } + }, "79b896b1a8ddab285294638302976b75d0d915f36036383cc21bd2fc48d4502c": { "query": "\n DELETE FROM loaders_versions WHERE version_id = $1\n ", "describe": { @@ -2731,24 +2970,34 @@ ] } }, - "7d760d046292b81a88c5551ce8cde776719ce9d647f04d2928c6f6c122a8ee70": { - "query": "\n SELECT mf.mod_id FROM mod_follows mf\n WHERE mf.follower_id = $1\n ", + "7a3183f77f403d2272665727affb07775a9304cbe1fb8ee7e603d779edb95d03": { + "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13,\n $14, $15, $16, $17,\n LOWER($18), $19\n )\n ", "describe": { - "columns": [ - { - "ordinal": 0, - "name": "mod_id", - "type_info": "Int8" - } - ], + "columns": [], "parameters": { "Left": [ - "Int8" + "Int8", + "Int8", + "Varchar", + "Varchar", + "Varchar", + "Timestamptz", + "Int4", + "Varchar", + "Varchar", + "Varchar", + "Varchar", + "Int4", + "Varchar", + "Int4", + "Int4", + "Varchar", + "Int4", + "Text", + "Int4" ] }, - "nullable": [ - false - ] + "nullable": [] } }, "7e73d3a17807f57ba6def5ff718e6dcb3a65ef8da653d839560b24635334cf05": { @@ -2871,6 +3120,18 @@ ] } }, + "87fd169e19ba231c6cf131ad2841d5c3b95adde53e5ed4000f8e7d54c0e87320": { + "query": "\n DELETE FROM project_types\n WHERE name = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + } + }, "89310b2bc5f020744a9a42dae6f15dfebc1544cdd754939f0d09714353f2aa7c": { "query": "\n SELECT id, team_id, role, permissions, accepted\n FROM team_members\n WHERE user_id = $1\n ", "describe": { @@ -3011,140 +3272,6 @@ ] } }, - "8f02d5c2b9095c21498802f01cefdbc57a5f1c2a7aee717ba19daaffc498c5cd": { - "query": "\n SELECT title, description, downloads, follows,\n icon_url, body, body_url, published,\n updated, status,\n issues_url, source_url, wiki_url, discord_url, license_url,\n team_id, client_side, server_side, license, slug\n FROM mods\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "title", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "description", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "follows", - "type_info": "Int4" - }, - { - "ordinal": 4, - "name": "icon_url", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "body", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "body_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "updated", - "type_info": "Timestamptz" - }, - { - "ordinal": 9, - "name": "status", - "type_info": "Int4" - }, - { - "ordinal": 10, - "name": "issues_url", - "type_info": "Varchar" - }, - { - "ordinal": 11, - "name": "source_url", - "type_info": "Varchar" - }, - { - "ordinal": 12, - "name": "wiki_url", - "type_info": "Varchar" - }, - { - "ordinal": 13, - "name": "discord_url", - "type_info": "Varchar" - }, - { - "ordinal": 14, - "name": "license_url", - "type_info": "Varchar" - }, - { - "ordinal": 15, - "name": "team_id", - "type_info": "Int8" - }, - { - "ordinal": 16, - "name": "client_side", - "type_info": "Int4" - }, - { - "ordinal": 17, - "name": "server_side", - "type_info": "Int4" - }, - { - "ordinal": 18, - "name": "license", - "type_info": "Int4" - }, - { - "ordinal": 19, - "name": "slug", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - true, - true, - true, - true, - true, - false, - false, - false, - false, - true - ] - } - }, "8f706d78ac4235ea04c59e2c220a4791e1d08fdf287b783b4aaef36fd2445467": { "query": "\n DELETE FROM loaders\n WHERE loader = $1\n ", "describe": { @@ -3192,26 +3319,6 @@ "nullable": [] } }, - "97143e41c18d191d09d244113b7b6cdf5bd6ab89c62ac46d0980d700ab288f48": { - "query": "\n SELECT name FROM side_types\n WHERE id = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "name", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false - ] - } - }, "97690dda7edea8c985891cae5ad405f628ed81e333bc88df5493c928a4324d43": { "query": "SELECT EXISTS(SELECT 1 FROM reports WHERE id=$1)", "describe": { @@ -3232,6 +3339,100 @@ ] } }, + "9782399687ae7a79ea2931e8d3eb38c5c786240d214ff7fd7d0ae39f2d683108": { + "query": "\n SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv\n ORDER BY created DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "version_", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "type_", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "created", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + } + }, + "98841ab141b5133690c2d1d1a37de982b960a5d75d2aa98565fadb4a1ac206a1": { + "query": "\n SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv\n WHERE type = $1\n ORDER BY created DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "version_", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "type_", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "created", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + } + }, + "9985165594f04fb1d68e2e415a996b6553e8b5c91f121df3a9194806df10a197": { + "query": "\n SELECT name FROM side_types\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, "99a1eac69d7f5a5139703df431e6a5c3012a90143a8c635f93632f04d0bc41d4": { "query": "\n UPDATE mods\n SET wiki_url = $1\n WHERE (id = $2)\n ", "describe": { @@ -3347,86 +3548,6 @@ ] } }, - "a8f8acda6246fe82cc16c8804589102bfb176980fbcb93ca131f524ac395d86a": { - "query": "\n SELECT v.id, v.mod_id, v.author_id, v.name, v.version_number,\n v.changelog, v.changelog_url, v.date_published, v.downloads,\n v.release_channel, v.featured\n FROM versions v\n WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[]))\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "mod_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "author_id", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "name", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "version_number", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "changelog", - "type_info": "Varchar" - }, - { - "ordinal": 6, - "name": "changelog_url", - "type_info": "Varchar" - }, - { - "ordinal": 7, - "name": "date_published", - "type_info": "Timestamptz" - }, - { - "ordinal": 8, - "name": "downloads", - "type_info": "Int4" - }, - { - "ordinal": 9, - "name": "release_channel", - "type_info": "Int4" - }, - { - "ordinal": 10, - "name": "featured", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [ - "Int8Array" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - false - ] - } - }, "a90bb6904e1b790c0e29e060dac5ba4c2a6087e07c1197dc1f59f0aff31944c9": { "query": "\n DELETE FROM states\n WHERE expires < CURRENT_DATE\n ", "describe": { @@ -3537,6 +3658,47 @@ ] } }, + "aaec67a66b58dec36339c14000b319aed1b0ebb1324fc85e34d14c6430c26657": { + "query": "\n SELECT id FROM categories\n WHERE category = $1 AND project_type = $2\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text", + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, + "ac2d17b7d7147b14f072c15ffa214c14f32f27ffa6a3c2b2a5f80f3ad49ca5e9": { + "query": "\n SELECT id FROM users\n WHERE LOWER(username) = LOWER($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "ac840a3ba466cfa1f914a1e44fcc9052bd1e0e908140e7147d1ff72d1794cfbf": { "query": "\n SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)\n ", "describe": { @@ -3607,46 +3769,6 @@ "nullable": [] } }, - "b3c1b38d2e72c5ec9e6f34d497fb6eb5d01d6cdd07f38ee4a2bbae3b92911df7": { - "query": "\n SELECT version FROM game_versions\n WHERE major = $1 AND type = $2\n ORDER BY created DESC\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "version", - "type_info": "Varchar" - } - ], - "parameters": { - "Left": [ - "Bool", - "Text" - ] - }, - "nullable": [ - false - ] - } - }, - "b446a7ddda6860b72517501332a51e64e76f6ab6de6527ad86b4c77db71f6037": { - "query": "\n INSERT INTO users (\n id, github_id, username, name, email,\n avatar_url, bio, created\n )\n VALUES (\n $1, $2, LOWER($3), $4, $5,\n $6, $7, $8\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Timestamptz" - ] - }, - "nullable": [] - } - }, "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169": { "query": "\n DELETE FROM users\n WHERE id = $1\n ", "describe": { @@ -3696,6 +3818,45 @@ "nullable": [] } }, + "b90ec97f24315f2eeb918d4be26b3bfb1191548bb26626eee844314d25a894be": { + "query": "\n SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv\n WHERE major = $1 AND type = $2\n ORDER BY created DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "version_", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "type_", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "created", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Bool", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + } + }, "b9399840dbbf807a03d69b7fcb3bd479ef20920ab1e3c91706a1c2c7089f48e7": { "query": "\n INSERT INTO teams (id)\n VALUES ($1)\n ", "describe": { @@ -3871,6 +4032,200 @@ "nullable": [] } }, + "bf35a3797ff92917ba3568284b5dbae4ae08eedf3516b39b9a02f8bf03164bc3": { + "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[]))\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "project_type", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "title", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "downloads", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "follows", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "icon_url", + "type_info": "Varchar" + }, + { + "ordinal": 7, + "name": "body", + "type_info": "Varchar" + }, + { + "ordinal": 8, + "name": "body_url", + "type_info": "Varchar" + }, + { + "ordinal": 9, + "name": "published", + "type_info": "Timestamptz" + }, + { + "ordinal": 10, + "name": "updated", + "type_info": "Timestamptz" + }, + { + "ordinal": 11, + "name": "status", + "type_info": "Int4" + }, + { + "ordinal": 12, + "name": "issues_url", + "type_info": "Varchar" + }, + { + "ordinal": 13, + "name": "source_url", + "type_info": "Varchar" + }, + { + "ordinal": 14, + "name": "wiki_url", + "type_info": "Varchar" + }, + { + "ordinal": 15, + "name": "discord_url", + "type_info": "Varchar" + }, + { + "ordinal": 16, + "name": "license_url", + "type_info": "Varchar" + }, + { + "ordinal": 17, + "name": "team_id", + "type_info": "Int8" + }, + { + "ordinal": 18, + "name": "client_side", + "type_info": "Int4" + }, + { + "ordinal": 19, + "name": "server_side", + "type_info": "Int4" + }, + { + "ordinal": 20, + "name": "license", + "type_info": "Int4" + }, + { + "ordinal": 21, + "name": "slug", + "type_info": "Varchar" + }, + { + "ordinal": 22, + "name": "status_name", + "type_info": "Varchar" + }, + { + "ordinal": 23, + "name": "client_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 24, + "name": "server_side_type", + "type_info": "Varchar" + }, + { + "ordinal": 25, + "name": "short", + "type_info": "Varchar" + }, + { + "ordinal": 26, + "name": "license_name", + "type_info": "Varchar" + }, + { + "ordinal": 27, + "name": "project_type_name", + "type_info": "Varchar" + }, + { + "ordinal": 28, + "name": "categories", + "type_info": "Text" + }, + { + "ordinal": 29, + "name": "versions", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8Array" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + true, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + true, + false, + false, + false, + false, + false, + false, + null, + null + ] + } + }, "bf7f721664f5e0ed41adc41b5483037256635f28ff6c4e5d3cbcec4387f9c8ef": { "query": "SELECT EXISTS(SELECT 1 FROM users WHERE id=$1)", "describe": { @@ -4252,18 +4607,21 @@ "nullable": [] } }, - "cc8b672c2733bfd110ed3361c6f477b185b530228c7206cb641dbaa40e41ea9f": { - "query": "\n SELECT loader FROM loaders\n ", + "cc8eeb14e2069b9e4f92b224d42b283e569258d61be3cc3b3f7564f0dadac89b": { + "query": "\n INSERT INTO loaders (loader, icon)\n VALUES ($1, $2)\n ON CONFLICT (loader, icon) DO NOTHING\n RETURNING id\n ", "describe": { "columns": [ { "ordinal": 0, - "name": "loader", - "type_info": "Varchar" + "name": "id", + "type_info": "Int4" } ], "parameters": { - "Left": [] + "Left": [ + "Varchar", + "Varchar" + ] }, "nullable": [ false @@ -4342,35 +4700,6 @@ "nullable": [] } }, - "ce4e3569e69bb87b28af75f6f836715357b290704896edc661185e6a9c3f778e": { - "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, discord_url,\n client_side, server_side, license_url, license,\n slug\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13,\n $14, $15, $16, $17,\n LOWER($18)\n )\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar", - "Varchar", - "Varchar", - "Timestamptz", - "Int4", - "Varchar", - "Varchar", - "Varchar", - "Varchar", - "Int4", - "Varchar", - "Int4", - "Int4", - "Varchar", - "Int4", - "Text" - ] - }, - "nullable": [] - } - }, "cf031f19c7882833a8a30348ee90175a5d8b1fb7d9645c5deb2dc68c6eb33683": { "query": "\n SELECT id FROM release_channels\n WHERE channel = $1\n ", "describe": { @@ -4443,6 +4772,44 @@ ] } }, + "d4f7661c0156b04720d83ec5d76d901dd1dff23b1c93c7e2fde650f43c8ed86a": { + "query": "\n SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv\n WHERE major = $1\n ORDER BY created DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "version_", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "type_", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "created", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Bool" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + } + }, "d5b00d6237b04018822db529995f0b001cd1cabf5ca93b4aff37f12c4feb83f6": { "query": "\n INSERT INTO donation_platforms (short, name)\n VALUES ($1, $2)\n ON CONFLICT (short) DO NOTHING\n RETURNING id\n ", "describe": { @@ -4484,6 +4851,42 @@ ] } }, + "d7744589d9e20c48f6f726a8a540822c1e521b791ebc2fee86a1108d442aedb8": { + "query": "\n SELECT c.id id, c.category category, c.icon icon, pt.name project_type\n FROM categories c\n INNER JOIN project_types pt ON c.project_type = pt.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "category", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "icon", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "project_type", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false + ] + } + }, "d8020ed838c032c2c287dc0f08989b3ab7156f2571bc75505e6f57b0caeef9c7": { "query": "\n SELECT id FROM donation_platforms\n WHERE short = $1\n ", "describe": { @@ -4726,6 +5129,26 @@ ] } }, + "e8dc09a76d69e689d4b97527755aebfc049bbb4d470627a688eb9d56f01f8bd5": { + "query": "\n SELECT name FROM project_types\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false + ] + } + }, "e925b15ec46f0263c7775ba1ba00ed11cfd6749fa792d4eabed73b619f230585": { "query": "\n UPDATE mods\n SET status = $1\n WHERE (id = $2)\n ", "describe": { @@ -4833,18 +5256,65 @@ "nullable": [] } }, - "ed4c0b620d01cdcdd0c2b3b5727ae3485d51114ca76e17331cec0d244d7f972d": { - "query": "\n SELECT version FROM game_versions\n ORDER BY created DESC\n ", + "ef3d43d3424824eed67370f10cc0672581a95a169bf404022cbe3cac0415d99c": { + "query": "\n SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id project_id FROM hashes h\n INNER JOIN files f ON h.file_id = f.id\n INNER JOIN versions v ON v.id = f.version_id\n WHERE h.algorithm = $2 AND h.hash = $1\n ", "describe": { "columns": [ { "ordinal": 0, - "name": "version", + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "version_id", + "type_info": "Int8" + }, + { + "ordinal": 2, + "name": "filename", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "version_number", + "type_info": "Varchar" + }, + { + "ordinal": 4, + "name": "project_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + } + }, + "ef59f99fc0ab66ff5779d0e71c4a2134e2f26eed002ff9ea5626ea3e23518594": { + "query": "\n SELECT name FROM project_types pt\n INNER JOIN mods ON mods.project_type = pt.id\n WHERE mods.id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "name", "type_info": "Varchar" } ], "parameters": { - "Left": [] + "Left": [ + "Int8" + ] }, "nullable": [ false @@ -4871,26 +5341,6 @@ ] } }, - "f12ae54acf02e06e9b8774e8c2ea95058a78f6d724645adcd02f9dea6538024f": { - "query": "\n INSERT INTO categories (category)\n VALUES ($1)\n ON CONFLICT (category) DO NOTHING\n RETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Varchar" - ] - }, - "nullable": [ - false - ] - } - }, "f22e9aee090f9952cf795a3540c03b0a5036dab0b740847d05e03d4565756283": { "query": "\n DELETE FROM team_members\n WHERE user_id = $1\n ", "describe": { @@ -4903,6 +5353,52 @@ "nullable": [] } }, + "f23fcac002c2694e8b8ff96708e4178428d24972a120f068b1897981a1ff988b": { + "query": "\n SELECT id, name FROM project_types\n WHERE name IN (SELECT * FROM UNNEST($1::varchar[]))\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Varchar" + } + ], + "parameters": { + "Left": [ + "VarcharArray" + ] + }, + "nullable": [ + false, + false + ] + } + }, + "f3a8ad4a802dde0eb9304078e0368066e7d48121dfe73a63b2911b0998840a79": { + "query": "\n SELECT id FROM users\n WHERE LOWER(username) = LOWER($1)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + } + }, "f453b43772c4d2d9d09dc389eb95482cc75e7f0eaf9dc7ff48cf40f22f1497cc": { "query": "\n UPDATE users\n SET bio = $1\n WHERE (id = $2)\n ", "describe": { diff --git a/src/database/cache/mod.rs b/src/database/cache/mod.rs new file mode 100644 index 00000000..442796b8 --- /dev/null +++ b/src/database/cache/mod.rs @@ -0,0 +1,44 @@ +//pub mod project_cache; +//pub mod project_query_cache; +#[macro_export] +macro_rules! generate_cache { + ($name:ident,$id:ty, $val:ty, $cache_name:ident, $mod_name:ident, $getter_name:ident, $setter_name:ident) => { + pub mod $mod_name { + use cached::async_mutex::Mutex; + use cached::{Cached, SizedCache}; + use lazy_static::lazy_static; + lazy_static! { + static ref $cache_name: Mutex> = + Mutex::new(SizedCache::with_size(400)); + } + + pub async fn $getter_name<'a>(id: $id) -> Option<$val> { + let mut cache = $cache_name.lock().await; + Cached::cache_get(&mut *cache, &id).map(|e| e.clone()) + } + pub async fn $setter_name<'a>(id: $id, val: &$val) { + let mut cache = $cache_name.lock().await; + Cached::cache_set(&mut *cache, id, val.clone()); + } + } + }; +} + +generate_cache!( + project, + String, + crate::database::Project, + PROJECT_CACHE, + project_cache, + get_cache_project, + set_cache_project +); +generate_cache!( + query_project, + String, + crate::database::models::project_item::QueryProject, + QUERY_PROJECT_CACHE, + query_project_cache, + get_cache_query_project, + set_cache_query_project +); diff --git a/src/database/mod.rs b/src/database/mod.rs index 73d2d193..7c7f600d 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,7 +1,7 @@ +mod cache; pub mod models; mod postgres_database; - -pub use models::Mod; +pub use models::Project; pub use models::Version; pub use postgres_database::check_for_migrations; pub use postgres_database::connect; diff --git a/src/database/models/categories.rs b/src/database/models/categories.rs index 54c114d3..4ffc477e 100644 --- a/src/database/models/categories.rs +++ b/src/database/models/categories.rs @@ -2,19 +2,30 @@ use super::ids::*; use super::DatabaseError; use futures::TryStreamExt; +pub struct ProjectType { + pub id: ProjectTypeId, + pub name: String, +} + pub struct Loader { pub id: LoaderId, pub loader: String, + pub icon: String, + pub supported_project_types: Vec, } pub struct GameVersion { pub id: GameVersionId, pub version: String, + pub version_type: String, + pub date: chrono::DateTime, } pub struct Category { pub id: CategoryId, pub category: String, + pub project_type: String, + pub icon: String, } pub struct ReportType { @@ -36,11 +47,17 @@ pub struct DonationPlatform { pub struct CategoryBuilder<'a> { pub name: Option<&'a str>, + pub project_type: Option<&'a ProjectTypeId>, + pub icon: Option<&'a str>, } impl Category { pub fn builder() -> CategoryBuilder<'static> { - CategoryBuilder { name: None } + CategoryBuilder { + name: None, + project_type: None, + icon: None, + } } pub async fn get_id<'a, E>(name: &str, exec: E) -> Result, DatabaseError> @@ -59,7 +76,36 @@ impl Category { SELECT id FROM categories WHERE category = $1 ", - name + name, + ) + .fetch_optional(exec) + .await?; + + Ok(result.map(|r| CategoryId(r.id))) + } + + pub async fn get_id_project<'a, E>( + name: &str, + project_type: ProjectTypeId, + exec: E, + ) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + if !name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return Err(DatabaseError::InvalidIdentifier(name.to_string())); + } + + let result = sqlx::query!( + " + SELECT id FROM categories + WHERE category = $1 AND project_type = $2 + ", + name, + project_type as ProjectTypeId ) .fetch_optional(exec) .await?; @@ -84,18 +130,27 @@ impl Category { Ok(result.category) } - pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> + pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " - SELECT category FROM categories + SELECT c.id id, c.category category, c.icon icon, pt.name project_type + FROM categories c + INNER JOIN project_types pt ON c.project_type = pt.id " ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| c.category)) }) - .try_collect::>() + .try_filter_map(|e| async { + Ok(e.right().map(|c| Category { + id: CategoryId(c.id), + category: c.category, + project_type: c.project_type, + icon: c.icon, + })) + }) + .try_collect::>() .await?; Ok(result) @@ -133,24 +188,49 @@ impl<'a> CategoryBuilder<'a> { .chars() .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { - Ok(Self { name: Some(name) }) + Ok(Self { + name: Some(name), + ..self + }) } else { Err(DatabaseError::InvalidIdentifier(name.to_string())) } } + pub fn project_type( + self, + project_type: &'a ProjectTypeId, + ) -> Result, DatabaseError> { + Ok(Self { + project_type: Some(project_type), + ..self + }) + } + + pub fn icon(self, icon: &'a str) -> Result, DatabaseError> { + Ok(Self { + icon: Some(icon), + ..self + }) + } + pub async fn insert<'b, E>(self, exec: E) -> Result where E: sqlx::Executor<'b, Database = sqlx::Postgres>, { + let id = *self + .project_type + .ok_or_else(|| DatabaseError::Other("No project type specified.".to_string()))?; let result = sqlx::query!( " - INSERT INTO categories (category) - VALUES ($1) - ON CONFLICT (category) DO NOTHING + INSERT INTO categories (category, project_type, icon) + VALUES ($1, $2, $3) + ON CONFLICT (category, project_type, icon) DO NOTHING RETURNING id ", - self.name + self.name, + id as ProjectTypeId, + self.icon ) .fetch_one(exec) .await?; @@ -161,11 +241,17 @@ impl<'a> CategoryBuilder<'a> { pub struct LoaderBuilder<'a> { pub name: Option<&'a str>, + pub icon: Option<&'a str>, + pub supported_project_types: Option<&'a [ProjectTypeId]>, } impl Loader { pub fn builder() -> LoaderBuilder<'static> { - LoaderBuilder { name: None } + LoaderBuilder { + name: None, + icon: None, + supported_project_types: None, + } } pub async fn get_id<'a, E>(name: &str, exec: E) -> Result, DatabaseError> @@ -209,24 +295,41 @@ impl Loader { Ok(result.loader) } - pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> + pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " - SELECT loader FROM loaders + SELECT l.id id, l.loader loader, l.icon icon, + STRING_AGG(DISTINCT pt.name, ',') project_types + FROM loaders l + LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id + LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id + GROUP BY l.id; " ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| c.loader)) }) - .try_collect::>() + .try_filter_map(|e| async { + Ok(e.right().map(|x| Loader { + id: LoaderId(x.id), + loader: x.loader, + icon: x.icon, + supported_project_types: x + .project_types + .unwrap_or_default() + .split(',') + .map(|x| x.to_string()) + .collect(), + })) + }) + .try_collect::>() .await?; Ok(result) } - // TODO: remove loaders with mods using them + // TODO: remove loaders with projects using them pub async fn remove<'a, E>(name: &str, exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, @@ -259,28 +362,74 @@ impl<'a> LoaderBuilder<'a> { .chars() .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { - Ok(Self { name: Some(name) }) + Ok(Self { + name: Some(name), + ..self + }) } else { Err(DatabaseError::InvalidIdentifier(name.to_string())) } } - pub async fn insert<'b, E>(self, exec: E) -> Result - where - E: sqlx::Executor<'b, Database = sqlx::Postgres>, - { + pub fn icon(self, icon: &'a str) -> Result, DatabaseError> { + Ok(Self { + icon: Some(icon), + ..self + }) + } + + pub fn supported_project_types( + self, + supported_project_types: &'a [ProjectTypeId], + ) -> Result, DatabaseError> { + Ok(Self { + supported_project_types: Some(supported_project_types), + ..self + }) + } + + pub async fn insert( + self, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + ) -> Result { let result = sqlx::query!( " - INSERT INTO loaders (loader) - VALUES ($1) - ON CONFLICT (loader) DO NOTHING + INSERT INTO loaders (loader, icon) + VALUES ($1, $2) + ON CONFLICT (loader, icon) DO NOTHING RETURNING id ", - self.name + self.name, + self.icon ) - .fetch_one(exec) + .fetch_one(&mut *transaction) .await?; + if let Some(project_types) = self.supported_project_types { + sqlx::query!( + " + DELETE FROM loaders_project_types + WHERE joining_loader_id = $1 + ", + result.id + ) + .execute(&mut *transaction) + .await?; + + for project_type in project_types { + sqlx::query!( + " + INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) + VALUES ($1, $2) + ", + result.id, + project_type.0, + ) + .execute(&mut *transaction) + .await?; + } + } + Ok(LoaderId(result.id)) } } @@ -341,19 +490,24 @@ impl GameVersion { Ok(result.version) } - pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> + pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " - SELECT version FROM game_versions + SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv ORDER BY created DESC " ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) - .try_collect::>() + .try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion { + id: GameVersionId(c.id), + version: c.version_, + version_type: c.type_, + date: c.created + })) }) + .try_collect::>() .await?; Ok(result) @@ -363,7 +517,7 @@ impl GameVersion { version_type_option: Option<&str>, major_option: Option, exec: E, - ) -> Result, DatabaseError> + ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { @@ -373,7 +527,7 @@ impl GameVersion { if let Some(major) = major_option { result = sqlx::query!( " - SELECT version FROM game_versions + SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv WHERE major = $1 AND type = $2 ORDER BY created DESC ", @@ -381,35 +535,50 @@ impl GameVersion { version_type ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) - .try_collect::>() + .try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion { + id: GameVersionId(c.id), + version: c.version_, + version_type: c.type_, + date: c.created + })) }) + .try_collect::>() .await?; } else { result = sqlx::query!( " - SELECT version FROM game_versions + SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv WHERE type = $1 ORDER BY created DESC ", version_type ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) - .try_collect::>() + .try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion { + id: GameVersionId(c.id), + version: c.version_, + version_type: c.type_, + date: c.created + })) }) + .try_collect::>() .await?; } } else if let Some(major) = major_option { result = sqlx::query!( " - SELECT version FROM game_versions + SELECT gv.id id, gv.version version_, gv.type type_, gv.created created FROM game_versions gv WHERE major = $1 ORDER BY created DESC ", major ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) - .try_collect::>() + .try_filter_map(|e| async { Ok(e.right().map(|c| GameVersion { + id: GameVersionId(c.id), + version: c.version_, + version_type: c.type_, + date: c.created + })) }) + .try_collect::>() .await?; } else { result = Vec::new(); @@ -867,7 +1036,6 @@ impl ReportType { Ok(result) } - // TODO: remove loaders with mods using them pub async fn remove<'a, E>(name: &str, exec: E) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, @@ -925,3 +1093,156 @@ impl<'a> ReportTypeBuilder<'a> { Ok(ReportTypeId(result.id)) } } + +pub struct ProjectTypeBuilder<'a> { + pub name: Option<&'a str>, +} + +impl ProjectType { + pub fn builder() -> ProjectTypeBuilder<'static> { + ProjectTypeBuilder { name: None } + } + + pub async fn get_id<'a, E>(name: &str, exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + if !name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return Err(DatabaseError::InvalidIdentifier(name.to_string())); + } + + let result = sqlx::query!( + " + SELECT id FROM project_types + WHERE name = $1 + ", + name + ) + .fetch_optional(exec) + .await?; + + Ok(result.map(|r| ProjectTypeId(r.id))) + } + + pub async fn get_many_id<'a, E>( + names: &Vec, + exec: E, + ) -> Result, sqlx::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let project_types = sqlx::query!( + " + SELECT id, name FROM project_types + WHERE name IN (SELECT * FROM UNNEST($1::varchar[])) + ", + names + ) + .fetch_many(exec) + .try_filter_map(|e| async { + Ok(e.right().map(|x| ProjectType { + id: ProjectTypeId(x.id), + name: x.name, + })) + }) + .try_collect::>() + .await?; + + Ok(project_types) + } + + pub async fn get_name<'a, E>(id: ProjectTypeId, exec: E) -> Result + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT name FROM project_types + WHERE id = $1 + ", + id as ProjectTypeId + ) + .fetch_one(exec) + .await?; + + Ok(result.name) + } + + pub async fn list<'a, E>(exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT name FROM project_types + " + ) + .fetch_many(exec) + .try_filter_map(|e| async { Ok(e.right().map(|c| c.name)) }) + .try_collect::>() + .await?; + + Ok(result) + } + + // TODO: remove loaders with mods using them + pub async fn remove<'a, E>(name: &str, exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + use sqlx::Done; + + let result = sqlx::query!( + " + DELETE FROM project_types + WHERE name = $1 + ", + name + ) + .execute(exec) + .await?; + + if result.rows_affected() == 0 { + // Nothing was deleted + Ok(None) + } else { + Ok(Some(())) + } + } +} + +impl<'a> ProjectTypeBuilder<'a> { + /// The name of the project type. Must be ASCII alphanumeric or `-`/`_` + pub fn name(self, name: &'a str) -> Result, DatabaseError> { + if name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + Ok(Self { name: Some(name) }) + } else { + Err(DatabaseError::InvalidIdentifier(name.to_string())) + } + } + + pub async fn insert<'b, E>(self, exec: E) -> Result + where + E: sqlx::Executor<'b, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + INSERT INTO project_types (name) + VALUES ($1) + ON CONFLICT (name) DO NOTHING + RETURNING id + ", + self.name + ) + .fetch_one(exec) + .await?; + + Ok(ProjectTypeId(result.id)) + } +} diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index e5875c31..4adbe825 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -38,11 +38,11 @@ macro_rules! generate_ids { } generate_ids!( - pub generate_mod_id, - ModId, + pub generate_project_id, + ProjectId, 8, "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)", - ModId + ProjectId ); generate_ids!( pub generate_version_id, @@ -115,7 +115,11 @@ pub struct TeamMemberId(pub i64); #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] -pub struct ModId(pub i64); +pub struct ProjectId(pub i64); +#[derive(Copy, Clone, Debug, Type)] +#[sqlx(transparent)] +pub struct ProjectTypeId(pub i32); + #[derive(Copy, Clone, Debug, Type)] #[sqlx(transparent)] pub struct StatusId(pub i32); @@ -169,14 +173,14 @@ pub struct NotificationActionId(pub i32); use crate::models::ids; -impl From for ModId { - fn from(id: ids::ModId) -> Self { - ModId(id.0 as i64) +impl From for ProjectId { + fn from(id: ids::ProjectId) -> Self { + ProjectId(id.0 as i64) } } -impl From for ids::ModId { - fn from(id: ModId) -> Self { - ids::ModId(id.0 as u64) +impl From for ids::ProjectId { + fn from(id: ProjectId) -> Self { + ids::ProjectId(id.0 as u64) } } impl From for UserId { diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index 820df48d..b7246bf1 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -5,15 +5,15 @@ use thiserror::Error; pub mod categories; pub mod ids; -pub mod mod_item; pub mod notification_item; +pub mod project_item; pub mod report_item; pub mod team_item; pub mod user_item; pub mod version_item; pub use ids::*; -pub use mod_item::Mod; +pub use project_item::Project; pub use team_item::Team; pub use team_item::TeamMember; pub use user_item::User; @@ -62,7 +62,7 @@ impl ids::ChannelId { impl ids::StatusId { pub async fn get_id<'a, E>( - status: &crate::models::mods::ModStatus, + status: &crate::models::projects::ProjectStatus, exec: E, ) -> Result, DatabaseError> where @@ -84,7 +84,7 @@ impl ids::StatusId { impl ids::SideTypeId { pub async fn get_id<'a, E>( - side: &crate::models::mods::SideType, + side: &crate::models::projects::SideType, exec: E, ) -> Result, DatabaseError> where @@ -122,3 +122,22 @@ impl ids::DonationPlatformId { Ok(result.map(|r| ids::DonationPlatformId(r.id))) } } + +impl ids::ProjectTypeId { + pub async fn get_id<'a, E>(project_type: String, exec: E) -> Result, DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT id FROM project_types + WHERE name = $1 + ", + project_type + ) + .fetch_optional(exec) + .await?; + + Ok(result.map(|r| ids::ProjectTypeId(r.id))) + } +} diff --git a/src/database/models/notification_item.rs b/src/database/models/notification_item.rs index fec14e5a..4ac075d0 100644 --- a/src/database/models/notification_item.rs +++ b/src/database/models/notification_item.rs @@ -179,7 +179,8 @@ impl Notification { FROM notifications n LEFT OUTER JOIN notifications_actions na on n.id = na.notification_id WHERE n.id IN (SELECT * FROM UNNEST($1::bigint[])) - GROUP BY n.id, n.user_id; + GROUP BY n.id, n.user_id + ORDER BY n.created DESC; ", ¬ification_ids_parsed ) diff --git a/src/database/models/mod_item.rs b/src/database/models/project_item.rs similarity index 66% rename from src/database/models/mod_item.rs rename to src/database/models/project_item.rs index 432258f3..0561bc5a 100644 --- a/src/database/models/mod_item.rs +++ b/src/database/models/project_item.rs @@ -1,7 +1,11 @@ use super::ids::*; - +use crate::database::cache::project_cache::{get_cache_project, set_cache_project}; +use crate::database::cache::query_project_cache::{ + get_cache_query_project, set_cache_query_project, +}; +#[derive(Clone, Debug)] pub struct DonationUrl { - pub mod_id: ModId, + pub project_id: ProjectId, pub platform_id: DonationPlatformId, pub platform_short: String, pub platform_name: String, @@ -22,7 +26,7 @@ impl DonationUrl { $1, $2, $3 ) ", - self.mod_id as ModId, + self.project_id as ProjectId, self.platform_id as DonationPlatformId, self.url, ) @@ -33,8 +37,9 @@ impl DonationUrl { } } -pub struct ModBuilder { - pub mod_id: ModId, +pub struct ProjectBuilder { + pub project_id: ProjectId, + pub project_type_id: ProjectTypeId, pub team_id: TeamId, pub title: String, pub description: String, @@ -55,13 +60,14 @@ pub struct ModBuilder { pub donation_urls: Vec, } -impl ModBuilder { +impl ProjectBuilder { pub async fn insert( self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, - ) -> Result { - let mod_struct = Mod { - id: self.mod_id, + ) -> Result { + let project_struct = Project { + id: self.project_id, + project_type: self.project_type_id, team_id: self.team_id, title: self.title, description: self.description, @@ -83,15 +89,15 @@ impl ModBuilder { license: self.license, slug: self.slug, }; - mod_struct.insert(&mut *transaction).await?; + project_struct.insert(&mut *transaction).await?; for mut version in self.initial_versions { - version.mod_id = self.mod_id; + version.project_id = self.project_id; version.insert(&mut *transaction).await?; } for mut donation in self.donation_urls { - donation.mod_id = self.mod_id; + donation.project_id = self.project_id; donation.insert(&mut *transaction).await?; } @@ -101,19 +107,20 @@ impl ModBuilder { INSERT INTO mods_categories (joining_mod_id, joining_category_id) VALUES ($1, $2) ", - self.mod_id as ModId, + self.project_id as ProjectId, category as CategoryId, ) .execute(&mut *transaction) .await?; } - Ok(self.mod_id) + Ok(self.project_id) } } - -pub struct Mod { - pub id: ModId, +#[derive(Clone, Debug)] +pub struct Project { + pub id: ProjectId, + pub project_type: ProjectTypeId, pub team_id: TeamId, pub title: String, pub description: String, @@ -136,7 +143,7 @@ pub struct Mod { pub slug: Option, } -impl Mod { +impl Project { pub async fn insert( &self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, @@ -148,17 +155,17 @@ impl Mod { published, downloads, icon_url, issues_url, source_url, wiki_url, status, discord_url, client_side, server_side, license_url, license, - slug + slug, project_type ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, - LOWER($18) + LOWER($18), $19 ) ", - self.id as ModId, + self.id as ProjectId, self.team_id as TeamId, &self.title, &self.description, @@ -175,7 +182,8 @@ impl Mod { self.server_side as SideTypeId, self.license_url.as_ref(), self.license as LicenseId, - self.slug.as_ref() + self.slug.as_ref(), + self.project_type as ProjectTypeId ) .execute(&mut *transaction) .await?; @@ -183,13 +191,16 @@ impl Mod { Ok(()) } - pub async fn get<'a, 'b, E>(id: ModId, executor: E) -> Result, sqlx::error::Error> + pub async fn get<'a, 'b, E>( + id: ProjectId, + executor: E, + ) -> Result, sqlx::error::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " - SELECT title, description, downloads, follows, + SELECT project_type, title, description, downloads, follows, icon_url, body, body_url, published, updated, status, issues_url, source_url, wiki_url, discord_url, license_url, @@ -197,14 +208,15 @@ impl Mod { FROM mods WHERE id = $1 ", - id as ModId, + id as ProjectId, ) .fetch_optional(executor) .await?; if let Some(row) = result { - Ok(Some(Mod { + Ok(Some(Project { id, + project_type: ProjectTypeId(row.project_type), team_id: TeamId(row.team_id), title: row.title, description: row.description, @@ -231,16 +243,19 @@ impl Mod { } } - pub async fn get_many<'a, E>(mod_ids: Vec, exec: E) -> Result, sqlx::Error> + pub async fn get_many<'a, E>( + project_ids: Vec, + exec: E, + ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { use futures::stream::TryStreamExt; - let mod_ids_parsed: Vec = mod_ids.into_iter().map(|x| x.0).collect(); - let mods = sqlx::query!( + let project_ids_parsed: Vec = project_ids.into_iter().map(|x| x.0).collect(); + let projects = sqlx::query!( " - SELECT id, title, description, downloads, follows, + SELECT id, project_type, title, description, downloads, follows, icon_url, body, body_url, published, updated, status, issues_url, source_url, wiki_url, discord_url, license_url, @@ -248,12 +263,13 @@ impl Mod { FROM mods WHERE id IN (SELECT * FROM UNNEST($1::bigint[])) ", - &mod_ids_parsed + &project_ids_parsed ) .fetch_many(exec) .try_filter_map(|e| async { - Ok(e.right().map(|m| Mod { - id: ModId(m.id), + Ok(e.right().map(|m| Project { + id: ProjectId(m.id), + project_type: ProjectTypeId(m.project_type), team_id: TeamId(m.team_id), title: m.title, description: m.description, @@ -276,14 +292,14 @@ impl Mod { follows: m.follows, })) }) - .try_collect::>() + .try_collect::>() .await?; - Ok(mods) + Ok(projects) } pub async fn remove_full<'a, 'b, E>( - id: ModId, + id: ProjectId, exec: E, ) -> Result, sqlx::error::Error> where @@ -293,7 +309,7 @@ impl Mod { " SELECT team_id FROM mods WHERE id = $1 ", - id as ModId, + id as ProjectId, ) .fetch_optional(exec) .await?; @@ -309,7 +325,7 @@ impl Mod { DELETE FROM mod_follows WHERE mod_id = $1 ", - id as ModId + id as ProjectId ) .execute(exec) .await?; @@ -319,7 +335,7 @@ impl Mod { DELETE FROM mod_follows WHERE mod_id = $1 ", - id as ModId, + id as ProjectId, ) .execute(exec) .await?; @@ -329,7 +345,7 @@ impl Mod { DELETE FROM reports WHERE mod_id = $1 ", - id as ModId, + id as ProjectId, ) .execute(exec) .await?; @@ -339,7 +355,7 @@ impl Mod { DELETE FROM mods_categories WHERE joining_mod_id = $1 ", - id as ModId, + id as ProjectId, ) .execute(exec) .await?; @@ -349,7 +365,7 @@ impl Mod { DELETE FROM mods_donations WHERE joining_mod_id = $1 ", - id as ModId, + id as ProjectId, ) .execute(exec) .await?; @@ -360,7 +376,7 @@ impl Mod { SELECT id FROM versions WHERE mod_id = $1 ", - id as ModId, + id as ProjectId, ) .fetch_many(exec) .try_filter_map(|e| async { Ok(e.right().map(|c| VersionId(c.id))) }) @@ -376,7 +392,7 @@ impl Mod { DELETE FROM mods WHERE id = $1 ", - id as ModId, + id as ProjectId, ) .execute(exec) .await?; @@ -407,7 +423,7 @@ impl Mod { pub async fn get_full_from_slug<'a, 'b, E>( slug: &str, executor: E, - ) -> Result, sqlx::error::Error> + ) -> Result, sqlx::error::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { @@ -421,49 +437,154 @@ impl Mod { .fetch_optional(executor) .await?; - if let Some(mod_id) = id { - Mod::get_full(ModId(mod_id.id), executor).await + if let Some(project_id) = id { + Project::get_full(ProjectId(project_id.id), executor).await } else { Ok(None) } } - pub async fn get_full<'a, 'b, E>( - id: ModId, + pub async fn get_from_slug<'a, 'b, E>( + slug: &str, executor: E, - ) -> Result, sqlx::error::Error> + ) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + let id = sqlx::query!( + " + SELECT id FROM mods + WHERE LOWER(slug) = LOWER($1) + ", + slug + ) + .fetch_optional(executor) + .await?; + + if let Some(project_id) = id { + Project::get(ProjectId(project_id.id), executor).await + } else { + Ok(None) + } + } + + pub async fn get_from_slug_or_project_id<'a, 'b, E>( + slug_or_project_id: String, + executor: E, + ) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + // Check in the cache + let cached = get_cache_project(slug_or_project_id.clone()).await; + if let Some(data) = cached { + return Ok(Some(data)); + } + let id_option = + crate::models::ids::base62_impl::parse_base62(&*slug_or_project_id.clone()).ok(); + + if let Some(id) = id_option { + let mut project = Project::get(ProjectId(id as i64), executor).await?; + + if project.is_none() { + project = Project::get_from_slug(&slug_or_project_id, executor).await?; + } + // Cache the response + if let Some(data) = project { + set_cache_project(slug_or_project_id.clone(), &data).await; + Ok(Some(data)) + } else { + Ok(None) + } + } else { + let project = Project::get_from_slug(&slug_or_project_id, executor).await?; + // Capture the data, and try to cache it + if let Some(data) = project { + set_cache_project(slug_or_project_id.clone(), &data).await; + Ok(Some(data)) + } else { + Ok(None) + } + } + } + + pub async fn get_full_from_slug_or_project_id<'a, 'b, E>( + slug_or_project_id: String, + executor: E, + ) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + // Query cache + let cached = get_cache_query_project(slug_or_project_id.clone()).await; + if let Some(data) = cached { + return Ok(Some(data)); + } + let id_option = + crate::models::ids::base62_impl::parse_base62(&*slug_or_project_id.clone()).ok(); + + if let Some(id) = id_option { + let mut project = Project::get_full(ProjectId(id as i64), executor).await?; + + if project.is_none() { + project = Project::get_full_from_slug(&slug_or_project_id, executor).await?; + } + // Save the variable + if let Some(data) = project { + set_cache_query_project(slug_or_project_id.clone(), &data).await; + Ok(Some(data)) + } else { + Ok(None) + } + } else { + let project = Project::get_full_from_slug(&slug_or_project_id, executor).await?; + if let Some(data) = project { + set_cache_query_project(slug_or_project_id.clone(), &data).await; + Ok(Some(data)) + } else { + Ok(None) + } + } + } + + pub async fn get_full<'a, 'b, E>( + id: ProjectId, + executor: E, + ) -> Result, sqlx::error::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { let result = sqlx::query!( " - SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows, + SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows, m.icon_url icon_url, m.body body, m.body_url body_url, m.published published, m.updated updated, m.status status, m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, - s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, + s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name, STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions FROM mods m LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id LEFT OUTER JOIN versions v ON v.mod_id = m.id + INNER JOIN project_types pt ON pt.id = m.project_type INNER JOIN statuses s ON s.id = m.status INNER JOIN side_types cs ON m.client_side = cs.id INNER JOIN side_types ss ON m.server_side = ss.id INNER JOIN licenses l ON m.license = l.id WHERE m.id = $1 - GROUP BY m.id, s.id, cs.id, ss.id, l.id; + GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id; ", - id as ModId, + id as ProjectId, ) .fetch_optional(executor) .await?; if let Some(m) = result { - Ok(Some(QueryMod { - inner: Mod { - id: ModId(m.id), + Ok(Some(QueryProject { + inner: Project { + id: ProjectId(m.id), + project_type: ProjectTypeId(m.project_type), team_id: TeamId(m.team_id), title: m.title.clone(), description: m.description.clone(), @@ -485,6 +606,7 @@ impl Mod { body: m.body.clone(), follows: m.follows, }, + project_type: m.project_type_name, categories: m .categories .unwrap_or_default() @@ -498,11 +620,11 @@ impl Mod { .map(|x| VersionId(x.parse().unwrap_or_default())) .collect(), donation_urls: vec![], - status: crate::models::mods::ModStatus::from_str(&m.status_name), + status: crate::models::projects::ProjectStatus::from_str(&m.status_name), license_id: m.short, license_name: m.license_name, - client_side: crate::models::mods::SideType::from_str(&m.client_side_type), - server_side: crate::models::mods::SideType::from_str(&m.server_side_type), + client_side: crate::models::projects::SideType::from_str(&m.client_side_type), + server_side: crate::models::projects::SideType::from_str(&m.server_side_type), })) } else { Ok(None) @@ -510,42 +632,44 @@ impl Mod { } pub async fn get_many_full<'a, E>( - mod_ids: Vec, + project_ids: Vec, exec: E, - ) -> Result, sqlx::Error> + ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { use futures::TryStreamExt; - let mod_ids_parsed: Vec = mod_ids.into_iter().map(|x| x.0).collect(); + let project_ids_parsed: Vec = project_ids.into_iter().map(|x| x.0).collect(); sqlx::query!( " - SELECT m.id id, m.title title, m.description description, m.downloads downloads, m.follows follows, + SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows, m.icon_url icon_url, m.body body, m.body_url body_url, m.published published, m.updated updated, m.status status, m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url, m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, - s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, + s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name, STRING_AGG(DISTINCT c.category, ',') categories, STRING_AGG(DISTINCT v.id::text, ',') versions FROM mods m LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id LEFT OUTER JOIN versions v ON v.mod_id = m.id + INNER JOIN project_types pt ON pt.id = m.project_type INNER JOIN statuses s ON s.id = m.status INNER JOIN side_types cs ON m.client_side = cs.id INNER JOIN side_types ss ON m.server_side = ss.id INNER JOIN licenses l ON m.license = l.id WHERE m.id IN (SELECT * FROM UNNEST($1::bigint[])) - GROUP BY m.id, s.id, cs.id, ss.id, l.id; + GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id; ", - &mod_ids_parsed + &project_ids_parsed ) .fetch_many(exec) .try_filter_map(|e| async { - Ok(e.right().map(|m| QueryMod { - inner: Mod { - id: ModId(m.id), + Ok(e.right().map(|m| QueryProject { + inner: Project { + id: ProjectId(m.id), + project_type: ProjectTypeId(m.project_type), team_id: TeamId(m.team_id), title: m.title.clone(), description: m.description.clone(), @@ -567,30 +691,31 @@ impl Mod { body: m.body.clone(), follows: m.follows }, + project_type: m.project_type_name, categories: m.categories.unwrap_or_default().split(',').map(|x| x.to_string()).collect(), versions: m.versions.unwrap_or_default().split(',').map(|x| VersionId(x.parse().unwrap_or_default())).collect(), donation_urls: vec![], - status: crate::models::mods::ModStatus::from_str(&m.status_name), + status: crate::models::projects::ProjectStatus::from_str(&m.status_name), license_id: m.short, license_name: m.license_name, - client_side: crate::models::mods::SideType::from_str(&m.client_side_type), - server_side: crate::models::mods::SideType::from_str(&m.server_side_type), + client_side: crate::models::projects::SideType::from_str(&m.client_side_type), + server_side: crate::models::projects::SideType::from_str(&m.server_side_type), })) }) - .try_collect::>() + .try_collect::>() .await } } - -pub struct QueryMod { - pub inner: Mod, - +#[derive(Clone, Debug)] +pub struct QueryProject { + pub inner: Project, + pub project_type: String, pub categories: Vec, pub versions: Vec, pub donation_urls: Vec, - pub status: crate::models::mods::ModStatus, + pub status: crate::models::projects::ProjectStatus, pub license_id: String, pub license_name: String, - pub client_side: crate::models::mods::SideType, - pub server_side: crate::models::mods::SideType, + pub client_side: crate::models::projects::SideType, + pub server_side: crate::models::projects::SideType, } diff --git a/src/database/models/report_item.rs b/src/database/models/report_item.rs index 1034b3e1..3949fafe 100644 --- a/src/database/models/report_item.rs +++ b/src/database/models/report_item.rs @@ -3,7 +3,7 @@ use super::ids::*; pub struct Report { pub id: ReportId, pub report_type_id: ReportTypeId, - pub mod_id: Option, + pub project_id: Option, pub version_id: Option, pub user_id: Option, pub body: String, @@ -14,7 +14,7 @@ pub struct Report { pub struct QueryReport { pub id: ReportId, pub report_type: String, - pub mod_id: Option, + pub project_id: Option, pub version_id: Option, pub user_id: Option, pub body: String, @@ -40,7 +40,7 @@ impl Report { ", self.id as ReportId, self.report_type_id as ReportTypeId, - self.mod_id.map(|x| x.0 as i64), + self.project_id.map(|x| x.0 as i64), self.version_id.map(|x| x.0 as i64), self.user_id.map(|x| x.0 as i64), self.body, @@ -72,7 +72,7 @@ impl Report { Ok(Some(QueryReport { id, report_type: row.name, - mod_id: row.mod_id.map(ModId), + project_id: row.mod_id.map(ProjectId), version_id: row.version_id.map(VersionId), user_id: row.user_id.map(UserId), body: row.body, @@ -108,7 +108,7 @@ impl Report { Ok(e.right().map(|row| QueryReport { id: ReportId(row.id), report_type: row.name, - mod_id: row.mod_id.map(ModId), + project_id: row.mod_id.map(ProjectId), version_id: row.version_id.map(VersionId), user_id: row.user_id.map(UserId), body: row.body, diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index 267b338d..2f69b1a7 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -61,7 +61,7 @@ impl TeamBuilder { } } -/// A team of users who control a mod +/// A team of users who control a project pub struct Team { /// The id of the team pub id: TeamId, @@ -412,8 +412,8 @@ impl TeamMember { Ok(()) } - pub async fn get_from_user_id_mod<'a, 'b, E>( - id: ModId, + pub async fn get_from_user_id_project<'a, 'b, E>( + id: ProjectId, user_id: UserId, executor: E, ) -> Result, super::DatabaseError> @@ -426,7 +426,7 @@ impl TeamMember { INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE WHERE m.id = $1 ", - id as ModId, + id as ProjectId, user_id as UserId ) .fetch_optional(executor) diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index d82c5ac5..d0932103 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -1,4 +1,4 @@ -use super::ids::{ModId, UserId}; +use super::ids::{ProjectId, UserId}; pub struct User { pub id: UserId, @@ -24,7 +24,7 @@ impl User { avatar_url, bio, created ) VALUES ( - $1, $2, LOWER($3), $4, $5, + $1, $2, $3, $4, $5, $6, $7, $8 ) ", @@ -186,17 +186,17 @@ impl User { Ok(users) } - pub async fn get_mods<'a, E>( + pub async fn get_projects<'a, E>( user_id: UserId, status: &str, exec: E, - ) -> Result, sqlx::Error> + ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { use futures::stream::TryStreamExt; - let mods = sqlx::query!( + let projects = sqlx::query!( " SELECT m.id FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id @@ -206,23 +206,23 @@ impl User { status, ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) }) - .try_collect::>() + .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) }) + .try_collect::>() .await?; - Ok(mods) + Ok(projects) } - pub async fn get_mods_private<'a, E>( + pub async fn get_projects_private<'a, E>( user_id: UserId, exec: E, - ) -> Result, sqlx::Error> + ) -> Result, sqlx::Error> where E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { use futures::stream::TryStreamExt; - let mods = sqlx::query!( + let projects = sqlx::query!( " SELECT m.id FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id @@ -231,11 +231,11 @@ impl User { user_id as UserId, ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) }) - .try_collect::>() + .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) }) + .try_collect::>() .await?; - Ok(mods) + Ok(projects) } pub async fn remove<'a, 'b, E>(id: UserId, exec: E) -> Result, sqlx::error::Error> @@ -353,7 +353,7 @@ impl User { E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, { use futures::TryStreamExt; - let mods: Vec = sqlx::query!( + let projects: Vec = sqlx::query!( " SELECT m.id FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id @@ -363,12 +363,12 @@ impl User { crate::models::teams::OWNER_ROLE ) .fetch_many(exec) - .try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.id))) }) - .try_collect::>() + .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) }) + .try_collect::>() .await?; - for mod_id in mods { - let _result = super::mod_item::Mod::remove_full(mod_id, exec).await?; + for project_id in projects { + let _result = super::project_item::Project::remove_full(project_id, exec).await?; } let notifications: Vec = sqlx::query!( @@ -439,4 +439,56 @@ impl User { Ok(Some(())) } + + pub async fn get_id_from_username_or_id<'a, 'b, E>( + username_or_id: String, + executor: E, + ) -> Result, sqlx::error::Error> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + { + let id_option = crate::models::ids::base62_impl::parse_base62(&*username_or_id).ok(); + + if let Some(id) = id_option { + let id = UserId(id as i64); + + let mut user_id = sqlx::query!( + " + SELECT id FROM users + WHERE id = $1 + ", + id as UserId + ) + .fetch_optional(executor) + .await? + .map(|x| UserId(x.id)); + + if user_id.is_none() { + user_id = sqlx::query!( + " + SELECT id FROM users + WHERE LOWER(username) = LOWER($1) + ", + username_or_id + ) + .fetch_optional(executor) + .await? + .map(|x| UserId(x.id)); + } + + Ok(user_id) + } else { + let id = sqlx::query!( + " + SELECT id FROM users + WHERE LOWER(username) = LOWER($1) + ", + username_or_id + ) + .fetch_optional(executor) + .await?; + + Ok(id.map(|x| UserId(x.id))) + } + } } diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index db6c7e65..beac46a1 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; pub struct VersionBuilder { pub version_id: VersionId, - pub mod_id: ModId, + pub project_id: ProjectId, pub author_id: UserId, pub name: String, pub version_number: String, @@ -75,7 +75,7 @@ impl VersionBuilder { ) -> Result { let version = Version { id: self.version_id, - mod_id: self.mod_id, + project_id: self.project_id, author_id: self.author_id, name: self.name, version_number: self.version_number, @@ -95,7 +95,7 @@ impl VersionBuilder { SET updated = NOW() WHERE id = $1 ", - self.mod_id as ModId, + self.project_id as ProjectId, ) .execute(&mut *transaction) .await?; @@ -150,7 +150,7 @@ impl VersionBuilder { pub struct Version { pub id: VersionId, - pub mod_id: ModId, + pub project_id: ProjectId, pub author_id: UserId, pub name: String, pub version_number: String, @@ -182,7 +182,7 @@ impl Version { ) ", self.id as VersionId, - self.mod_id as ModId, + self.project_id as ProjectId, self.author_id as UserId, &self.name, &self.version_number, @@ -359,8 +359,8 @@ impl Version { Ok(vec) } - pub async fn get_mod_versions<'a, E>( - mod_id: ModId, + pub async fn get_project_versions<'a, E>( + project_id: ProjectId, game_versions: Option>, loaders: Option>, exec: E, @@ -382,7 +382,7 @@ impl Version { ) AS version ORDER BY version.date_published ASC ", - mod_id as ModId, + project_id as ProjectId, &game_versions.unwrap_or_default(), &loaders.unwrap_or_default(), ) @@ -417,7 +417,7 @@ impl Version { if let Some(row) = result { Ok(Some(Version { id, - mod_id: ModId(row.mod_id), + project_id: ProjectId(row.mod_id), author_id: UserId(row.author_id), name: row.name, version_number: row.version_number, @@ -450,6 +450,7 @@ impl Version { v.release_channel, v.featured FROM versions v WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[])) + ORDER BY v.date_published ASC ", &version_ids_parsed ) @@ -457,7 +458,7 @@ impl Version { .try_filter_map(|e| async { Ok(e.right().map(|v| Version { id: VersionId(v.id), - mod_id: ModId(v.mod_id), + project_id: ProjectId(v.mod_id), author_id: UserId(v.author_id), name: v.name, version_number: v.version_number, @@ -480,7 +481,7 @@ impl Version { executor: E, ) -> Result, sqlx::error::Error> where - E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy, + E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = sqlx::query!( " @@ -566,7 +567,7 @@ impl Version { Ok(Some(QueryVersion { id: VersionId(v.id), - mod_id: ModId(v.mod_id), + project_id: ProjectId(v.mod_id), author_id: UserId(v.author_id), name: v.version_name, version_number: v.version_number, @@ -625,7 +626,8 @@ impl Version { LEFT OUTER JOIN hashes h on f.id = h.file_id LEFT OUTER JOIN dependencies d on v.id = d.dependent_id WHERE v.id IN (SELECT * FROM UNNEST($1::bigint[])) - GROUP BY v.id, rc.id; + GROUP BY v.id, rc.id + ORDER BY v.date_published ASC; ", &version_ids_parsed ) @@ -683,7 +685,7 @@ impl Version { QueryVersion { id: VersionId(v.id), - mod_id: ModId(v.mod_id), + project_id: ProjectId(v.mod_id), author_id: UserId(v.author_id), name: v.version_name, version_number: v.version_number, @@ -727,7 +729,7 @@ pub struct FileHash { #[derive(Clone)] pub struct QueryVersion { pub id: VersionId, - pub mod_id: ModId, + pub project_id: ProjectId, pub author_id: UserId, pub name: String, pub version_number: String, diff --git a/src/file_hosting/backblaze.rs b/src/file_hosting/backblaze.rs index 06065db0..737a4c5e 100644 --- a/src/file_hosting/backblaze.rs +++ b/src/file_hosting/backblaze.rs @@ -79,61 +79,3 @@ impl FileHost for BackblazeHost { }) } } - -/*#[cfg(test)] -mod tests { - use super::*; - use authorization::*; - use delete::*; - use upload::*; - - #[actix_rt::test] - async fn test_authorization() { - println!("{}", dotenv::var("BACKBLAZE_BUCKET_ID").unwrap()); - let authorization_data = authorize_account( - &dotenv::var("BACKBLAZE_KEY_ID").unwrap(), - &dotenv::var("BACKBLAZE_KEY").unwrap(), - ) - .await - .unwrap(); - - get_upload_url( - &authorization_data, - &dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(), - ) - .await - .unwrap(); - } - - #[actix_rt::test] - async fn test_file_management() { - let authorization_data = authorize_account( - &dotenv::var("BACKBLAZE_KEY_ID").unwrap(), - &dotenv::var("BACKBLAZE_KEY").unwrap(), - ) - .await - .unwrap(); - let upload_url_data = get_upload_url( - &authorization_data, - &dotenv::var("BACKBLAZE_BUCKET_ID").unwrap(), - ) - .await - .unwrap(); - let upload_data = upload_file( - &upload_url_data, - "text/plain", - "test.txt", - "test file".to_string().into_bytes(), - ) - .await - .unwrap(); - - delete_file_version( - &authorization_data, - &upload_data.file_id, - &upload_data.file_name, - ) - .await - .unwrap(); - } -}*/ diff --git a/src/file_hosting/s3_host.rs b/src/file_hosting/s3_host.rs index d8a2b396..0b235b75 100644 --- a/src/file_hosting/s3_host.rs +++ b/src/file_hosting/s3_host.rs @@ -76,32 +76,3 @@ impl FileHost for S3Host { }) } } - -#[cfg(test)] -mod tests { - use crate::file_hosting::s3_host::S3Host; - use crate::file_hosting::FileHost; - - #[actix_rt::test] - async fn test_file_management() { - let s3_host = S3Host::new( - &*dotenv::var("S3_BUCKET_NAME").unwrap(), - &*dotenv::var("S3_REGION").unwrap(), - &*dotenv::var("S3_URL").unwrap(), - &*dotenv::var("S3_ACCESS_TOKEN").unwrap(), - &*dotenv::var("S3_SECRET").unwrap(), - ) - .unwrap(); - - s3_host - .upload_file( - "text/plain", - "test.txt", - "test file".to_string().into_bytes(), - ) - .await - .unwrap(); - - s3_host.delete_file_version("", "test.txt").await.unwrap(); - } -} diff --git a/src/main.rs b/src/main.rs index 5b7491fe..ab013f23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use env_logger::Env; use gumdrop::Options; use log::{error, info, warn}; use rand::Rng; -use search::indexing::index_mods; +use search::indexing::index_projects; use search::indexing::IndexingSettings; use std::sync::Arc; @@ -18,6 +18,7 @@ mod models; mod routes; mod scheduler; mod search; +mod validate; #[derive(Debug, Options)] struct Config { @@ -158,13 +159,10 @@ async fn main() -> std::io::Result<()> { return; } info!("Indexing local database"); - let settings = IndexingSettings { - index_local: true, - index_external: false, - }; - let result = index_mods(pool_ref, settings, &thread_search_config).await; + let settings = IndexingSettings { index_local: true }; + let result = index_projects(pool_ref, settings, &thread_search_config).await; if let Err(e) = result { - warn!("Local mod indexing failed: {:?}", e); + warn!("Local project indexing failed: {:?}", e); } info!("Done indexing local database"); } @@ -229,12 +227,12 @@ async fn main() -> std::io::Result<()> { if local_skip { return; } - info!("Indexing created mod queue"); + info!("Indexing created project queue"); let result = search::indexing::queue::index_queue(&*queue, &thread_search_config).await; if let Err(e) = result { - warn!("Indexing created mods failed: {:?}", e); + warn!("Indexing created projects failed: {:?}", e); } - info!("Done indexing created mod queue"); + info!("Done indexing created project queue"); } }); @@ -252,13 +250,12 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() .wrap( - Cors::new() + Cors::default() .allowed_methods(vec!["GET", "POST", "DELETE", "PATCH", "PUT"]) .allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT]) .allowed_header(http::header::CONTENT_TYPE) - .send_wildcard() - .max_age(3600) - .finish(), + .allow_any_origin() + .max_age(3600), ) .wrap( // This is a hacky workaround to allowing the frontend server-side renderer to have @@ -297,19 +294,9 @@ async fn main() -> std::io::Result<()> { .data(indexing_queue.clone()) .data(search_config.clone()) .data(ip_salt.clone()) + .configure(routes::v1_config) + .configure(routes::v2_config) .service(routes::index_get) - .service( - web::scope("/api/v1/") - .configure(routes::auth_config) - .configure(routes::tags_config) - .configure(routes::mods_config) - .configure(routes::versions_config) - .configure(routes::teams_config) - .configure(routes::users_config) - .configure(routes::moderation_config) - .configure(routes::reports_config) - .configure(routes::notifications_config), - ) .service(web::scope("/maven/").configure(routes::maven_config)) .default_service(web::get().to(routes::not_found)) }) diff --git a/src/models/ids.rs b/src/models/ids.rs index 5563a370..9a85f569 100644 --- a/src/models/ids.rs +++ b/src/models/ids.rs @@ -1,7 +1,7 @@ use thiserror::Error; -pub use super::mods::{ModId, VersionId}; pub use super::notifications::NotificationId; +pub use super::projects::{ProjectId, VersionId}; pub use super::reports::ReportId; pub use super::teams::TeamId; pub use super::users::UserId; @@ -105,7 +105,7 @@ macro_rules! base62_id_impl { impl_base62_display!($struct); } } -base62_id_impl!(ModId, ModId); +base62_id_impl!(ProjectId, ProjectId); base62_id_impl!(UserId, UserId); base62_id_impl!(VersionId, VersionId); base62_id_impl!(TeamId, TeamId); diff --git a/src/models/mod.rs b/src/models/mod.rs index 8295104a..0c9b7c58 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,7 +1,7 @@ pub mod error; pub mod ids; -pub mod mods; pub mod notifications; +pub mod projects; pub mod reports; pub mod teams; pub mod users; diff --git a/src/models/mods.rs b/src/models/projects.rs similarity index 65% rename from src/models/mods.rs rename to src/models/projects.rs index df607873..6174cf8f 100644 --- a/src/models/mods.rs +++ b/src/models/projects.rs @@ -3,74 +3,77 @@ use super::teams::TeamId; use super::users::UserId; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; +use validator::Validate; -/// The ID of a specific mod, encoded as base62 for usage in the API +/// The ID of a specific project, encoded as base62 for usage in the API #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(from = "Base62Id")] #[serde(into = "Base62Id")] -pub struct ModId(pub u64); +pub struct ProjectId(pub u64); -/// The ID of a specific version of a mod +/// The ID of a specific version of a project #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(from = "Base62Id")] #[serde(into = "Base62Id")] pub struct VersionId(pub u64); -/// A mod returned from the API +/// A project returned from the API #[derive(Serialize, Deserialize)] -pub struct Mod { - /// The ID of the mod, encoded as a base62 string. - pub id: ModId, - /// The slug of a mod, used for vanity URLs +pub struct Project { + /// The ID of the project, encoded as a base62 string. + pub id: ProjectId, + /// The slug of a project, used for vanity URLs pub slug: Option, - /// The team of people that has ownership of this mod. + /// The project type of the project + pub project_type: String, + /// The team of people that has ownership of this project. pub team: TeamId, - /// The title or name of the mod. + /// The title or name of the project. pub title: String, - /// A short description of the mod. + /// A short description of the project. pub description: String, - /// A long form description of the mod. + /// A long form description of the project. pub body: String, - /// The link to the long description of the mod. (Deprecated), being replaced by `body` + /// The link to the long description of the project. (Deprecated), being replaced by `body` pub body_url: Option, - /// The date at which the mod was first published. + /// The date at which the project was first published. pub published: DateTime, - /// The date at which the mod was first published. + /// The date at which the project was first published. pub updated: DateTime, - /// The status of the mod - pub status: ModStatus, - /// The license of this mod + /// The status of the project + pub status: ProjectStatus, + /// The license of this project pub license: License, - /// The support range for the client mod + /// The support range for the client project* pub client_side: SideType, - /// The support range for the server mod + /// The support range for the server project pub server_side: SideType, - /// The total number of downloads the mod has had. + /// The total number of downloads the project has had. pub downloads: u32, - /// The total number of followers this mod has accumulated + /// The total number of followers this project has accumulated pub followers: u32, - /// A list of the categories that the mod is in. + /// A list of the categories that the project is in. pub categories: Vec, - /// A list of ids for versions of the mod. + /// A list of ids for versions of the project. pub versions: Vec, - /// The URL of the icon of the mod + /// The URL of the icon of the project pub icon_url: Option, - /// An optional link to where to submit bugs or issues with the mod. + /// An optional link to where to submit bugs or issues with the project. pub issues_url: Option, - /// An optional link to the source code for the mod. + /// An optional link to the source code for the project. pub source_url: Option, - /// An optional link to the mod's wiki page or other relevant information. + /// An optional link to the project's wiki page or other relevant information. pub wiki_url: Option, - /// An optional link to the mod's discord + /// An optional link to the project's discord pub discord_url: Option, - /// An optional list of all donation links the mod has + /// An optional list of all donation links the project has pub donation_urls: Option>, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "kebab-case")] pub enum SideType { Required, @@ -113,22 +116,23 @@ pub struct License { pub url: Option, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Validate, Clone)] pub struct DonationLink { pub id: String, pub platform: String, + #[validate(url)] pub url: String, } -/// A status decides the visbility of a mod in search, URLs, and the whole site itself. -/// Approved - Mod is displayed on search, and accessible by URL -/// Rejected - Mod is not displayed on search, and not accessible by URL (Temporary state, mod can reapply) -/// Draft - Mod is not displayed on search, and not accessible by URL -/// Unlisted - Mod is not displayed on search, but accessible by URL -/// Processing - Mod is not displayed on search, and not accessible by URL (Temporary state, mod under review) -#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +/// A status decides the visbility of a project in search, URLs, and the whole site itself. +/// Approved - Project is displayed on search, and accessible by URL +/// Rejected - Project is not displayed on search, and not accessible by URL (Temporary state, project can reapply) +/// Draft - Project is not displayed on search, and not accessible by URL +/// Unlisted - Project is not displayed on search, but accessible by URL +/// Processing - Project is not displayed on search, and not accessible by URL (Temporary state, project under review) +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)] #[serde(rename_all = "lowercase")] -pub enum ModStatus { +pub enum ProjectStatus { Approved, Rejected, Draft, @@ -137,57 +141,57 @@ pub enum ModStatus { Unknown, } -impl std::fmt::Display for ModStatus { +impl std::fmt::Display for ProjectStatus { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { write!(fmt, "{}", self.as_str()) } } -impl ModStatus { - pub fn from_str(string: &str) -> ModStatus { +impl ProjectStatus { + pub fn from_str(string: &str) -> ProjectStatus { match string { - "processing" => ModStatus::Processing, - "rejected" => ModStatus::Rejected, - "approved" => ModStatus::Approved, - "draft" => ModStatus::Draft, - "unlisted" => ModStatus::Unlisted, - _ => ModStatus::Unknown, + "processing" => ProjectStatus::Processing, + "rejected" => ProjectStatus::Rejected, + "approved" => ProjectStatus::Approved, + "draft" => ProjectStatus::Draft, + "unlisted" => ProjectStatus::Unlisted, + _ => ProjectStatus::Unknown, } } pub fn as_str(&self) -> &'static str { match self { - ModStatus::Approved => "approved", - ModStatus::Rejected => "rejected", - ModStatus::Draft => "draft", - ModStatus::Unlisted => "unlisted", - ModStatus::Processing => "processing", - ModStatus::Unknown => "unknown", + ProjectStatus::Approved => "approved", + ProjectStatus::Rejected => "rejected", + ProjectStatus::Draft => "draft", + ProjectStatus::Unlisted => "unlisted", + ProjectStatus::Processing => "processing", + ProjectStatus::Unknown => "unknown", } } pub fn is_hidden(&self) -> bool { match self { - ModStatus::Approved => false, - ModStatus::Rejected => true, - ModStatus::Draft => true, - ModStatus::Unlisted => false, - ModStatus::Processing => true, - ModStatus::Unknown => true, + ProjectStatus::Approved => false, + ProjectStatus::Rejected => true, + ProjectStatus::Draft => true, + ProjectStatus::Unlisted => false, + ProjectStatus::Processing => true, + ProjectStatus::Unknown => true, } } pub fn is_searchable(&self) -> bool { - matches!(self, ModStatus::Approved) + matches!(self, ProjectStatus::Approved) } } -/// A specific version of a mod +/// A specific version of a project #[derive(Serialize, Deserialize)] pub struct Version { /// The ID of the version, encoded as a base62 string. pub id: VersionId, - /// The ID of the mod this version is for. - pub mod_id: ModId, + /// The ID of the project this version is for. + pub project_id: ProjectId, /// The ID of the author who published this version pub author_id: UserId, /// Whether the version is featured or not @@ -197,9 +201,9 @@ pub struct Version { pub name: String, /// The version number. Ideally will follow semantic versioning pub version_number: String, - /// The changelog for this version of the mod. + /// The changelog for this version of the project. pub changelog: String, - /// A link to the changelog for this version of the mod. (Deprecated), being replaced by `changelog` + /// A link to the changelog for this version of the project. (Deprecated), being replaced by `changelog` pub changelog_url: Option, /// The date that this version was published. pub date_published: DateTime, @@ -210,15 +214,15 @@ pub struct Version { /// A list of files available for download for this version. pub files: Vec, - /// A list of mods that this version depends on. + /// A list of projects that this version depends on. pub dependencies: Vec, - /// A list of versions of Minecraft that this version of the mod supports. + /// A list of versions of Minecraft that this version of the project supports. pub game_versions: Vec, /// The loaders that this version works on - pub loaders: Vec, + pub loaders: Vec, } -/// A single mod file, with a url for the file and the file's hash +/// A single project file, with a url for the file and the file's hash #[derive(Serialize, Deserialize)] pub struct VersionFile { /// A map of hashes of the file. The key is the hashing algorithm @@ -310,14 +314,14 @@ impl DependencyType { } /// A specific version of Minecraft -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, PartialEq)] #[serde(transparent)] pub struct GameVersion(pub String); -/// A mod loader +/// A project loader #[derive(Serialize, Deserialize, Clone)] #[serde(transparent)] -pub struct ModLoader(pub String); +pub struct Loader(pub String); // These fields must always succeed parsing; deserialize errors aren't // processed correctly (don't return JSON errors) diff --git a/src/models/reports.rs b/src/models/reports.rs index c9f76b2b..e970f343 100644 --- a/src/models/reports.rs +++ b/src/models/reports.rs @@ -22,7 +22,7 @@ pub struct Report { #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub enum ItemType { - Mod, + Project, Version, User, Unknown, @@ -31,7 +31,7 @@ pub enum ItemType { impl ItemType { pub fn as_str(&self) -> &'static str { match self { - ItemType::Mod => "mod", + ItemType::Project => "project", ItemType::Version => "version", ItemType::User => "user", ItemType::Unknown => "unknown", diff --git a/src/models/teams.rs b/src/models/teams.rs index c2586eb8..056013d9 100644 --- a/src/models/teams.rs +++ b/src/models/teams.rs @@ -11,7 +11,7 @@ pub struct TeamId(pub u64); pub const OWNER_ROLE: &str = "Owner"; // TODO: permissions, role names, etc -/// A team of users who control a mod +/// A team of users who control a project #[derive(Serialize, Deserialize)] pub struct Team { /// The id of the team @@ -31,7 +31,7 @@ bitflags::bitflags! { const MANAGE_INVITES = 1 << 4; const REMOVE_MEMBER = 1 << 5; const EDIT_MEMBER = 1 << 6; - const DELETE_MOD = 1 << 7; + const DELETE_PROJECT = 1 << 7; const ALL = 0b11111111; } } diff --git a/src/routes/index.rs b/src/routes/index.rs index c55e34cc..1e990636 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -11,4 +11,4 @@ pub async fn index_get() -> HttpResponse { }); HttpResponse::Ok().json(data) -} \ No newline at end of file +} diff --git a/src/routes/maven.rs b/src/routes/maven.rs index 0b674ddb..b695ca6c 100644 --- a/src/routes/maven.rs +++ b/src/routes/maven.rs @@ -1,6 +1,6 @@ use crate::auth::get_user_from_headers; use crate::database; -use crate::models::mods::ModId; +use crate::models::projects::ProjectId; use crate::routes::ApiError; use actix_web::{get, web, HttpRequest, HttpResponse}; use sqlx::PgPool; @@ -55,22 +55,13 @@ pub async fn maven_metadata( pool: web::Data, ) -> Result { let string = info.into_inner().0; - let id_option: Option = serde_json::from_str(&*format!("\"{}\"", string)).ok(); - let mod_data = if let Some(id) = id_option { - match database::models::Mod::get_full(id.into(), &**pool).await { - Ok(Some(data)) => Ok(Some(data)), - Ok(None) => database::models::Mod::get_full_from_slug(&string, &**pool).await, - Err(e) => Err(e), - } - } else { - database::models::Mod::get_full_from_slug(&string, &**pool).await - } - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let project_data = + database::models::Project::get_full_from_slug_or_project_id(string, &**pool).await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); - let data = if let Some(data) = mod_data { + let data = if let Some(data) = project_data { data } else { return Ok(HttpResponse::NotFound().body("")); @@ -85,17 +76,16 @@ pub async fn maven_metadata( } else { let user_id: database::models::ids::UserId = user.id.into(); - let mod_exists = sqlx::query!( + let project_exists = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", data.inner.team_id as database::models::ids::TeamId, user_id as database::models::ids::UserId, ) .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? .exists; - authorized = mod_exists.unwrap_or(false); + authorized = project_exists.unwrap_or(false); } } } @@ -110,15 +100,16 @@ pub async fn maven_metadata( LEFT JOIN release_channels ON release_channels.id = versions.release_channel WHERE mod_id = $1 ", - data.inner.id as database::models::ids::ModId + data.inner.id as database::models::ids::ProjectId ) .fetch_all(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; + + let project_id: ProjectId = data.inner.id.into(); let respdata = Metadata { group_id: "maven.modrinth".to_string(), - artifact_id: string, + artifact_id: format!("{}", project_id), versioning: Versioning { latest: version_names .last() @@ -141,7 +132,7 @@ pub async fn maven_metadata( Ok(HttpResponse::Ok() .content_type("text/xml") - .body(yaserde::ser::to_string(&respdata).map_err(|e| ApiError::XmlError(e))?)) + .body(yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?)) } #[get("maven/modrinth/{id}/{versionnum}/{file}")] @@ -150,22 +141,21 @@ pub async fn version_file( web::Path((string, vnum, file)): web::Path<(String, String, String)>, pool: web::Data, ) -> Result { - let id_option: Option = serde_json::from_str(&*format!("\"{}\"", string)).ok(); + let id_option: Option = serde_json::from_str(&*format!("\"{}\"", string)).ok(); - let mod_data = if let Some(id) = id_option { - match database::models::Mod::get_full(id.into(), &**pool).await { + let project_data = if let Some(id) = id_option { + match database::models::Project::get_full(id.into(), &**pool).await { Ok(Some(data)) => Ok(Some(data)), - Ok(None) => database::models::Mod::get_full_from_slug(&string, &**pool).await, + Ok(None) => database::models::Project::get_full_from_slug(&string, &**pool).await, Err(e) => Err(e), } } else { - database::models::Mod::get_full_from_slug(&string, &**pool).await - } - .map_err(|e| ApiError::DatabaseError(e.into()))?; + database::models::Project::get_full_from_slug(&string, &**pool).await + }?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); - let data = if let Some(data) = mod_data { + let data = if let Some(data) = project_data { data } else { return Ok(HttpResponse::NotFound().body("")); @@ -180,17 +170,16 @@ pub async fn version_file( } else { let user_id: database::models::ids::UserId = user.id.into(); - let mod_exists = sqlx::query!( + let project_exists = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", data.inner.team_id as database::models::ids::TeamId, user_id as database::models::ids::UserId, ) .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? .exists; - authorized = mod_exists.unwrap_or(false); + authorized = project_exists.unwrap_or(false); } } } @@ -201,12 +190,11 @@ pub async fn version_file( let vid = if let Some(vid) = sqlx::query!( "SELECT id FROM versions WHERE mod_id = $1 AND version_number = $2", - data.inner.id as database::models::ids::ModId, + data.inner.id as database::models::ids::ProjectId, vnum ) .fetch_optional(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? { vid } else { @@ -215,8 +203,7 @@ pub async fn version_file( let version = if let Some(version) = database::models::Version::get_full(database::models::ids::VersionId(vid.id), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? { version } else { @@ -238,19 +225,17 @@ pub async fn version_file( }; return Ok(HttpResponse::Ok() .content_type("text/xml") - .body(yaserde::ser::to_string(&respdata).map_err(|e| ApiError::XmlError(e))?)); - } else { - if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) { + .body(yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?)); + } else if let Some(selected_file) = version.files.iter().find(|x| x.filename == file) { + return Ok(HttpResponse::TemporaryRedirect() + .header("Location", &*selected_file.url) + .body("")); + } else if file == format!("{}-{}.jar", &string, &version.version_number) { + if let Some(selected_file) = version.files.iter().last() { return Ok(HttpResponse::TemporaryRedirect() .header("Location", &*selected_file.url) .body("")); - } else if file == format!("{}-{}.jar", &string, &version.version_number) { - if let Some(selected_file) = version.files.iter().last() { - return Ok(HttpResponse::TemporaryRedirect() - .header("Location", &*selected_file.url) - .body("")); - } - }; + } } Ok(HttpResponse::NotFound().body("")) diff --git a/src/routes/mod.rs b/src/routes/mod.rs index ad66d1f9..68a82ae4 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,18 +1,22 @@ use actix_web::web; +mod v1; +pub use v1::v1_config; + mod auth; mod index; mod maven; -mod mod_creation; mod moderation; -mod mods; mod not_found; mod notifications; +mod project_creation; +mod projects; mod reports; mod tags; mod teams; mod users; mod version_creation; +mod version_file; mod versions; pub use auth::config as auth_config; @@ -22,21 +26,35 @@ pub use self::index::index_get; pub use self::not_found::not_found; use crate::file_hosting::FileHostingError; -pub fn mods_config(cfg: &mut web::ServiceConfig) { - cfg.service(mods::mod_search); - cfg.service(mods::mods_get); - cfg.service(mod_creation::mod_create); +pub fn v2_config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/v2/") + .configure(auth_config) + .configure(tags_config) + .configure(projects_config) + .configure(versions_config) + .configure(teams_config) + .configure(users_config) + .configure(moderation_config) + .configure(reports_config) + .configure(notifications_config), + ); +} + +pub fn projects_config(cfg: &mut web::ServiceConfig) { + cfg.service(projects::project_search); + cfg.service(projects::projects_get); + cfg.service(project_creation::project_create); cfg.service( - web::scope("mod") - .service(mods::mod_slug_get) - .service(mods::mod_get) - .service(mods::mod_delete) - .service(mods::mod_edit) - .service(mods::mod_icon_edit) - .service(mods::mod_follow) - .service(mods::mod_unfollow) - .service(web::scope("{mod_id}").service(versions::version_list)), + web::scope("project") + .service(projects::project_get) + .service(projects::project_delete) + .service(projects::project_edit) + .service(projects::project_icon_edit) + .service(projects::project_follow) + .service(projects::project_unfollow) + .service(web::scope("{project_id}").service(versions::version_list)), ); } @@ -57,9 +75,17 @@ pub fn versions_config(cfg: &mut web::ServiceConfig) { ); cfg.service( web::scope("version_file") - .service(versions::delete_file) - .service(versions::get_version_from_hash) - .service(versions::download_version), + .service(version_file::delete_file) + .service(version_file::get_version_from_hash) + .service(version_file::download_version) + .service(version_file::get_update_from_hash), + ); + + cfg.service( + web::scope("version_files") + .service(version_file::get_versions_from_hashes) + .service(version_file::download_files) + .service(version_file::update_files), ); } @@ -69,9 +95,8 @@ pub fn users_config(cfg: &mut web::ServiceConfig) { cfg.service(users::users_get); cfg.service( web::scope("user") - .service(users::user_username_get) .service(users::user_get) - .service(users::mods_list) + .service(users::projects_list) .service(users::user_delete) .service(users::user_edit) .service(users::user_icon_edit) @@ -102,7 +127,7 @@ pub fn notifications_config(cfg: &mut web::ServiceConfig) { } pub fn moderation_config(cfg: &mut web::ServiceConfig) { - cfg.service(web::scope("moderation").service(moderation::mods)); + cfg.service(web::scope("moderation").service(moderation::get_projects)); } pub fn reports_config(cfg: &mut web::ServiceConfig) { @@ -117,8 +142,10 @@ pub enum ApiError { EnvError(#[from] dotenv::Error), #[error("Error while uploading file")] FileHostingError(#[from] FileHostingError), - #[error("Internal server error: {0}")] + #[error("Database Error: {0}")] DatabaseError(#[from] crate::database::models::DatabaseError), + #[error("Database Error: {0}")] + SqlxDatabaseError(#[from] sqlx::Error), #[error("Internal server error: {0}")] XmlError(String), #[error("Deserialization error: {0}")] @@ -129,6 +156,8 @@ pub enum ApiError { CustomAuthenticationError(String), #[error("Invalid Input: {0}")] InvalidInputError(String), + #[error("Error while validating input: {0}")] + ValidationError(#[from] validator::ValidationErrors), #[error("Search Error: {0}")] SearchError(#[from] meilisearch_sdk::errors::Error), #[error("Indexing Error: {0}")] @@ -140,6 +169,7 @@ impl actix_web::ResponseError for ApiError { match self { ApiError::EnvError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, ApiError::DatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, + ApiError::SqlxDatabaseError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, ApiError::AuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED, ApiError::CustomAuthenticationError(..) => actix_web::http::StatusCode::UNAUTHORIZED, ApiError::XmlError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, @@ -148,6 +178,7 @@ impl actix_web::ResponseError for ApiError { ApiError::IndexingError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, ApiError::FileHostingError(..) => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, ApiError::InvalidInputError(..) => actix_web::http::StatusCode::BAD_REQUEST, + ApiError::ValidationError(..) => actix_web::http::StatusCode::BAD_REQUEST, } } @@ -156,6 +187,7 @@ impl actix_web::ResponseError for ApiError { crate::models::error::ApiError { error: match self { ApiError::EnvError(..) => "environment_error", + ApiError::SqlxDatabaseError(..) => "database_error", ApiError::DatabaseError(..) => "database_error", ApiError::AuthenticationError(..) => "unauthorized", ApiError::CustomAuthenticationError(..) => "unauthorized", @@ -165,6 +197,7 @@ impl actix_web::ResponseError for ApiError { ApiError::IndexingError(..) => "indexing_error", ApiError::FileHostingError(..) => "file_hosting_error", ApiError::InvalidInputError(..) => "invalid_input", + ApiError::ValidationError(..) => "invalid_input", }, description: &self.to_string(), }, diff --git a/src/routes/moderation.rs b/src/routes/moderation.rs index 8604ea5d..f2d548a7 100644 --- a/src/routes/moderation.rs +++ b/src/routes/moderation.rs @@ -1,7 +1,7 @@ use super::ApiError; use crate::auth::check_is_moderator_from_headers; use crate::database; -use crate::models::mods::{Mod, ModStatus}; +use crate::models::projects::{Project, ProjectStatus}; use actix_web::{get, web, HttpRequest, HttpResponse}; use serde::Deserialize; use sqlx::PgPool; @@ -9,15 +9,15 @@ use sqlx::PgPool; #[derive(Deserialize)] pub struct ResultCount { #[serde(default = "default_count")] - count: i16, + pub count: i16, } fn default_count() -> i16 { 100 } -#[get("mods")] -pub async fn mods( +#[get("projects")] +pub async fn get_projects( req: HttpRequest, pool: web::Data, count: web::Query, @@ -26,7 +26,7 @@ pub async fn mods( use futures::stream::TryStreamExt; - let mod_ids = sqlx::query!( + let project_ids = sqlx::query!( " SELECT id FROM mods WHERE status = ( @@ -35,21 +35,19 @@ pub async fn mods( ORDER BY updated ASC LIMIT $2; ", - ModStatus::Processing.as_str(), + ProjectStatus::Processing.as_str(), count.count as i64 ) .fetch_many(&**pool) - .try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ids::ModId(m.id))) }) - .try_collect::>() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ProjectId(m.id))) }) + .try_collect::>() + .await?; - let mods: Vec = database::models::mod_item::Mod::get_many_full(mod_ids, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + let projects: Vec = database::Project::get_many_full(project_ids, &**pool) + .await? .into_iter() - .map(super::mods::convert_mod) + .map(super::projects::convert_project) .collect(); - Ok(HttpResponse::Ok().json(mods)) + Ok(HttpResponse::Ok().json(projects)) } diff --git a/src/routes/notifications.rs b/src/routes/notifications.rs index faea1e00..2cb9b497 100644 --- a/src/routes/notifications.rs +++ b/src/routes/notifications.rs @@ -27,8 +27,7 @@ pub async fn notifications_get( let notifications_data = database::models::notification_item::Notification::get_many(notification_ids, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; let mut notifications: Vec = Vec::new(); @@ -52,9 +51,7 @@ pub async fn notification_get( let id = info.into_inner().0; let notification_data = - database::models::notification_item::Notification::get(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + database::models::notification_item::Notification::get(id.into(), &**pool).await?; if let Some(data) = notification_data { if user.id == data.user_id.into() || user.role.is_mod() { @@ -100,17 +97,13 @@ pub async fn notification_delete( let id = info.into_inner().0; let notification_data = - database::models::notification_item::Notification::get(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + database::models::notification_item::Notification::get(id.into(), &**pool).await?; if let Some(data) = notification_data { if data.user_id == user.id.into() || user.role.is_mod() { - database::models::notification_item::Notification::remove(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + database::models::notification_item::Notification::remove(id.into(), &**pool).await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthenticationError( "You are not authorized to delete this notification!".to_string(), diff --git a/src/routes/mod_creation.rs b/src/routes/project_creation.rs similarity index 67% rename from src/routes/mod_creation.rs rename to src/routes/project_creation.rs index cbe67d6e..dd79314b 100644 --- a/src/routes/mod_creation.rs +++ b/src/routes/project_creation.rs @@ -2,7 +2,9 @@ use crate::auth::{get_user_from_headers, AuthenticationError}; use crate::database::models; use crate::file_hosting::{FileHost, FileHostingError}; use crate::models::error::ApiError; -use crate::models::mods::{DonationLink, License, ModId, ModStatus, SideType, VersionId}; +use crate::models::projects::{ + DonationLink, License, ProjectId, ProjectStatus, SideType, VersionId, +}; use crate::models::users::UserId; use crate::routes::version_creation::InitialVersionData; use crate::search::indexing::{queue::CreationQueue, IndexingError}; @@ -11,16 +13,19 @@ use actix_web::http::StatusCode; use actix_web::web::Data; use actix_web::{post, HttpRequest, HttpResponse}; use futures::stream::StreamExt; +use lazy_static::lazy_static; +use regex::Regex; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; use std::sync::Arc; use thiserror::Error; +use validator::Validate; #[derive(Error, Debug)] pub enum CreateError { #[error("Environment Error")] EnvError(#[from] dotenv::Error), - #[error("An unknown database error occured")] + #[error("An unknown database error occurred")] SqlxDatabaseError(#[from] sqlx::Error), #[error("Database Error: {0}")] DatabaseError(#[from] models::DatabaseError), @@ -30,11 +35,15 @@ pub enum CreateError { MultipartError(actix_multipart::MultipartError), #[error("Error while parsing JSON: {0}")] SerDeError(#[from] serde_json::Error), + #[error("Error while validating input: {0}")] + ValidationError(#[from] validator::ValidationErrors), #[error("Error while uploading file")] FileHostingError(#[from] FileHostingError), + #[error("Error while validating uploaded file: {0}")] + FileValidationError(#[from] crate::validate::ValidationError), #[error("{}", .0)] MissingValueError(String), - #[error("Invalid format for mod icon: {0}")] + #[error("Invalid format for project icon: {0}")] InvalidIconFormat(String), #[error("Error with multipart data: {0}")] InvalidInput(String), @@ -46,7 +55,7 @@ pub enum CreateError { InvalidCategory(String), #[error("Invalid file type for version file: {0}")] InvalidFileType(String), - #[error("Slug collides with other mod's id!")] + #[error("Slug collides with other project's id!")] SlugCollision, #[error("Authentication Error: {0}")] Unauthorized(#[from] AuthenticationError), @@ -74,6 +83,8 @@ impl actix_web::ResponseError for CreateError { CreateError::Unauthorized(..) => StatusCode::UNAUTHORIZED, CreateError::CustomAuthenticationError(..) => StatusCode::UNAUTHORIZED, CreateError::SlugCollision => StatusCode::BAD_REQUEST, + CreateError::ValidationError(..) => StatusCode::BAD_REQUEST, + CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST, } } @@ -97,46 +108,81 @@ impl actix_web::ResponseError for CreateError { CreateError::Unauthorized(..) => "unauthorized", CreateError::CustomAuthenticationError(..) => "unauthorized", CreateError::SlugCollision => "invalid_input", + CreateError::ValidationError(..) => "invalid_input", + CreateError::FileValidationError(..) => "invalid_input", }, description: &self.to_string(), }) } } -#[derive(Serialize, Deserialize, Clone)] -struct ModCreateData { - /// The title or name of the mod. - pub mod_name: String, - /// The slug of a mod, used for vanity URLs - pub mod_slug: String, - /// A short description of the mod. - pub mod_description: String, - /// A long description of the mod, in markdown. - pub mod_body: String, - /// A list of initial versions to upload with the created mod - pub initial_versions: Vec, - /// A list of the categories that the mod is in. - pub categories: Vec, - /// An optional link to where to submit bugs or issues with the mod. - pub issues_url: Option, - /// An optional link to the source code for the mod. - pub source_url: Option, - /// An optional link to the mod's wiki page or other relevant information. - pub wiki_url: Option, - /// An optional link to the mod's license page - pub license_url: Option, - /// An optional link to the mod's discord. - pub discord_url: Option, - /// An optional boolean. If true, the mod will be created as a draft. - pub is_draft: Option, - /// The support range for the client mod +lazy_static! { + static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap(); +} + +fn default_project_type() -> String { + "mod".to_string() +} + +#[derive(Serialize, Deserialize, Validate, Clone)] +struct ProjectCreateData { + #[validate(length(min = 3, max = 256))] + #[serde(alias = "mod_name")] + /// The title or name of the project. + pub title: String, + #[validate(length(min = 1, max = 64))] + #[serde(default = "default_project_type")] + /// The project type of this mod + pub project_type: String, + #[validate(length(min = 3, max = 64), regex = "RE_URL_SAFE")] + #[serde(alias = "mod_slug")] + /// The slug of a project, used for vanity URLs + pub slug: String, + #[validate(length(min = 3, max = 2048))] + #[serde(alias = "mod_description")] + /// A short description of the project. + pub description: String, + #[validate(length(max = 65536))] + #[serde(alias = "mod_body")] + /// A long description of the project, in markdown. + pub body: String, + + /// The support range for the client project pub client_side: SideType, - /// The support range for the server mod + /// The support range for the server project pub server_side: SideType, - /// The license id that the mod follows - pub license_id: String, - /// An optional list of all donation links the mod has + + #[validate(length(max = 64))] + /// A list of initial versions to upload with the created project + pub initial_versions: Vec, + #[validate(length(max = 3))] + /// A list of the categories that the project is in. + pub categories: Vec, + + #[validate(url, length(max = 2048))] + /// An optional link to where to submit bugs or issues with the project. + pub issues_url: Option, + #[validate(url, length(max = 2048))] + /// An optional link to the source code for the project. + pub source_url: Option, + #[validate(url, length(max = 2048))] + /// An optional link to the project's wiki page or other relevant information. + pub wiki_url: Option, + #[validate(url, length(max = 2048))] + /// An optional link to the project's license page + pub license_url: Option, + #[validate(url, length(max = 2048))] + /// An optional link to the project's discord. + pub discord_url: Option, + /// An optional list of all donation links the project has\ + #[validate] pub donation_urls: Option>, + + /// An optional boolean. If true, the project will be created as a draft. + pub is_draft: Option, + + /// The license id that the project follows + pub license_id: String, } pub struct UploadedFile { @@ -156,8 +202,8 @@ pub async fn undo_uploads( Ok(()) } -#[post("mod")] -pub async fn mod_create( +#[post("project")] +pub async fn project_create( req: HttpRequest, payload: Multipart, client: Data, @@ -167,7 +213,7 @@ pub async fn mod_create( let mut transaction = client.begin().await?; let mut uploaded_files = Vec::new(); - let result = mod_create_inner( + let result = project_create_inner( req, payload, &mut transaction, @@ -196,7 +242,7 @@ pub async fn mod_create( /* -Mod Creation Steps: +Project Creation Steps: Get logged in user Must match the author in the version creation @@ -206,12 +252,12 @@ Get logged in user - Create versions - Some shared logic with version creation - Create list of VersionBuilders - - Create ModBuilder + - Create ProjectBuilder 2. Upload - Icon: check file format & size - Upload to backblaze & record URL - - Mod files + - Project files - Check for matching version - File size limits? - Check file type @@ -221,10 +267,10 @@ Get logged in user 3. Creation - Database stuff - - Add mod data to indexing queue + - Add project data to indexing queue */ -async fn mod_create_inner( +pub async fn project_create_inner( req: HttpRequest, mut payload: Multipart, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, @@ -238,15 +284,17 @@ async fn mod_create_inner( // The currently logged in user let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?; - let mod_id: ModId = models::generate_mod_id(transaction).await?.into(); + let project_id: ProjectId = models::generate_project_id(transaction).await?.into(); - let mod_create_data; + let project_create_data; let mut versions; let mut versions_map = std::collections::HashMap::new(); + let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?; + { // The first multipart field must be named "data" and contain a - // JSON `ModCreateData` object. + // JSON `ProjectCreateData` object. let mut field = payload .next() @@ -275,75 +323,20 @@ async fn mod_create_inner( while let Some(chunk) = field.next().await { data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?); } - let create_data: ModCreateData = serde_json::from_slice(&data)?; + let create_data: ProjectCreateData = serde_json::from_slice(&data)?; - { - // Verify the lengths of various fields in the mod create data - /* - # ModCreateData - mod_name: 3..=256 - mod_description: 3..=2048, - mod_body: max of 64KiB?, - categories: Vec, 1..=256 - issues_url: 0..=2048, (Validate url?) - source_url: 0..=2048, - wiki_url: 0..=2048, + create_data.validate()?; - initial_versions: Vec, - team_members: Vec, + let slug_project_id_option: Option = + serde_json::from_str(&*format!("\"{}\"", create_data.slug)).ok(); - # TeamMember: - name: 3..=64 - role: 3..=64 - */ - - check_length(3..=256, "mod name", &create_data.mod_name)?; - check_length(3..=2048, "mod description", &create_data.mod_description)?; - check_length(3..=64, "mod slug", &create_data.mod_slug)?; - check_length(..65536, "mod body", &create_data.mod_body)?; - - if create_data.categories.len() > 3 { - return Err(CreateError::InvalidInput( - "The maximum number of categories for a mod is four.".to_string(), - )); - } - - create_data - .categories - .iter() - .try_for_each(|f| check_length(1..=256, "category", f))?; - - if let Some(url) = &create_data.issues_url { - check_length(..=2048, "url", url)?; - } - if let Some(url) = &create_data.wiki_url { - check_length(..=2048, "url", url)?; - } - if let Some(url) = &create_data.source_url { - check_length(..=2048, "url", url)?; - } - if let Some(url) = &create_data.discord_url { - check_length(..=2048, "url", url)?; - } - if let Some(url) = &create_data.license_url { - check_length(..=2048, "url", url)?; - } - - create_data - .initial_versions - .iter() - .try_for_each(|v| super::version_creation::check_version(v))?; - } - - let slug_modid_option: Option = - serde_json::from_str(&*format!("\"{}\"", create_data.mod_slug)).ok(); - if let Some(slug_modid) = slug_modid_option { - let slug_modid: models::ids::ModId = slug_modid.into(); + if let Some(slug_project_id) = slug_project_id_option { + let slug_project_id: models::ids::ProjectId = slug_project_id.into(); let results = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1) ", - slug_modid as models::ids::ModId + slug_project_id as models::ids::ProjectId ) .fetch_one(&mut *transaction) .await @@ -366,13 +359,31 @@ async fn mod_create_inner( ))); } } - versions - .push(create_initial_version(data, mod_id, current_user.id, transaction).await?); + versions.push( + create_initial_version( + data, + project_id, + current_user.id, + &all_game_versions, + transaction, + ) + .await?, + ); } - mod_create_data = create_data; + project_create_data = create_data; } + let project_type_id = + models::ProjectTypeId::get_id(project_create_data.project_type.clone(), &mut *transaction) + .await? + .ok_or_else(|| { + CreateError::InvalidInput(format!( + "Project Type {} does not exist.", + project_create_data.project_type.clone() + )) + })?; + let mut icon_url = None; while let Some(item) = payload.next().await { @@ -391,14 +402,14 @@ async fn mod_create_inner( if name == "icon" { if icon_url.is_some() { return Err(CreateError::InvalidInput(String::from( - "Mods can only have one icon", + "Projects can only have one icon", ))); } // Upload the icon to the cdn icon_url = Some( process_icon_upload( uploaded_files, - mod_id, + project_id, file_extension, file_host, field, @@ -420,27 +431,33 @@ async fn mod_create_inner( // `index` is always valid for these lists let created_version = versions.get_mut(index).unwrap(); - let version_data = mod_create_data.initial_versions.get(index).unwrap(); + let version_data = project_create_data.initial_versions.get(index).unwrap(); // Upload the new jar file - let file_builder = super::version_creation::upload_file( + super::version_creation::upload_file( &mut field, file_host, uploaded_files, + &mut created_version.files, &cdn_url, &content_disposition, - mod_id, + project_id, &version_data.version_number, + &*project_create_data.project_type, + version_data.loaders.clone(), + version_data.game_versions.clone(), + &all_game_versions, + false, ) .await?; - - // Add the newly uploaded file to the existing or new version - created_version.files.push(file_builder); } { // Check to make sure that all specified files were uploaded - for (version_data, builder) in mod_create_data.initial_versions.iter().zip(versions.iter()) + for (version_data, builder) in project_create_data + .initial_versions + .iter() + .zip(versions.iter()) { if version_data.file_parts.len() != builder.files.len() { return Err(CreateError::InvalidInput(String::from( @@ -450,11 +467,15 @@ async fn mod_create_inner( } // Convert the list of category names to actual categories - let mut categories = Vec::with_capacity(mod_create_data.categories.len()); - for category in &mod_create_data.categories { - let id = models::categories::Category::get_id(&category, &mut *transaction) - .await? - .ok_or_else(|| CreateError::InvalidCategory(category.clone()))?; + let mut categories = Vec::with_capacity(project_create_data.categories.len()); + for category in &project_create_data.categories { + let id = models::categories::Category::get_id_project( + &category, + project_type_id, + &mut *transaction, + ) + .await? + .ok_or_else(|| CreateError::InvalidCategory(category.clone()))?; categories.push(id); } @@ -470,10 +491,10 @@ async fn mod_create_inner( let team_id = team.insert(&mut *transaction).await?; let status; - if mod_create_data.is_draft.unwrap_or(false) { - status = ModStatus::Draft; + if project_create_data.is_draft.unwrap_or(false) { + status = ProjectStatus::Draft; } else { - status = ModStatus::Processing; + status = ProjectStatus::Processing; } let status_id = models::StatusId::get_id(&status, &mut *transaction) @@ -482,7 +503,7 @@ async fn mod_create_inner( CreateError::InvalidInput(format!("Status {} does not exist.", status.clone())) })?; let client_side_id = - models::SideTypeId::get_id(&mod_create_data.client_side, &mut *transaction) + models::SideTypeId::get_id(&project_create_data.client_side, &mut *transaction) .await? .ok_or_else(|| { CreateError::InvalidInput( @@ -491,7 +512,7 @@ async fn mod_create_inner( })?; let server_side_id = - models::SideTypeId::get_id(&mod_create_data.server_side, &mut *transaction) + models::SideTypeId::get_id(&project_create_data.server_side, &mut *transaction) .await? .ok_or_else(|| { CreateError::InvalidInput( @@ -500,14 +521,14 @@ async fn mod_create_inner( })?; let license_id = - models::categories::License::get_id(&mod_create_data.license_id, &mut *transaction) + models::categories::License::get_id(&project_create_data.license_id, &mut *transaction) .await? .ok_or_else(|| { CreateError::InvalidInput("License specified does not exist.".to_string()) })?; let mut donation_urls = vec![]; - if let Some(urls) = &mod_create_data.donation_urls { + if let Some(urls) = &project_create_data.donation_urls { for url in urls { let platform_id = models::DonationPlatformId::get_id(&url.id, &mut *transaction) .await? @@ -518,8 +539,8 @@ async fn mod_create_inner( )) })?; - donation_urls.push(models::mod_item::DonationUrl { - mod_id: mod_id.into(), + donation_urls.push(models::project_item::DonationUrl { + project_id: project_id.into(), platform_id, platform_short: "".to_string(), platform_name: "".to_string(), @@ -528,72 +549,76 @@ async fn mod_create_inner( } } - let mod_builder = models::mod_item::ModBuilder { - mod_id: mod_id.into(), + let project_builder = models::project_item::ProjectBuilder { + project_id: project_id.into(), + project_type_id, team_id, - title: mod_create_data.mod_name, - description: mod_create_data.mod_description, - body: mod_create_data.mod_body, + title: project_create_data.title, + description: project_create_data.description, + body: project_create_data.body, icon_url, - issues_url: mod_create_data.issues_url, - source_url: mod_create_data.source_url, - wiki_url: mod_create_data.wiki_url, + issues_url: project_create_data.issues_url, + source_url: project_create_data.source_url, + wiki_url: project_create_data.wiki_url, - license_url: mod_create_data.license_url, - discord_url: mod_create_data.discord_url, + license_url: project_create_data.license_url, + discord_url: project_create_data.discord_url, categories, initial_versions: versions, status: status_id, client_side: client_side_id, server_side: server_side_id, license: license_id, - slug: Some(mod_create_data.mod_slug), + slug: Some(project_create_data.slug), donation_urls, }; let now = chrono::Utc::now(); - let response = crate::models::mods::Mod { - id: mod_id, - slug: mod_builder.slug.clone(), + let response = crate::models::projects::Project { + id: project_id, + slug: project_builder.slug.clone(), + project_type: project_create_data.project_type.clone(), team: team_id.into(), - title: mod_builder.title.clone(), - description: mod_builder.description.clone(), - body: mod_builder.body.clone(), + title: project_builder.title.clone(), + description: project_builder.description.clone(), + body: project_builder.body.clone(), body_url: None, published: now, updated: now, status: status.clone(), license: License { - id: mod_create_data.license_id.clone(), + id: project_create_data.license_id.clone(), name: "".to_string(), - url: mod_builder.license_url.clone(), + url: project_builder.license_url.clone(), }, - client_side: mod_create_data.client_side, - server_side: mod_create_data.server_side, + client_side: project_create_data.client_side, + server_side: project_create_data.server_side, downloads: 0, followers: 0, - categories: mod_create_data.categories, - versions: mod_builder + categories: project_create_data.categories, + versions: project_builder .initial_versions .iter() .map(|v| v.version_id.into()) .collect::>(), - icon_url: mod_builder.icon_url.clone(), - issues_url: mod_builder.issues_url.clone(), - source_url: mod_builder.source_url.clone(), - wiki_url: mod_builder.wiki_url.clone(), - discord_url: mod_builder.discord_url.clone(), - donation_urls: mod_create_data.donation_urls.clone(), + icon_url: project_builder.icon_url.clone(), + issues_url: project_builder.issues_url.clone(), + source_url: project_builder.source_url.clone(), + wiki_url: project_builder.wiki_url.clone(), + discord_url: project_builder.discord_url.clone(), + donation_urls: project_create_data.donation_urls.clone(), }; - let _mod_id = mod_builder.insert(&mut *transaction).await?; + let _project_id = project_builder.insert(&mut *transaction).await?; if status.is_searchable() { - let index_mod = - crate::search::indexing::local_import::query_one(mod_id.into(), &mut *transaction) - .await?; - indexing_queue.add(index_mod); + let index_project = crate::search::indexing::local_import::query_one( + project_id.into(), + &mut *transaction, + ) + .await?; + indexing_queue.add(index_project); } Ok(HttpResponse::Ok().json(response)) @@ -602,18 +627,18 @@ async fn mod_create_inner( async fn create_initial_version( version_data: &InitialVersionData, - mod_id: ModId, + project_id: ProjectId, author: UserId, + all_game_versions: &[models::categories::GameVersion], transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { - if version_data.mod_id.is_some() { + if version_data.project_id.is_some() { return Err(CreateError::InvalidInput(String::from( - "Found mod id in initial version for new mod", + "Found project id in initial version for new project", ))); } - check_length(3..=256, "version name", &version_data.version_title)?; - check_length(1..=32, "version number", &version_data.version_number)?; + version_data.validate()?; // Randomly generate a new id to be used for the version let version_id: VersionId = models::generate_version_id(transaction).await?.into(); @@ -623,13 +648,17 @@ async fn create_initial_version( .await? .expect("Release Channel not found in database"); - let mut game_versions = Vec::with_capacity(version_data.game_versions.len()); - for v in &version_data.game_versions { - let id = models::categories::GameVersion::get_id(&v.0, &mut *transaction) - .await? - .ok_or_else(|| CreateError::InvalidGameVersion(v.0.clone()))?; - game_versions.push(id); - } + let game_versions = version_data + .game_versions + .iter() + .map(|x| { + all_game_versions + .iter() + .find(|y| y.version == x.0) + .ok_or_else(|| CreateError::InvalidGameVersion(x.0.clone())) + .map(|y| y.id) + }) + .collect::, CreateError>>()?; let mut loaders = Vec::with_capacity(version_data.loaders.len()); for l in &version_data.loaders { @@ -647,7 +676,7 @@ async fn create_initial_version( let version = models::version_item::VersionBuilder { version_id: version_id.into(), - mod_id: mod_id.into(), + project_id: project_id.into(), author_id: author.into(), name: version_data.version_title.clone(), version_number: version_data.version_number.clone(), @@ -668,7 +697,7 @@ async fn create_initial_version( async fn process_icon_upload( uploaded_files: &mut Vec, - mod_id: ModId, + project_id: ProjectId, file_extension: &str, file_host: &dyn FileHost, mut field: actix_multipart::Field, @@ -689,7 +718,7 @@ async fn process_icon_upload( let upload_data = file_host .upload_file( content_type, - &format!("data/{}/icon.{}", mod_id, file_extension), + &format!("data/{}/icon.{}", project_id, file_extension), data, ) .await?; @@ -723,31 +752,3 @@ pub fn get_image_content_type(extension: &str) -> Option<&'static str> { None } } - -pub fn check_length( - range: impl std::ops::RangeBounds + std::fmt::Debug, - field_name: &str, - field: &str, -) -> Result<(), CreateError> { - use std::ops::Bound; - - let length = field.len(); - if !range.contains(&length) { - let bounds = match (range.start_bound(), range.end_bound()) { - (Bound::Included(a), Bound::Included(b)) => format!("between {} and {} bytes", a, b), - (Bound::Included(a), Bound::Excluded(b)) => { - format!("between {} and {} bytes", a, b - 1) - } - (Bound::Included(a), Bound::Unbounded) => format!("more than {} bytes", a), - (Bound::Unbounded, Bound::Included(b)) => format!("less than or equal to {} bytes", b), - (Bound::Unbounded, Bound::Excluded(b)) => format!("less than {} bytes", b), - _ => format!("{:?}", range), - }; - Err(CreateError::InvalidInput(format!( - "The {} must be {}; got {}.", - field_name, bounds, length - ))) - } else { - Ok(()) - } -} diff --git a/src/routes/mods.rs b/src/routes/projects.rs similarity index 59% rename from src/routes/mods.rs rename to src/routes/projects.rs index 05d3fc7f..257359e1 100644 --- a/src/routes/mods.rs +++ b/src/routes/projects.rs @@ -2,54 +2,56 @@ use crate::auth::get_user_from_headers; use crate::database; use crate::file_hosting::FileHost; use crate::models; -use crate::models::mods::{DonationLink, License, ModId, ModStatus, SearchRequest, SideType}; +use crate::models::projects::{ + DonationLink, License, ProjectId, ProjectStatus, SearchRequest, SideType, +}; use crate::models::teams::Permissions; use crate::routes::ApiError; use crate::search::indexing::queue::CreationQueue; -use crate::search::{search_for_mod, SearchConfig, SearchError}; +use crate::search::{search_for_project, SearchConfig, SearchError}; use actix_web::web::Data; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use futures::StreamExt; +use lazy_static::lazy_static; +use regex::Regex; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::sync::Arc; +use validator::Validate; -#[get("mod")] -pub async fn mod_search( +#[get("search")] +pub async fn project_search( web::Query(info): web::Query, config: web::Data, ) -> Result { - let results = search_for_mod(&info, &**config).await?; + let results = search_for_project(&info, &**config).await?; Ok(HttpResponse::Ok().json(results)) } #[derive(Serialize, Deserialize)] -pub struct ModIds { +pub struct ProjectIds { pub ids: String, } -// TODO: Make this return the full mod struct -#[get("mods")] -pub async fn mods_get( +#[get("projects")] +pub async fn projects_get( req: HttpRequest, - web::Query(ids): web::Query, + web::Query(ids): web::Query, pool: web::Data, ) -> Result { - let mod_ids = serde_json::from_str::>(&*ids.ids)? + let project_ids = serde_json::from_str::>(&*ids.ids)? .into_iter() .map(|x| x.into()) .collect(); - let mods_data = database::models::Mod::get_many_full(mod_ids, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let projects_data = database::models::Project::get_many_full(project_ids, &**pool).await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); - let mut mods = Vec::new(); + let mut projects = Vec::new(); - for mod_data in mods_data { - let mut authorized = !mod_data.status.is_hidden(); + for project_data in projects_data { + let mut authorized = !project_data.status.is_hidden(); if let Some(user) = &user_option { if !authorized { @@ -58,106 +60,42 @@ pub async fn mods_get( } else { let user_id: database::models::ids::UserId = user.id.into(); - let mod_exists = sqlx::query!( + let project_exists = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", - mod_data.inner.team_id as database::models::ids::TeamId, + project_data.inner.team_id as database::models::ids::TeamId, user_id as database::models::ids::UserId, ) .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? .exists; - authorized = mod_exists.unwrap_or(false); + authorized = project_exists.unwrap_or(false); } } } if authorized { - mods.push(convert_mod(mod_data)); + projects.push(convert_project(project_data)); } } - Ok(HttpResponse::Ok().json(mods)) -} - -#[get("@{id}")] -pub async fn mod_slug_get( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, -) -> Result { - let id = info.into_inner().0; - let mod_data = database::models::Mod::get_full_from_slug(&id, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); - - if let Some(data) = mod_data { - let mut authorized = !data.status.is_hidden(); - - if let Some(user) = user_option { - if !authorized { - if user.role.is_mod() { - authorized = true; - } else { - let user_id: database::models::ids::UserId = user.id.into(); - - let mod_exists = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", - data.inner.team_id as database::models::ids::TeamId, - user_id as database::models::ids::UserId, - ) - .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .exists; - - authorized = mod_exists.unwrap_or(false); - } - } - } - - if authorized { - return Ok(HttpResponse::Ok().json(convert_mod(data))); - } - - Ok(HttpResponse::NotFound().body("")) - } else { - Ok(HttpResponse::NotFound().body("")) - } + Ok(HttpResponse::Ok().json(projects)) } #[get("{id}")] -pub async fn mod_get( +pub async fn project_get( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, ) -> Result { let string = info.into_inner().0; - let id_option: Option = serde_json::from_str(&*format!("\"{}\"", string)).ok(); - let mut mod_data; - - if let Some(id) = id_option { - mod_data = database::models::Mod::get_full(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - if mod_data.is_none() { - mod_data = database::models::Mod::get_full_from_slug(&string, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - } - } else { - mod_data = database::models::Mod::get_full_from_slug(&string, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - } + let project_data = + database::models::Project::get_full_from_slug_or_project_id(string, &**pool).await?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); - if let Some(data) = mod_data { + if let Some(data) = project_data { let mut authorized = !data.status.is_hidden(); if let Some(user) = user_option { @@ -167,23 +105,22 @@ pub async fn mod_get( } else { let user_id: database::models::ids::UserId = user.id.into(); - let mod_exists = sqlx::query!( + let project_exists = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", data.inner.team_id as database::models::ids::TeamId, user_id as database::models::ids::UserId, ) .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? .exists; - authorized = mod_exists.unwrap_or(false); + authorized = project_exists.unwrap_or(false); } } } if authorized { - return Ok(HttpResponse::Ok().json(convert_mod(data))); + return Ok(HttpResponse::Ok().json(convert_project(data))); } Ok(HttpResponse::NotFound().body("")) @@ -192,12 +129,15 @@ pub async fn mod_get( } } -pub fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods::Mod { +pub fn convert_project( + data: database::models::project_item::QueryProject, +) -> models::projects::Project { let m = data.inner; - models::mods::Mod { + models::projects::Project { id: m.id.into(), slug: m.slug, + project_type: data.project_type, team: m.team_id.into(), title: m.title, description: m.description, @@ -235,44 +175,58 @@ pub fn convert_mod(data: database::models::mod_item::QueryMod) -> models::mods:: } } -/// A mod returned from the API -#[derive(Serialize, Deserialize)] -pub struct EditMod { +lazy_static! { + static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap(); +} + +/// A project returned from the API +#[derive(Serialize, Deserialize, Validate)] +pub struct EditProject { + #[validate(length(min = 3, max = 256))] pub title: Option, + #[validate(length(min = 3, max = 2048))] pub description: Option, + #[validate(length(max = 65536))] pub body: Option, - pub status: Option, + pub status: Option, + #[validate(length(max = 3))] pub categories: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] + #[validate(url, length(max = 2048))] pub issues_url: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] + #[validate(url, length(max = 2048))] pub source_url: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] + #[validate(url, length(max = 2048))] pub wiki_url: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] + #[validate(url, length(max = 2048))] pub license_url: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] + #[validate(url, length(max = 2048))] pub discord_url: Option>, + #[validate] pub donation_urls: Option>, pub license_id: Option, pub client_side: Option, @@ -282,30 +236,32 @@ pub struct EditMod { skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] + #[validate(length(min = 3, max = 64), regex = "RE_URL_SAFE")] pub slug: Option>, } #[patch("{id}")] -pub async fn mod_edit( +pub async fn project_edit( req: HttpRequest, - info: web::Path<(models::ids::ModId,)>, + info: web::Path<(String,)>, pool: web::Data, config: web::Data, - new_mod: web::Json, + new_project: web::Json, indexing_queue: Data>, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let mod_id = info.into_inner().0; - let id = mod_id.into(); + new_project.validate()?; - let result = database::models::Mod::get_full(id, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let string = info.into_inner().0; + let result = + database::models::Project::get_full_from_slug_or_project_id(string, &**pool).await?; + + if let Some(project_item) = result { + let id = project_item.inner.id; - if let Some(mod_item) = result { let team_member = database::models::TeamMember::get_from_user_id( - mod_item.inner.team_id, + project_item.inner.team_id, user.id.into(), &**pool, ) @@ -321,25 +277,16 @@ pub async fn mod_edit( } if let Some(perms) = permissions { - let mut transaction = pool - .begin() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let mut transaction = pool.begin().await?; - if let Some(title) = &new_mod.title { + if let Some(title) = &new_project.title { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the title of this mod!" + "You do not have the permissions to edit the title of this project!" .to_string(), )); } - if title.len() > 256 || title.len() < 3 { - return Err(ApiError::InvalidInputError( - "The mod's title must be within 3-256 characters!".to_string(), - )); - } - sqlx::query!( " UPDATE mods @@ -347,27 +294,20 @@ pub async fn mod_edit( WHERE (id = $2) ", title, - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(description) = &new_mod.description { + if let Some(description) = &new_project.description { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the description of this mod!" + "You do not have the permissions to edit the description of this project!" .to_string(), )); } - if description.len() > 2048 || description.len() < 3 { - return Err(ApiError::InvalidInputError( - "The mod's description must be within 3-256 characters!".to_string(), - )); - } - sqlx::query!( " UPDATE mods @@ -375,22 +315,21 @@ pub async fn mod_edit( WHERE (id = $2) ", description, - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(status) = &new_mod.status { + if let Some(status) = &new_project.status { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the status of this mod!" + "You do not have the permissions to edit the status of this project!" .to_string(), )); } - if (status == &ModStatus::Rejected || status == &ModStatus::Approved) + if (status == &ProjectStatus::Rejected || status == &ProjectStatus::Approved) && !user.role.is_mod() { return Err(ApiError::CustomAuthenticationError( @@ -413,49 +352,39 @@ pub async fn mod_edit( WHERE (id = $2) ", status_id as database::models::ids::StatusId, - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; - if mod_item.status.is_searchable() && !status.is_searchable() { - delete_from_index(mod_id, config).await?; - } else if !mod_item.status.is_searchable() && status.is_searchable() { - let index_mod = crate::search::indexing::local_import::query_one( - mod_id.into(), - &mut *transaction, - ) - .await?; + if project_item.status.is_searchable() && !status.is_searchable() { + delete_from_index(id.into(), config).await?; + } else if !project_item.status.is_searchable() && status.is_searchable() { + let index_project = + crate::search::indexing::local_import::query_one(id, &mut *transaction) + .await?; - indexing_queue.add(index_mod); + indexing_queue.add(index_project); } } - if let Some(categories) = &new_mod.categories { + if let Some(categories) = &new_project.categories { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the categories of this mod!" + "You do not have the permissions to edit the categories of this project!" .to_string(), )); } - if categories.len() > 3 { - return Err(ApiError::InvalidInputError( - "The maximum number of categories for a mod is four.".to_string(), - )); - } - sqlx::query!( " DELETE FROM mods_categories WHERE joining_mod_id = $1 ", - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; for category in categories { let category_id = database::models::categories::Category::get_id( @@ -475,31 +404,22 @@ pub async fn mod_edit( INSERT INTO mods_categories (joining_mod_id, joining_category_id) VALUES ($1, $2) ", - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, category_id as database::models::ids::CategoryId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } } - if let Some(issues_url) = &new_mod.issues_url { + if let Some(issues_url) = &new_project.issues_url { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the issues URL of this mod!" + "You do not have the permissions to edit the issues URL of this project!" .to_string(), )); } - if let Some(issues) = issues_url { - if issues.len() > 2048 { - return Err(ApiError::InvalidInputError( - "The mod's issues url must be less than 2048 characters!".to_string(), - )); - } - } - sqlx::query!( " UPDATE mods @@ -507,29 +427,20 @@ pub async fn mod_edit( WHERE (id = $2) ", issues_url.as_deref(), - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(source_url) = &new_mod.source_url { + if let Some(source_url) = &new_project.source_url { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the source URL of this mod!" + "You do not have the permissions to edit the source URL of this project!" .to_string(), )); } - if let Some(source) = source_url { - if source.len() > 2048 { - return Err(ApiError::InvalidInputError( - "The mod's source url must be less than 2048 characters!".to_string(), - )); - } - } - sqlx::query!( " UPDATE mods @@ -537,29 +448,20 @@ pub async fn mod_edit( WHERE (id = $2) ", source_url.as_deref(), - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(wiki_url) = &new_mod.wiki_url { + if let Some(wiki_url) = &new_project.wiki_url { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the wiki URL of this mod!" + "You do not have the permissions to edit the wiki URL of this project!" .to_string(), )); } - if let Some(wiki) = wiki_url { - if wiki.len() > 2048 { - return Err(ApiError::InvalidInputError( - "The mod's wiki url must be less than 2048 characters!".to_string(), - )); - } - } - sqlx::query!( " UPDATE mods @@ -567,29 +469,20 @@ pub async fn mod_edit( WHERE (id = $2) ", wiki_url.as_deref(), - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(license_url) = &new_mod.license_url { + if let Some(license_url) = &new_project.license_url { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the license URL of this mod!" + "You do not have the permissions to edit the license URL of this project!" .to_string(), )); } - if let Some(license) = license_url { - if license.len() > 2048 { - return Err(ApiError::InvalidInputError( - "The mod's license url must be less than 2048 characters!".to_string(), - )); - } - } - sqlx::query!( " UPDATE mods @@ -597,29 +490,20 @@ pub async fn mod_edit( WHERE (id = $2) ", license_url.as_deref(), - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(discord_url) = &new_mod.discord_url { + if let Some(discord_url) = &new_project.discord_url { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the discord URL of this mod!" + "You do not have the permissions to edit the discord URL of this project!" .to_string(), )); } - if let Some(discord) = discord_url { - if discord.len() > 2048 { - return Err(ApiError::InvalidInputError( - "The mod's discord url must be less than 2048 characters!".to_string(), - )); - } - } - sqlx::query!( " UPDATE mods @@ -627,44 +511,38 @@ pub async fn mod_edit( WHERE (id = $2) ", discord_url.as_deref(), - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(slug) = &new_mod.slug { + if let Some(slug) = &new_project.slug { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the slug of this mod!".to_string(), + "You do not have the permissions to edit the slug of this project!" + .to_string(), )); } if let Some(slug) = slug { - if slug.len() > 64 || slug.len() < 3 { - return Err(ApiError::InvalidInputError( - "The mod's slug must be within 3-64 characters!".to_string(), - )); - } - - let slug_modid_option: Option = + let slug_project_id_option: Option = serde_json::from_str(&*format!("\"{}\"", slug)).ok(); - if let Some(slug_modid) = slug_modid_option { - let slug_modid: database::models::ids::ModId = slug_modid.into(); + if let Some(slug_project_id) = slug_project_id_option { + let slug_project_id: database::models::ids::ProjectId = + slug_project_id.into(); let results = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1) ", - slug_modid as database::models::ids::ModId + slug_project_id as database::models::ids::ProjectId ) .fetch_one(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; if results.exists.unwrap_or(true) { return Err(ApiError::InvalidInputError( - "Slug collides with other mod's id!".to_string(), + "Slug collides with other project's id!".to_string(), )); } } @@ -677,14 +555,13 @@ pub async fn mod_edit( WHERE (id = $2) ", slug.as_deref(), - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(new_side) = &new_mod.client_side { + if let Some(new_side) = &new_project.client_side { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( "You do not have the permissions to edit the side type of this mod!" @@ -704,17 +581,16 @@ pub async fn mod_edit( WHERE (id = $2) ", side_type_id as database::models::SideTypeId, - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(new_side) = &new_mod.server_side { + if let Some(new_side) = &new_project.server_side { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the side type of this mod!" + "You do not have the permissions to edit the side type of this project!" .to_string(), )); } @@ -731,17 +607,16 @@ pub async fn mod_edit( WHERE (id = $2) ", side_type_id as database::models::SideTypeId, - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(license) = &new_mod.license_id { + if let Some(license) = &new_project.license_id { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the license of this mod!" + "You do not have the permissions to edit the license of this project!" .to_string(), )); } @@ -758,17 +633,16 @@ pub async fn mod_edit( WHERE (id = $2) ", license_id as database::models::LicenseId, - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - if let Some(donations) = &new_mod.donation_urls { + if let Some(donations) = &new_project.donation_urls { if !perms.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the donation links of this mod!" + "You do not have the permissions to edit the donation links of this project!" .to_string(), )); } @@ -778,11 +652,10 @@ pub async fn mod_edit( DELETE FROM mods_donations WHERE joining_mod_id = $1 ", - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; for donation in donations { let platform_id = database::models::DonationPlatformId::get_id( @@ -802,26 +675,20 @@ pub async fn mod_edit( INSERT INTO mods_donations (joining_mod_id, joining_platform_id, url) VALUES ($1, $2, $3) ", - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, platform_id as database::models::ids::DonationPlatformId, donation.url ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } } - if let Some(body) = &new_mod.body { + if let Some(body) = &new_project.body { if !perms.contains(Permissions::EDIT_BODY) { return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the body of this mod!".to_string(), - )); - } - - if body.len() > 65536 { - return Err(ApiError::InvalidInputError( - "The mod's body must be less than 65536 characters!".to_string(), + "You do not have the permissions to edit the body of this project!" + .to_string(), )); } @@ -832,21 +699,17 @@ pub async fn mod_edit( WHERE (id = $2) ", body, - id as database::models::ids::ModId, + id as database::models::ids::ProjectId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - transaction - .commit() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - Ok(HttpResponse::Ok().body("")) + transaction.commit().await?; + Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthenticationError( - "You do not have permission to edit this mod!".to_string(), + "You do not have permission to edit this project!".to_string(), )) } } else { @@ -860,42 +723,45 @@ pub struct Extension { } #[patch("{id}/icon")] -pub async fn mod_icon_edit( +pub async fn project_icon_edit( web::Query(ext): web::Query, req: HttpRequest, - info: web::Path<(models::ids::ModId,)>, + info: web::Path<(String,)>, pool: web::Data, file_host: web::Data>, mut payload: web::Payload, ) -> Result { - if let Some(content_type) = super::mod_creation::get_image_content_type(&*ext.ext) { + if let Some(content_type) = super::project_creation::get_image_content_type(&*ext.ext) { let cdn_url = dotenv::var("CDN_URL")?; let user = get_user_from_headers(req.headers(), &**pool).await?; - let id = info.into_inner().0; + let string = info.into_inner().0; - let mod_item = database::models::Mod::get(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?; + let project_item = database::models::Project::get_from_slug_or_project_id(string, &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; if !user.role.is_mod() { let team_member = database::models::TeamMember::get_from_user_id( - mod_item.team_id, + project_item.team_id, user.id.into(), &**pool, ) .await .map_err(ApiError::DatabaseError)? - .ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?; + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { return Err(ApiError::CustomAuthenticationError( - "You don't have permission to edit this mod's icon.".to_string(), + "You don't have permission to edit this project's icon.".to_string(), )); } } - if let Some(icon) = mod_item.icon_url { + if let Some(icon) = project_item.icon_url { let name = icon.split('/').next(); if let Some(icon_path) = name { @@ -916,15 +782,16 @@ pub async fn mod_icon_edit( ))); } + let project_id: ProjectId = project_item.id.into(); + let upload_data = file_host .upload_file( content_type, - &format!("data/{}/icon.{}", id, ext.ext), + &format!("data/{}/icon.{}", project_id, ext.ext), bytes.to_vec(), ) .await?; - let mod_id: database::models::ids::ModId = id.into(); sqlx::query!( " UPDATE mods @@ -932,87 +799,96 @@ pub async fn mod_icon_edit( WHERE (id = $2) ", format!("{}/{}", cdn_url, upload_data.file_name), - mod_id as database::models::ids::ModId, + project_item.id as database::models::ids::ProjectId, ) .execute(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInputError(format!( - "Invalid format for mod icon: {}", + "Invalid format for project icon: {}", ext.ext ))) } } #[delete("{id}")] -pub async fn mod_delete( +pub async fn project_delete( req: HttpRequest, - info: web::Path<(models::ids::ModId,)>, + info: web::Path<(String,)>, pool: web::Data, config: web::Data, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let id = info.into_inner().0; + let string = info.into_inner().0; + + let project = database::models::Project::get_from_slug_or_project_id(string, &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; if !user.role.is_mod() { - let team_member = - database::models::TeamMember::get_from_user_id_mod(id.into(), user.id.into(), &**pool) - .await - .map_err(ApiError::DatabaseError)? - .ok_or_else(|| { - ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()) - })?; + let team_member = database::models::TeamMember::get_from_user_id_project( + project.id, + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::DatabaseError)? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; - if !team_member.permissions.contains(Permissions::DELETE_MOD) { + if !team_member + .permissions + .contains(Permissions::DELETE_PROJECT) + { return Err(ApiError::CustomAuthenticationError( - "You don't have permission to delete this mod".to_string(), + "You don't have permission to delete this project!".to_string(), )); } } - let result = database::models::Mod::remove_full(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let result = database::models::Project::remove_full(project.id, &**pool).await?; - delete_from_index(id, config).await?; + delete_from_index(project.id.into(), config).await?; if result.is_some() { - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } } #[post("{id}/follow")] -pub async fn mod_follow( +pub async fn project_follow( req: HttpRequest, - info: web::Path<(models::ids::ModId,)>, + info: web::Path<(String,)>, pool: web::Data, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let id = info.into_inner().0; + let string = info.into_inner().0; - let _result = database::models::Mod::get(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .ok_or_else(|| ApiError::InvalidInputError("Invalid Mod ID specified!".to_string()))?; + let result = database::models::Project::get_from_slug_or_project_id(string, &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; let user_id: database::models::ids::UserId = user.id.into(); - let mod_id: database::models::ids::ModId = id.into(); + let project_id: database::models::ids::ProjectId = result.id; let following = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM mod_follows mf WHERE mf.follower_id = $1 AND mf.mod_id = $2) ", user_id as database::models::ids::UserId, - mod_id as database::models::ids::ModId + project_id as database::models::ids::ProjectId ) .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? .exists .unwrap_or(false); @@ -1023,11 +899,10 @@ pub async fn mod_follow( SET follows = follows + 1 WHERE id = $1 ", - mod_id as database::models::ids::ModId, + project_id as database::models::ids::ProjectId, ) .execute(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; sqlx::query!( " @@ -1035,42 +910,46 @@ pub async fn mod_follow( VALUES ($1, $2) ", user_id as database::models::ids::UserId, - mod_id as database::models::ids::ModId + project_id as database::models::ids::ProjectId ) .execute(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInputError( - "You are already following this mod!".to_string(), + "You are already following this project!".to_string(), )) } } #[delete("{id}/follow")] -pub async fn mod_unfollow( +pub async fn project_unfollow( req: HttpRequest, - info: web::Path<(models::ids::ModId,)>, + info: web::Path<(String,)>, pool: web::Data, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let id = info.into_inner().0; + let string = info.into_inner().0; + + let result = database::models::Project::get_from_slug_or_project_id(string, &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("The specified project does not exist!".to_string()) + })?; let user_id: database::models::ids::UserId = user.id.into(); - let mod_id: database::models::ids::ModId = id.into(); + let project_id = result.id; let following = sqlx::query!( " SELECT EXISTS(SELECT 1 FROM mod_follows mf WHERE mf.follower_id = $1 AND mf.mod_id = $2) ", user_id as database::models::ids::UserId, - mod_id as database::models::ids::ModId + project_id as database::models::ids::ProjectId ) .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? .exists .unwrap_or(false); @@ -1081,11 +960,10 @@ pub async fn mod_unfollow( SET follows = follows - 1 WHERE id = $1 ", - mod_id as database::models::ids::ModId, + project_id as database::models::ids::ProjectId, ) .execute(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; sqlx::query!( " @@ -1093,22 +971,21 @@ pub async fn mod_unfollow( WHERE follower_id = $1 AND mod_id = $2 ", user_id as database::models::ids::UserId, - mod_id as database::models::ids::ModId + project_id as database::models::ids::ProjectId ) .execute(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInputError( - "You are not following this mod!".to_string(), + "You are not following this project!".to_string(), )) } } pub async fn delete_from_index( - id: crate::models::mods::ModId, + id: crate::models::projects::ProjectId, config: web::Data, ) -> Result<(), meilisearch_sdk::errors::Error> { let client = meilisearch_sdk::client::Client::new(&*config.address, &*config.key); diff --git a/src/routes/reports.rs b/src/routes/reports.rs index ecad6fe0..2fa047cc 100644 --- a/src/routes/reports.rs +++ b/src/routes/reports.rs @@ -1,5 +1,5 @@ use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; -use crate::models::ids::{ModId, UserId, VersionId}; +use crate::models::ids::{ProjectId, UserId, VersionId}; use crate::models::reports::{ItemType, Report}; use crate::routes::ApiError; use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; @@ -21,10 +21,7 @@ pub async fn report_create( pool: web::Data, mut body: web::Payload, ) -> Result { - let mut transaction = pool - .begin() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let mut transaction = pool.begin().await?; let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?; @@ -48,7 +45,7 @@ pub async fn report_create( let mut report = crate::database::models::report_item::Report { id, report_type_id: report_type, - mod_id: None, + project_id: None, version_id: None, user_id: None, body: new_report.body.clone(), @@ -57,9 +54,10 @@ pub async fn report_create( }; match new_report.item_type { - ItemType::Mod => { - report.mod_id = - Some(serde_json::from_str::(&*format!("\"{}\"", new_report.item_id))?.into()) + ItemType::Project => { + report.project_id = Some( + serde_json::from_str::(&*format!("\"{}\"", new_report.item_id))?.into(), + ) } ItemType::Version => { report.version_id = Some( @@ -79,14 +77,8 @@ pub async fn report_create( } } - report - .insert(&mut transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - transaction - .commit() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + report.insert(&mut transaction).await?; + transaction.commit().await?; Ok(HttpResponse::Ok().json(Report { id: id.into(), @@ -133,12 +125,10 @@ pub async fn reports( .map(|m| crate::database::models::ids::ReportId(m.id))) }) .try_collect::>() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; - let query_reports = crate::database::models::report_item::Report::get_many(report_ids, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let query_reports = + crate::database::models::report_item::Report::get_many(report_ids, &**pool).await?; let mut reports = Vec::new(); @@ -146,9 +136,9 @@ pub async fn reports( let mut item_id = "".to_string(); let mut item_type = ItemType::Unknown; - if let Some(mod_id) = x.mod_id { - item_id = serde_json::to_string::(&mod_id.into())?; - item_type = ItemType::Mod; + if let Some(project_id) = x.project_id { + item_id = serde_json::to_string::(&project_id.into())?; + item_type = ItemType::Project; } else if let Some(version_id) = x.version_id { item_id = serde_json::to_string::(&version_id.into())?; item_type = ItemType::Version; @@ -183,11 +173,10 @@ pub async fn delete_report( info.into_inner().0.into(), &**pool, ) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; if result.is_some() { - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } diff --git a/src/routes/tags.rs b/src/routes/tags.rs index 944ca157..edeff712 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -1,7 +1,7 @@ use super::ApiError; use crate::auth::check_is_admin_from_headers; use crate::database::models; -use crate::database::models::categories::{DonationPlatform, License, ReportType}; +use crate::database::models::categories::{DonationPlatform, License, ProjectType, ReportType}; use actix_web::{delete, get, put, web, HttpRequest, HttpResponse}; use models::categories::{Category, GameVersion, Loader}; use sqlx::PgPool; @@ -30,27 +30,55 @@ pub fn config(cfg: &mut web::ServiceConfig) { ); } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct CategoryData { + icon: String, + name: String, + project_type: String, +} + // TODO: searching / filtering? Could be used to implement a live // searching category list #[get("category")] pub async fn category_list(pool: web::Data) -> Result { - let results = Category::list(&**pool).await?; + let results = Category::list(&**pool) + .await? + .into_iter() + .map(|x| CategoryData { + icon: x.icon, + name: x.category, + project_type: x.project_type, + }) + .collect::>(); + Ok(HttpResponse::Ok().json(results)) } -#[put("category/{name}")] +#[put("category")] pub async fn category_create( req: HttpRequest, pool: web::Data, - category: web::Path<(String,)>, + new_category: web::Json, ) -> Result { check_is_admin_from_headers(req.headers(), &**pool).await?; - let name = category.into_inner().0; + let project_type = crate::database::models::ProjectTypeId::get_id( + (&new_category).project_type.clone(), + &**pool, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("Specified project type does not exist!".to_string()) + })?; - let _id = Category::builder().name(&name)?.insert(&**pool).await?; + let _id = Category::builder() + .name(&new_category.name)? + .project_type(&project_type)? + .icon(&new_category.icon)? + .insert(&**pool) + .await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } #[delete("category/{name}")] @@ -72,31 +100,56 @@ pub async fn category_delete( .map_err(models::DatabaseError::from)?; if result.is_some() { - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct LoaderData { + icon: String, + name: String, + supported_project_types: Vec, +} + #[get("loader")] pub async fn loader_list(pool: web::Data) -> Result { - let results = Loader::list(&**pool).await?; + let results = Loader::list(&**pool) + .await? + .into_iter() + .map(|x| LoaderData { + icon: x.icon, + name: x.loader, + supported_project_types: x.supported_project_types, + }) + .collect::>(); Ok(HttpResponse::Ok().json(results)) } -#[put("loader/{name}")] +#[put("loader")] pub async fn loader_create( req: HttpRequest, pool: web::Data, - loader: web::Path<(String,)>, + new_loader: web::Json, ) -> Result { check_is_admin_from_headers(req.headers(), &**pool).await?; - let name = loader.into_inner().0; + let mut transaction = pool.begin().await?; - let _id = Loader::builder().name(&name)?.insert(&**pool).await?; + let project_types = + ProjectType::get_many_id(&new_loader.supported_project_types, &mut *transaction).await?; - Ok(HttpResponse::Ok().body("")) + let _id = Loader::builder() + .name(&new_loader.name)? + .icon(&new_loader.icon)? + .supported_project_types(&*project_types.into_iter().map(|x| x.id).collect::>())? + .insert(&mut transaction) + .await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) } #[delete("loader/{name}")] @@ -118,14 +171,21 @@ pub async fn loader_delete( .map_err(models::DatabaseError::from)?; if result.is_some() { - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } } -#[derive(serde::Deserialize)] +#[derive(serde::Serialize)] pub struct GameVersionQueryData { + pub version: String, + pub version_type: String, + pub date: chrono::DateTime, +} + +#[derive(serde::Deserialize)] +pub struct GameVersionQuery { #[serde(rename = "type")] type_: Option, major: Option, @@ -134,16 +194,22 @@ pub struct GameVersionQueryData { #[get("game_version")] pub async fn game_version_list( pool: web::Data, - query: web::Query, + query: web::Query, ) -> Result { - if query.type_.is_some() || query.major.is_some() { - let results = - GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await?; - Ok(HttpResponse::Ok().json(results)) + let results: Vec = if query.type_.is_some() || query.major.is_some() { + GameVersion::list_filter(query.type_.as_deref(), query.major, &**pool).await? } else { - let results = GameVersion::list(&**pool).await?; - Ok(HttpResponse::Ok().json(results)) + GameVersion::list(&**pool).await? } + .into_iter() + .map(|x| GameVersionQueryData { + version: x.version, + version_type: x.version_type, + date: x.date, + }) + .collect(); + + Ok(HttpResponse::Ok().json(results)) } #[derive(serde::Deserialize)] @@ -177,7 +243,7 @@ pub async fn game_version_create( let _id = builder.insert(&**pool).await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } #[delete("game_version/{name}")] @@ -199,7 +265,7 @@ pub async fn game_version_delete( .map_err(models::DatabaseError::from)?; if result.is_some() { - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } @@ -246,7 +312,7 @@ pub async fn license_create( .insert(&**pool) .await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } #[delete("license/{name}")] @@ -268,7 +334,7 @@ pub async fn license_delete( .map_err(models::DatabaseError::from)?; if result.is_some() { - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } @@ -315,7 +381,7 @@ pub async fn donation_platform_create( .insert(&**pool) .await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } #[delete("donation_platform/{name}")] @@ -337,7 +403,7 @@ pub async fn donation_platform_delete( .map_err(models::DatabaseError::from)?; if result.is_some() { - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } @@ -361,7 +427,7 @@ pub async fn report_type_create( let _id = ReportType::builder().name(&name)?.insert(&**pool).await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } #[delete("report_type/{name}")] @@ -383,7 +449,7 @@ pub async fn report_type_delete( .map_err(models::DatabaseError::from)?; if result.is_some() { - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } diff --git a/src/routes/teams.rs b/src/routes/teams.rs index 9c240d8c..93b4eb86 100644 --- a/src/routes/teams.rs +++ b/src/routes/teams.rs @@ -1,7 +1,7 @@ use crate::auth::get_user_from_headers; use crate::database::models::notification_item::{NotificationActionBuilder, NotificationBuilder}; use crate::database::models::TeamMember; -use crate::models::ids::ModId; +use crate::models::ids::ProjectId; use crate::models::teams::{Permissions, TeamId}; use crate::models::users::UserId; use crate::routes::ApiError; @@ -76,10 +76,7 @@ pub async fn join_team( "You are already a member of this team".to_string(), )); } - let mut transaction = pool - .begin() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let mut transaction = pool.begin().await?; // Edit Team Member to set Accepted to True TeamMember::edit_team_member( @@ -92,17 +89,14 @@ pub async fn join_team( ) .await?; - transaction - .commit() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + transaction.commit().await?; } else { return Err(ApiError::InvalidInputError( "There is no pending request from this team".to_string(), )); } - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } fn default_role() -> String { @@ -127,10 +121,7 @@ pub async fn add_team_member( ) -> Result { let team_id = info.into_inner().0.into(); - let mut transaction = pool - .begin() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let mut transaction = pool.begin().await?; let current_user = get_user_from_headers(req.headers(), &**pool).await?; let team_member = @@ -181,8 +172,7 @@ pub async fn add_team_member( } crate::database::models::User::get(member.user_id, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? .ok_or_else(|| ApiError::InvalidInputError("An invalid User ID specified".to_string()))?; let new_id = crate::database::models::ids::generate_team_member_id(&mut transaction).await?; @@ -195,8 +185,7 @@ pub async fn add_team_member( accepted: false, } .insert(&mut transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; let result = sqlx::query!( " @@ -206,17 +195,16 @@ pub async fn add_team_member( team_id as crate::database::models::ids::TeamId ) .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; let team: TeamId = team_id.into(); NotificationBuilder { title: "You have been invited to join a team!".to_string(), text: format!( - "Team invite from {} to join the team for mod {}", + "Team invite from {} to join the team for project {}", current_user.username, result.title ), - link: format!("mod/{}", ModId(result.id as u64)), + link: format!("project/{}", ProjectId(result.id as u64)), actions: vec![ NotificationActionBuilder { title: "Accept".to_string(), @@ -234,12 +222,9 @@ pub async fn add_team_member( .insert(new_member.user_id.into(), &mut transaction) .await?; - transaction - .commit() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + transaction.commit().await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } #[derive(Serialize, Deserialize, Clone)] @@ -262,10 +247,7 @@ pub async fn edit_team_member( let current_user = get_user_from_headers(req.headers(), &**pool).await?; let team_member = TeamMember::get_from_user_id(id, current_user.id.into(), &**pool).await?; - let mut transaction = pool - .begin() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let mut transaction = pool.begin().await?; let member = match team_member { Some(m) => m, @@ -306,12 +288,9 @@ pub async fn edit_team_member( ) .await?; - transaction - .commit() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + transaction.commit().await?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } #[delete("{id}/members/{user_id}")] @@ -371,7 +350,7 @@ pub async fn remove_team_member( "You do not have permission to cancel a team invite".to_string(), )); } - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } diff --git a/src/routes/users.rs b/src/routes/users.rs index fec10f92..a51336c8 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,33 +1,28 @@ use crate::auth::get_user_from_headers; use crate::database::models::User; use crate::file_hosting::FileHost; -use crate::models::ids::ModId; -use crate::models::mods::ModStatus; +use crate::models::ids::ProjectId; use crate::models::notifications::Notification; +use crate::models::projects::ProjectStatus; use crate::models::users::{Role, UserId}; use crate::routes::notifications::convert_notification; use crate::routes::ApiError; use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; use futures::StreamExt; +use lazy_static::lazy_static; +use regex::Regex; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::sync::Arc; +use validator::Validate; #[get("user")] pub async fn user_auth_get( req: HttpRequest, pool: web::Data, ) -> Result { - Ok(HttpResponse::Ok().json( - get_user_from_headers( - req.headers(), - &mut *pool - .acquire() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?, - ) - .await?, - )) + Ok(HttpResponse::Ok() + .json(get_user_from_headers(req.headers(), &mut *pool.acquire().await?).await?)) } #[derive(Serialize, Deserialize)] @@ -45,33 +40,13 @@ pub async fn users_get( .map(|x| x.into()) .collect(); - let users_data = User::get_many(user_ids, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let users_data = User::get_many(user_ids, &**pool).await?; let users: Vec = users_data.into_iter().map(convert_user).collect(); Ok(HttpResponse::Ok().json(users)) } -#[get("@{id}")] -pub async fn user_username_get( - info: web::Path<(String,)>, - pool: web::Data, -) -> Result { - let id = info.into_inner().0; - let user_data = User::get_from_username(id, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - if let Some(data) = user_data { - let response = convert_user(data); - Ok(HttpResponse::Ok().json(response)) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - #[get("{id}")] pub async fn user_get( info: web::Path<(String,)>, @@ -83,19 +58,13 @@ pub async fn user_get( let mut user_data; if let Some(id) = id_option { - user_data = User::get(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + user_data = User::get(id.into(), &**pool).await?; if user_data.is_none() { - user_data = User::get_from_username(string, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + user_data = User::get_from_username(string, &**pool).await?; } } else { - user_data = User::get_from_username(string, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + user_data = User::get_from_username(string, &**pool).await?; } if let Some(data) = user_data { @@ -120,48 +89,35 @@ fn convert_user(data: crate::database::models::user_item::User) -> crate::models } } -#[get("{user_id}/mods")] -pub async fn mods_list( +#[get("{user_id}/projects")] +pub async fn projects_list( req: HttpRequest, - info: web::Path<(UserId,)>, + info: web::Path<(String,)>, pool: web::Data, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await.ok(); - let id: crate::database::models::UserId = info.into_inner().0.into(); + let id_option = + crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool) + .await?; - let user_exists = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)", - id as crate::database::models::UserId, - ) - .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .exists; - - if user_exists.unwrap_or(false) { + if let Some(id) = id_option { let user_id: UserId = id.into(); - let mod_data = if let Some(current_user) = user { + let project_data = if let Some(current_user) = user { if current_user.role.is_mod() || current_user.id == user_id { - User::get_mods_private(id, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + User::get_projects_private(id, &**pool).await? } else { - User::get_mods(id, ModStatus::Approved.as_str(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await? } } else { - User::get_mods(id, ModStatus::Approved.as_str(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await? }; - let response = mod_data + let response = project_data .into_iter() .map(|v| v.into()) - .collect::>(); + .collect::>(); Ok(HttpResponse::Ok().json(response)) } else { @@ -169,140 +125,147 @@ pub async fn mods_list( } } -#[derive(Serialize, Deserialize)] +lazy_static! { + static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap(); +} + +#[derive(Serialize, Deserialize, Validate)] pub struct EditUser { + #[validate(length(min = 1, max = 255), regex = "RE_URL_SAFE")] pub username: Option, #[serde( default, skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] + #[validate(length(min = 1, max = 255), regex = "RE_URL_SAFE")] pub name: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] + #[validate(email)] pub email: Option>, #[serde( default, skip_serializing_if = "Option::is_none", with = "::serde_with::rust::double_option" )] + #[validate(length(max = 160))] pub bio: Option>, - pub role: Option, + pub role: Option, } #[patch("{id}")] pub async fn user_edit( req: HttpRequest, - info: web::Path<(UserId,)>, + info: web::Path<(String,)>, pool: web::Data, new_user: web::Json, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let user_id = info.into_inner().0; - let id: crate::database::models::ids::UserId = user_id.into(); + new_user.validate()?; - if user.id == user_id || user.role.is_mod() { - let mut transaction = pool - .begin() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let id_option = + crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool) + .await?; - if let Some(username) = &new_user.username { - sqlx::query!( - " + if let Some(id) = id_option { + let user_id: UserId = id.into(); + + if user.id == user_id || user.role.is_mod() { + let mut transaction = pool.begin().await?; + + if let Some(username) = &new_user.username { + sqlx::query!( + " UPDATE users SET username = $1 WHERE (id = $2) ", - username, - id as crate::database::models::ids::UserId, - ) - .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - } + username, + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await?; + } - if let Some(name) = &new_user.name { - sqlx::query!( - " + if let Some(name) = &new_user.name { + sqlx::query!( + " UPDATE users SET name = $1 WHERE (id = $2) ", - name.as_deref(), - id as crate::database::models::ids::UserId, - ) - .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - } + name.as_deref(), + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await?; + } - if let Some(bio) = &new_user.bio { - sqlx::query!( - " + if let Some(bio) = &new_user.bio { + sqlx::query!( + " UPDATE users SET bio = $1 WHERE (id = $2) ", - bio.as_deref(), - id as crate::database::models::ids::UserId, - ) - .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - } + bio.as_deref(), + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await?; + } - if let Some(email) = &new_user.email { - sqlx::query!( - " + if let Some(email) = &new_user.email { + sqlx::query!( + " UPDATE users SET email = $1 WHERE (id = $2) ", - email.as_deref(), - id as crate::database::models::ids::UserId, - ) - .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - } - - if let Some(role) = &new_user.role { - if !user.role.is_mod() { - return Err(ApiError::CustomAuthenticationError( - "You do not have the permissions to edit the role of this user!".to_string(), - )); + email.as_deref(), + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await?; } - let role = Role::from_string(role).to_string(); + if let Some(role) = &new_user.role { + if !user.role.is_mod() { + return Err(ApiError::CustomAuthenticationError( + "You do not have the permissions to edit the role of this user!" + .to_string(), + )); + } - sqlx::query!( - " + let role = role.to_string(); + + sqlx::query!( + " UPDATE users SET role = $1 WHERE (id = $2) ", - role, - id as crate::database::models::ids::UserId, - ) - .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - } + role, + id as crate::database::models::ids::UserId, + ) + .execute(&mut *transaction) + .await?; + } - transaction - .commit() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - Ok(HttpResponse::Ok().body("")) + transaction.commit().await?; + Ok(HttpResponse::NoContent().body("")) + } else { + Err(ApiError::CustomAuthenticationError( + "You do not have permission to edit this user!".to_string(), + )) + } } else { - Err(ApiError::CustomAuthenticationError( - "You do not have permission to edit this user!".to_string(), - )) + Ok(HttpResponse::NotFound().body("")) } } @@ -315,81 +278,87 @@ pub struct Extension { pub async fn user_icon_edit( web::Query(ext): web::Query, req: HttpRequest, - info: web::Path<(UserId,)>, + info: web::Path<(String,)>, pool: web::Data, file_host: web::Data>, mut payload: web::Payload, ) -> Result { - if let Some(content_type) = super::mod_creation::get_image_content_type(&*ext.ext) { + if let Some(content_type) = super::project_creation::get_image_content_type(&*ext.ext) { let cdn_url = dotenv::var("CDN_URL")?; let user = get_user_from_headers(req.headers(), &**pool).await?; - let id = info.into_inner().0; + let id_option = + crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool) + .await?; - if user.id != id && !user.role.is_mod() { - return Err(ApiError::CustomAuthenticationError( - "You don't have permission to edit this user's icon.".to_string(), - )); - } - - let mut icon_url = user.avatar_url; - - if user.id != id { - let new_user = User::get(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - if let Some(new) = new_user { - icon_url = new.avatar_url; - } else { - return Ok(HttpResponse::NotFound().body("")); + if let Some(id) = id_option { + if user.id != id.into() && !user.role.is_mod() { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to edit this user's icon.".to_string(), + )); } - } - if let Some(icon) = icon_url { - if icon.starts_with(&cdn_url) { - let name = icon.split('/').next(); + let mut icon_url = user.avatar_url; - if let Some(icon_path) = name { - file_host.delete_file_version("", icon_path).await?; + let user_id: UserId = id.into(); + + if user.id != user_id { + let new_user = User::get(id, &**pool).await?; + + if let Some(new) = new_user { + icon_url = new.avatar_url; + } else { + return Ok(HttpResponse::NotFound().body("")); } } - } - let mut bytes = web::BytesMut::new(); - while let Some(item) = payload.next().await { - bytes.extend_from_slice(&item.map_err(|_| { - ApiError::InvalidInputError("Unable to parse bytes in payload sent!".to_string()) - })?); - } + if let Some(icon) = icon_url { + if icon.starts_with(&cdn_url) { + let name = icon.split('/').next(); - if bytes.len() >= 262144 { - return Err(ApiError::InvalidInputError(String::from( - "Icons must be smaller than 256KiB", - ))); - } + if let Some(icon_path) = name { + file_host.delete_file_version("", icon_path).await?; + } + } + } - let upload_data = file_host - .upload_file( - content_type, - &format!("user/{}/icon.{}", id, ext.ext), - bytes.to_vec(), + let mut bytes = web::BytesMut::new(); + while let Some(item) = payload.next().await { + bytes.extend_from_slice(&item.map_err(|_| { + ApiError::InvalidInputError( + "Unable to parse bytes in payload sent!".to_string(), + ) + })?); + } + + if bytes.len() >= 262144 { + return Err(ApiError::InvalidInputError(String::from( + "Icons must be smaller than 256KiB", + ))); + } + + let upload_data = file_host + .upload_file( + content_type, + &format!("user/{}/icon.{}", user_id, ext.ext), + bytes.to_vec(), + ) + .await?; + + sqlx::query!( + " + UPDATE users + SET avatar_url = $1 + WHERE (id = $2) + ", + format!("{}/{}", cdn_url, upload_data.file_name), + id as crate::database::models::ids::UserId, ) + .execute(&**pool) .await?; - - let mod_id: crate::database::models::ids::UserId = id.into(); - sqlx::query!( - " - UPDATE users - SET avatar_url = $1 - WHERE (id = $2) - ", - format!("{}/{}", cdn_url, upload_data.file_name), - mod_id as crate::database::models::ids::UserId, - ) - .execute(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } } else { Err(ApiError::InvalidInputError(format!( "Invalid format for user icon: {}", @@ -411,32 +380,34 @@ fn default_removal() -> String { #[delete("{id}")] pub async fn user_delete( req: HttpRequest, - info: web::Path<(UserId,)>, + info: web::Path<(String,)>, pool: web::Data, removal_type: web::Query, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let id = info.into_inner().0; + let id_option = + crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool) + .await?; - if !user.role.is_mod() && user.id != id { - return Err(ApiError::CustomAuthenticationError( - "You do not have permission to delete this user!".to_string(), - )); - } + if let Some(id) = id_option { + if !user.role.is_mod() && user.id != id.into() { + return Err(ApiError::CustomAuthenticationError( + "You do not have permission to delete this user!".to_string(), + )); + } - let result; - if &*removal_type.removal_type == "full" { - result = crate::database::models::User::remove_full(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - } else { - result = crate::database::models::User::remove(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - }; + let result; + if &*removal_type.removal_type == "full" { + result = crate::database::models::User::remove_full(id, &**pool).await?; + } else { + result = crate::database::models::User::remove(id, &**pool).await?; + }; - if result.is_some() { - Ok(HttpResponse::Ok().body("")) + if result.is_some() { + Ok(HttpResponse::NoContent().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } } else { Ok(HttpResponse::NotFound().body("")) } @@ -445,59 +416,68 @@ pub async fn user_delete( #[get("{id}/follows")] pub async fn user_follows( req: HttpRequest, - info: web::Path<(UserId,)>, + info: web::Path<(String,)>, pool: web::Data, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let id = info.into_inner().0; + let id_option = + crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool) + .await?; - if !user.role.is_mod() && user.id != id { - return Err(ApiError::CustomAuthenticationError( - "You do not have permission to see the mods this user follows!".to_string(), - )); + if let Some(id) = id_option { + if !user.role.is_mod() && user.id != id.into() { + return Err(ApiError::CustomAuthenticationError( + "You do not have permission to see the projects this user follows!".to_string(), + )); + } + + use futures::TryStreamExt; + + let projects: Vec = sqlx::query!( + " + SELECT mf.mod_id FROM mod_follows mf + WHERE mf.follower_id = $1 + ", + id as crate::database::models::ids::UserId, + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.mod_id as u64))) }) + .try_collect::>() + .await?; + + Ok(HttpResponse::Ok().json(projects)) + } else { + Ok(HttpResponse::NotFound().body("")) } - - use futures::TryStreamExt; - - let user_id: crate::database::models::UserId = id.into(); - let mods: Vec = sqlx::query!( - " - SELECT mf.mod_id FROM mod_follows mf - WHERE mf.follower_id = $1 - ", - user_id as crate::database::models::ids::UserId, - ) - .fetch_many(&**pool) - .try_filter_map(|e| async { Ok(e.right().map(|m| ModId(m.mod_id as u64))) }) - .try_collect::>() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - Ok(HttpResponse::Ok().json(mods)) } #[get("{id}/notifications")] pub async fn user_notifications( req: HttpRequest, - info: web::Path<(UserId,)>, + info: web::Path<(String,)>, pool: web::Data, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let id = info.into_inner().0; + let id_option = + crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool) + .await?; - if !user.role.is_mod() && user.id != id { - return Err(ApiError::CustomAuthenticationError( - "You do not have permission to see the mods this user follows!".to_string(), - )); + if let Some(id) = id_option { + if !user.role.is_mod() && user.id != id.into() { + return Err(ApiError::CustomAuthenticationError( + "You do not have permission to see the notifications of this user!".to_string(), + )); + } + + let notifications: Vec = + crate::database::models::notification_item::Notification::get_many_user(id, &**pool) + .await? + .into_iter() + .map(convert_notification) + .collect(); + + Ok(HttpResponse::Ok().json(notifications)) + } else { + Ok(HttpResponse::NotFound().body("")) } - - let notifications: Vec = - crate::database::models::notification_item::Notification::get_many_user(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .into_iter() - .map(convert_notification) - .collect(); - - Ok(HttpResponse::Ok().json(notifications)) } diff --git a/src/routes/v1/mod.rs b/src/routes/v1/mod.rs new file mode 100644 index 00000000..03e5af0f --- /dev/null +++ b/src/routes/v1/mod.rs @@ -0,0 +1,129 @@ +use actix_web::web; + +mod moderation; +mod mods; +mod reports; +mod tags; +mod users; +mod versions; + +pub fn v1_config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/api/v1/") + .configure(super::auth_config) + .configure(tags_config) + .configure(mods_config) + .configure(versions_config) + .configure(teams_config) + .configure(users_config) + .configure(moderation_config) + .configure(reports_config) + .configure(notifications_config), + ); +} + +pub fn tags_config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/tag/") + .service(tags::category_list) + .service(tags::category_create) + .service(super::tags::category_delete) + .service(tags::loader_list) + .service(tags::loader_create) + .service(super::tags::loader_delete) + .service(super::tags::game_version_list) + .service(super::tags::game_version_create) + .service(super::tags::game_version_delete) + .service(super::tags::license_create) + .service(super::tags::license_delete) + .service(super::tags::license_list) + .service(super::tags::donation_platform_create) + .service(super::tags::donation_platform_list) + .service(super::tags::donation_platform_delete) + .service(super::tags::report_type_create) + .service(super::tags::report_type_delete) + .service(super::tags::report_type_list), + ); +} + +pub fn mods_config(cfg: &mut web::ServiceConfig) { + cfg.service(mods::mod_search); + cfg.service(mods::mods_get); + cfg.service(mods::mod_create); + + cfg.service( + web::scope("mod") + .service(super::projects::project_get) + .service(super::projects::project_delete) + .service(super::projects::project_edit) + .service(super::projects::project_icon_edit) + .service(super::projects::project_follow) + .service(super::projects::project_unfollow) + .service(web::scope("{mod_id}").service(versions::version_list)), + ); +} + +pub fn versions_config(cfg: &mut web::ServiceConfig) { + cfg.service(versions::versions_get); + cfg.service(super::version_creation::version_create); + cfg.service( + web::scope("version") + .service(versions::version_get) + .service(super::versions::version_delete) + .service(super::version_creation::upload_file_to_version) + .service(super::versions::version_edit), + ); + cfg.service( + web::scope("version_file") + .service(versions::delete_file) + .service(versions::get_version_from_hash) + .service(versions::download_version), + ); +} + +pub fn users_config(cfg: &mut web::ServiceConfig) { + cfg.service(super::users::user_auth_get); + + cfg.service(super::users::users_get); + cfg.service( + web::scope("user") + .service(super::users::user_get) + .service(users::mods_list) + .service(super::users::user_delete) + .service(super::users::user_edit) + .service(super::users::user_icon_edit) + .service(super::users::user_notifications) + .service(super::users::user_follows), + ); +} + +pub fn teams_config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("team") + .service(super::teams::team_members_get) + .service(super::teams::edit_team_member) + .service(super::teams::add_team_member) + .service(super::teams::join_team) + .service(super::teams::remove_team_member), + ); +} + +pub fn notifications_config(cfg: &mut web::ServiceConfig) { + cfg.service(super::notifications::notifications_get); + + cfg.service( + web::scope("notification") + .service(super::notifications::notification_get) + .service(super::notifications::notification_delete), + ); +} + +pub fn moderation_config(cfg: &mut web::ServiceConfig) { + cfg.service(web::scope("moderation").service(moderation::get_mods)); +} + +pub fn reports_config(cfg: &mut web::ServiceConfig) { + cfg.service(reports::reports); + cfg.service(reports::report_create); + cfg.service(super::reports::delete_report); +} diff --git a/src/routes/v1/moderation.rs b/src/routes/v1/moderation.rs new file mode 100644 index 00000000..7f6adf3b --- /dev/null +++ b/src/routes/v1/moderation.rs @@ -0,0 +1,44 @@ +use crate::auth::check_is_moderator_from_headers; +use crate::database; +use crate::models::projects::{Project, ProjectStatus}; +use crate::routes::moderation::ResultCount; +use crate::routes::ApiError; +use actix_web::web; +use actix_web::{get, HttpRequest, HttpResponse}; +use sqlx::PgPool; + +#[get("mods")] +pub async fn get_mods( + req: HttpRequest, + pool: web::Data, + count: web::Query, +) -> Result { + check_is_moderator_from_headers(req.headers(), &**pool).await?; + + use futures::stream::TryStreamExt; + + let project_ids = sqlx::query!( + " + SELECT id FROM mods + WHERE status = ( + SELECT id FROM statuses WHERE status = $1 + ) + ORDER BY updated ASC + LIMIT $2; + ", + ProjectStatus::Processing.as_str(), + count.count as i64 + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { Ok(e.right().map(|m| database::models::ProjectId(m.id))) }) + .try_collect::>() + .await?; + + let projects: Vec = database::Project::get_many_full(project_ids, &**pool) + .await? + .into_iter() + .map(crate::routes::projects::convert_project) + .collect(); + + Ok(HttpResponse::Ok().json(projects)) +} diff --git a/src/routes/v1/mods.rs b/src/routes/v1/mods.rs new file mode 100644 index 00000000..e7260956 --- /dev/null +++ b/src/routes/v1/mods.rs @@ -0,0 +1,172 @@ +use crate::auth::get_user_from_headers; +use crate::file_hosting::FileHost; +use crate::models::projects::SearchRequest; +use crate::routes::project_creation::{project_create_inner, undo_uploads, CreateError}; +use crate::routes::projects::{convert_project, ProjectIds}; +use crate::routes::ApiError; +use crate::search::indexing::queue::CreationQueue; +use crate::search::{search_for_project, SearchConfig, SearchError}; +use crate::{database, models}; +use actix_multipart::Multipart; +use actix_web::web; +use actix_web::web::Data; +use actix_web::{get, post, HttpRequest, HttpResponse}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use std::sync::Arc; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ResultSearchMod { + pub mod_id: String, + pub slug: Option, + pub author: String, + pub title: String, + pub description: String, + pub categories: Vec, + pub versions: Vec, + pub downloads: i32, + pub follows: i32, + pub page_url: String, + pub icon_url: String, + pub author_url: String, + pub date_created: String, + pub date_modified: String, + pub latest_version: String, + pub license: String, + pub client_side: String, + pub server_side: String, + pub host: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SearchResults { + pub hits: Vec, + pub offset: usize, + pub limit: usize, + pub total_hits: usize, +} + +#[get("mod")] +pub async fn mod_search( + web::Query(info): web::Query, + config: web::Data, +) -> Result { + let results = search_for_project(&info, &**config).await?; + Ok(HttpResponse::Ok().json(SearchResults { + hits: results + .hits + .into_iter() + .map(|x| ResultSearchMod { + mod_id: x.project_id.clone(), + slug: x.slug, + author: x.author.clone(), + title: x.title, + description: x.description, + categories: x.categories, + versions: x.versions, + downloads: x.downloads, + follows: x.follows, + page_url: format!("https://modrinth.com/mod/{}", x.project_id), + icon_url: x.icon_url, + author_url: format!("https://modrinth.com/user/{}", x.author), + date_created: x.date_created, + date_modified: x.date_modified, + latest_version: x.latest_version, + license: x.license, + client_side: x.client_side, + server_side: x.server_side, + host: "modrinth".to_string(), + }) + .collect(), + offset: results.offset, + limit: results.limit, + total_hits: results.total_hits, + })) +} + +#[get("mods")] +pub async fn mods_get( + req: HttpRequest, + ids: web::Query, + pool: web::Data, +) -> Result { + let project_ids = serde_json::from_str::>(&*ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect(); + + let projects_data = database::models::Project::get_many_full(project_ids, &**pool).await?; + + let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); + + let mut projects = Vec::new(); + + for project_data in projects_data { + let mut authorized = !project_data.status.is_hidden(); + + if let Some(user) = &user_option { + if !authorized { + if user.role.is_mod() { + authorized = true; + } else { + let user_id: database::models::ids::UserId = user.id.into(); + + let project_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)", + project_data.inner.team_id as database::models::ids::TeamId, + user_id as database::models::ids::UserId, + ) + .fetch_one(&**pool) + .await? + .exists; + + authorized = project_exists.unwrap_or(false); + } + } + } + + if authorized { + projects.push(convert_project(project_data)); + } + } + + Ok(HttpResponse::Ok().json(projects)) +} + +#[post("mod")] +pub async fn mod_create( + req: HttpRequest, + payload: Multipart, + client: Data, + file_host: Data>, + indexing_queue: Data>, +) -> Result { + let mut transaction = client.begin().await?; + let mut uploaded_files = Vec::new(); + + let result = project_create_inner( + req, + payload, + &mut transaction, + &***file_host, + &mut uploaded_files, + &***indexing_queue, + ) + .await; + + if result.is_err() { + let undo_result = undo_uploads(&***file_host, &uploaded_files).await; + let rollback_result = transaction.rollback().await; + + if let Err(e) = undo_result { + return Err(e); + } + if let Err(e) = rollback_result { + return Err(e.into()); + } + } else { + transaction.commit().await?; + } + + result +} diff --git a/src/routes/v1/reports.rs b/src/routes/v1/reports.rs new file mode 100644 index 00000000..4ad37e07 --- /dev/null +++ b/src/routes/v1/reports.rs @@ -0,0 +1,195 @@ +use crate::auth::{check_is_moderator_from_headers, get_user_from_headers}; +use crate::models::ids::ReportId; +use crate::models::projects::{ProjectId, VersionId}; +use crate::models::users::UserId; +use crate::routes::ApiError; +use actix_web::web; +use actix_web::{get, post, HttpRequest, HttpResponse}; +use chrono::{DateTime, Utc}; +use futures::StreamExt; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; + +#[derive(Serialize, Deserialize)] +pub struct Report { + pub id: ReportId, + pub report_type: String, + pub item_id: String, + pub item_type: ItemType, + pub reporter: UserId, + pub body: String, + pub created: DateTime, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub enum ItemType { + Mod, + Version, + User, + Unknown, +} + +impl ItemType { + pub fn as_str(&self) -> &'static str { + match self { + ItemType::Mod => "mod", + ItemType::Version => "version", + ItemType::User => "user", + ItemType::Unknown => "unknown", + } + } +} +#[derive(Deserialize)] +pub struct CreateReport { + pub report_type: String, + pub item_id: String, + pub item_type: ItemType, + pub body: String, +} + +#[post("report")] +pub async fn report_create( + req: HttpRequest, + pool: web::Data, + mut body: web::Payload, +) -> Result { + let mut transaction = pool.begin().await?; + + let current_user = get_user_from_headers(req.headers(), &mut *transaction).await?; + + let mut bytes = web::BytesMut::new(); + while let Some(item) = body.next().await { + bytes.extend_from_slice(&item.map_err(|_| { + ApiError::InvalidInputError("Error while parsing request payload!".to_string()) + })?); + } + let new_report: CreateReport = serde_json::from_slice(bytes.as_ref())?; + + let id = crate::database::models::generate_report_id(&mut transaction).await?; + let report_type = crate::database::models::categories::ReportType::get_id( + &*new_report.report_type, + &mut *transaction, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError(format!("Invalid report type: {}", new_report.report_type)) + })?; + let mut report = crate::database::models::report_item::Report { + id, + report_type_id: report_type, + project_id: None, + version_id: None, + user_id: None, + body: new_report.body.clone(), + reporter: current_user.id.into(), + created: chrono::Utc::now(), + }; + + match new_report.item_type { + ItemType::Mod => { + report.project_id = Some( + serde_json::from_str::(&*format!("\"{}\"", new_report.item_id))?.into(), + ) + } + ItemType::Version => { + report.version_id = Some( + serde_json::from_str::(&*format!("\"{}\"", new_report.item_id))?.into(), + ) + } + ItemType::User => { + report.user_id = Some( + serde_json::from_str::(&*format!("\"{}\"", new_report.item_id))?.into(), + ) + } + ItemType::Unknown => { + return Err(ApiError::InvalidInputError(format!( + "Invalid report item type: {}", + new_report.item_type.as_str() + ))) + } + } + + report.insert(&mut transaction).await?; + transaction.commit().await?; + + Ok(HttpResponse::Ok().json(Report { + id: id.into(), + report_type: new_report.report_type.clone(), + item_id: new_report.item_id.clone(), + item_type: new_report.item_type.clone(), + reporter: current_user.id, + body: new_report.body.clone(), + created: chrono::Utc::now(), + })) +} + +#[derive(Deserialize)] +pub struct ResultCount { + #[serde(default = "default_count")] + count: i16, +} + +fn default_count() -> i16 { + 100 +} + +#[get("report")] +pub async fn reports( + req: HttpRequest, + pool: web::Data, + count: web::Query, +) -> Result { + check_is_moderator_from_headers(req.headers(), &**pool).await?; + + use futures::stream::TryStreamExt; + + let report_ids = sqlx::query!( + " + SELECT id FROM reports + ORDER BY created ASC + LIMIT $1; + ", + count.count as i64 + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right() + .map(|m| crate::database::models::ids::ReportId(m.id))) + }) + .try_collect::>() + .await?; + + let query_reports = + crate::database::models::report_item::Report::get_many(report_ids, &**pool).await?; + + let mut reports = Vec::new(); + + for x in query_reports { + let mut item_id = "".to_string(); + let mut item_type = ItemType::Unknown; + + if let Some(project_id) = x.project_id { + item_id = serde_json::to_string::(&project_id.into())?; + item_type = ItemType::Mod; + } else if let Some(version_id) = x.version_id { + item_id = serde_json::to_string::(&version_id.into())?; + item_type = ItemType::Version; + } else if let Some(user_id) = x.user_id { + item_id = serde_json::to_string::(&user_id.into())?; + item_type = ItemType::User; + } + + reports.push(Report { + id: x.id.into(), + report_type: x.report_type, + item_id, + item_type, + reporter: x.reporter.into(), + body: x.body, + created: x.created, + }) + } + + Ok(HttpResponse::Ok().json(reports)) +} diff --git a/src/routes/v1/tags.rs b/src/routes/v1/tags.rs new file mode 100644 index 00000000..f5482e65 --- /dev/null +++ b/src/routes/v1/tags.rs @@ -0,0 +1,81 @@ +use crate::auth::check_is_admin_from_headers; +use crate::database::models::categories::{Category, Loader, ProjectType}; +use crate::routes::ApiError; +use actix_web::{get, put, web}; +use actix_web::{HttpRequest, HttpResponse}; +use sqlx::PgPool; + +const DEFAULT_ICON: &str = r#""#; + +#[get("category")] +pub async fn category_list(pool: web::Data) -> Result { + let results = Category::list(&**pool) + .await? + .into_iter() + .filter(|x| &*x.project_type == "mod") + .map(|x| x.project_type) + .collect::>(); + Ok(HttpResponse::Ok().json(results)) +} + +#[put("category/{name}")] +pub async fn category_create( + req: HttpRequest, + pool: web::Data, + category: web::Path<(String,)>, +) -> Result { + check_is_admin_from_headers(req.headers(), &**pool).await?; + + let name = category.into_inner().0; + + let project_type = crate::database::models::ProjectTypeId::get_id("mod".to_string(), &**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInputError("Specified project type does not exist!".to_string()) + })?; + + let _id = Category::builder() + .name(&name)? + .icon(DEFAULT_ICON)? + .project_type(&project_type)? + .insert(&**pool) + .await?; + + Ok(HttpResponse::NoContent().body("")) +} + +#[get("loader")] +pub async fn loader_list(pool: web::Data) -> Result { + let results = Loader::list(&**pool) + .await? + .into_iter() + .filter(|x| x.supported_project_types.contains(&"mod".to_string())) + .map(|x| x.loader) + .collect::>(); + + Ok(HttpResponse::Ok().json(results)) +} + +#[put("loader/{name}")] +pub async fn loader_create( + req: HttpRequest, + pool: web::Data, + loader: web::Path<(String,)>, +) -> Result { + check_is_admin_from_headers(req.headers(), &**pool).await?; + + let name = loader.into_inner().0; + let mut transaction = pool.begin().await?; + + let project_types = + ProjectType::get_many_id(&vec!["mod".to_string()], &mut *transaction).await?; + + let _id = Loader::builder() + .name(&name)? + .icon(DEFAULT_ICON)? + .supported_project_types(&*project_types.into_iter().map(|x| x.id).collect::>())? + .insert(&mut transaction) + .await?; + + Ok(HttpResponse::NoContent().body("")) +} diff --git a/src/routes/v1/users.rs b/src/routes/v1/users.rs new file mode 100644 index 00000000..b0a5d38f --- /dev/null +++ b/src/routes/v1/users.rs @@ -0,0 +1,44 @@ +use crate::auth::get_user_from_headers; +use crate::database::models::User; +use crate::models::ids::UserId; +use crate::models::projects::ProjectStatus; +use crate::routes::ApiError; +use actix_web::web; +use actix_web::{get, HttpRequest, HttpResponse}; +use sqlx::PgPool; + +#[get("{user_id}/mods")] +pub async fn mods_list( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await.ok(); + + let id_option = + crate::database::models::User::get_id_from_username_or_id(info.into_inner().0, &**pool) + .await?; + + if let Some(id) = id_option { + let user_id: UserId = id.into(); + + let project_data = if let Some(current_user) = user { + if current_user.role.is_mod() || current_user.id == user_id { + User::get_projects_private(id, &**pool).await? + } else { + User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await? + } + } else { + User::get_projects(id, ProjectStatus::Approved.as_str(), &**pool).await? + }; + + let response = project_data + .into_iter() + .map(|v| v.into()) + .collect::>(); + + Ok(HttpResponse::Ok().json(response)) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} diff --git a/src/routes/v1/versions.rs b/src/routes/v1/versions.rs new file mode 100644 index 00000000..e84b90ae --- /dev/null +++ b/src/routes/v1/versions.rs @@ -0,0 +1,416 @@ +use crate::auth::get_user_from_headers; +use crate::file_hosting::FileHost; +use crate::models::ids::{ProjectId, UserId, VersionId}; +use crate::models::projects::{Dependency, GameVersion, Loader, Version, VersionFile, VersionType}; +use crate::models::teams::Permissions; +use crate::routes::versions::{convert_version, VersionIds, VersionListFilters}; +use crate::routes::ApiError; +use crate::{database, models, Pepper}; +use actix_web::{delete, get, web, HttpRequest, HttpResponse}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use std::borrow::Borrow; +use std::sync::Arc; + +/// A specific version of a mod +#[derive(Serialize, Deserialize)] +pub struct LegacyVersion { + pub id: VersionId, + pub mod_id: ProjectId, + pub author_id: UserId, + pub featured: bool, + pub name: String, + pub version_number: String, + pub changelog: String, + pub changelog_url: Option, + pub date_published: DateTime, + pub downloads: u32, + pub version_type: VersionType, + pub files: Vec, + pub dependencies: Vec, + pub game_versions: Vec, + pub loaders: Vec, +} + +fn convert_to_legacy(version: Version) -> LegacyVersion { + LegacyVersion { + id: version.id, + mod_id: version.project_id, + author_id: version.author_id, + featured: version.featured, + name: version.name, + version_number: version.version_number, + changelog: version.changelog, + changelog_url: version.changelog_url, + date_published: version.date_published, + downloads: version.downloads, + version_type: version.version_type, + files: version.files, + dependencies: version.dependencies, + game_versions: version.game_versions, + loaders: version.loaders, + } +} + +#[get("version")] +pub async fn version_list( + info: web::Path<(String,)>, + web::Query(filters): web::Query, + pool: web::Data, +) -> Result { + let string = info.into_inner().0; + + let result = database::models::Project::get_from_slug_or_project_id(string, &**pool).await?; + + if let Some(project) = result { + let id = project.id; + + let version_ids = database::models::Version::get_project_versions( + id, + filters + .game_versions + .as_ref() + .map(|x| serde_json::from_str(x).unwrap_or_default()), + filters + .loaders + .as_ref() + .map(|x| serde_json::from_str(x).unwrap_or_default()), + &**pool, + ) + .await?; + + let mut versions = database::models::Version::get_many_full(version_ids, &**pool).await?; + + let mut response = versions + .iter() + .cloned() + .filter(|version| { + filters + .featured + .map(|featured| featured == version.featured) + .unwrap_or(true) + }) + .map(convert_version) + .map(convert_to_legacy) + .collect::>(); + + versions.sort_by(|a, b| b.date_published.cmp(&a.date_published)); + + // Attempt to populate versions with "auto featured" versions + if response.is_empty() && !versions.is_empty() && filters.featured.unwrap_or(false) { + let loaders = database::models::categories::Loader::list(&**pool).await?; + let game_versions = + database::models::categories::GameVersion::list_filter(None, Some(true), &**pool) + .await?; + + let mut joined_filters = Vec::new(); + for game_version in &game_versions { + for loader in &loaders { + joined_filters.push((game_version, loader)) + } + } + + joined_filters.into_iter().for_each(|filter| { + versions + .iter() + .find(|version| { + version.game_versions.contains(&filter.0.version) + && version.loaders.contains(&filter.1.loader) + }) + .map(|version| { + response.push(convert_to_legacy(convert_version(version.clone()))) + }) + .unwrap_or(()); + }); + + if response.is_empty() { + versions + .into_iter() + .for_each(|version| response.push(convert_to_legacy(convert_version(version)))); + } + } + + response.sort_by(|a, b| b.date_published.cmp(&a.date_published)); + response.dedup_by(|a, b| a.id == b.id); + + Ok(HttpResponse::Ok().json(response)) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +#[get("versions")] +pub async fn versions_get( + ids: web::Query, + pool: web::Data, +) -> Result { + let version_ids = serde_json::from_str::>(&*ids.ids)? + .into_iter() + .map(|x| x.into()) + .collect(); + let versions_data = database::models::Version::get_many_full(version_ids, &**pool).await?; + + let mut versions = Vec::new(); + + for version_data in versions_data { + versions.push(convert_to_legacy(convert_version(version_data))); + } + + Ok(HttpResponse::Ok().json(versions)) +} + +#[get("{version_id}")] +pub async fn version_get( + info: web::Path<(models::ids::VersionId,)>, + pool: web::Data, +) -> Result { + let id = info.into_inner().0; + let version_data = database::models::Version::get_full(id.into(), &**pool).await?; + + if let Some(data) = version_data { + Ok(HttpResponse::Ok().json(convert_to_legacy(convert_version(data)))) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +#[derive(Deserialize)] +pub struct Algorithm { + #[serde(default = "default_algorithm")] + algorithm: String, +} + +fn default_algorithm() -> String { + "sha1".into() +} + +// under /api/v1/version_file/{hash} +#[get("{version_id}")] +pub async fn get_version_from_hash( + info: web::Path<(String,)>, + pool: web::Data, + algorithm: web::Query, +) -> Result { + let hash = info.into_inner().0.to_lowercase(); + + let result = sqlx::query!( + " + SELECT f.version_id version_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + WHERE h.algorithm = $2 AND h.hash = $1 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .fetch_optional(&**pool) + .await?; + + if let Some(id) = result { + let version_data = database::models::Version::get_full( + database::models::VersionId(id.version_id), + &**pool, + ) + .await?; + + if let Some(data) = version_data { + Ok(HttpResponse::Ok().json(super::versions::convert_version(data))) + } else { + Ok(HttpResponse::NotFound().body("")) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +#[derive(Serialize, Deserialize)] +pub struct DownloadRedirect { + pub url: String, +} + +// under /api/v1/version_file/{hash}/download +#[allow(clippy::await_holding_refcell_ref)] +#[get("{version_id}/download")] +pub async fn download_version( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + algorithm: web::Query, + pepper: web::Data, +) -> Result { + let hash = info.into_inner().0; + + let result = sqlx::query!( + " + SELECT f.url url, f.id id, f.version_id version_id, v.mod_id mod_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + INNER JOIN versions v ON v.id = f.version_id + WHERE h.algorithm = $2 AND h.hash = $1 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .fetch_optional(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + if let Some(id) = result { + let real_ip = req.connection_info(); + let ip_option = real_ip.borrow().remote_addr(); + + if let Some(ip) = ip_option { + let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest(); + + let download_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM downloads WHERE version_id = $1 AND date > (CURRENT_DATE - INTERVAL '30 minutes ago') AND identifier = $2)", + id.version_id, + hash, + ) + .fetch_one(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))? + .exists.unwrap_or(false); + + if !download_exists { + sqlx::query!( + " + INSERT INTO downloads ( + version_id, identifier + ) + VALUES ( + $1, $2 + ) + ", + id.version_id, + hash + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + sqlx::query!( + " + UPDATE versions + SET downloads = downloads + 1 + WHERE id = $1 + ", + id.version_id, + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + + sqlx::query!( + " + UPDATE mods + SET downloads = downloads + 1 + WHERE id = $1 + ", + id.mod_id, + ) + .execute(&**pool) + .await + .map_err(|e| ApiError::DatabaseError(e.into()))?; + } + } + Ok(HttpResponse::TemporaryRedirect() + .header("Location", &*id.url) + .json(DownloadRedirect { url: id.url })) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +// under /api/v1/version_file/{hash} +#[delete("{version_id}")] +pub async fn delete_file( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + file_host: web::Data>, + algorithm: web::Query, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let hash = info.into_inner().0.to_lowercase(); + + let result = sqlx::query!( + " + SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id project_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + INNER JOIN versions v ON v.id = f.version_id + WHERE h.algorithm = $2 AND h.hash = $1 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .fetch_optional(&**pool) + .await + ?; + + if let Some(row) = result { + if !user.role.is_mod() { + let team_member = database::models::TeamMember::get_from_user_id_version( + database::models::ids::VersionId(row.version_id), + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::DatabaseError)? + .ok_or_else(|| { + ApiError::CustomAuthenticationError( + "You don't have permission to delete this file!".to_string(), + ) + })?; + + if !team_member + .permissions + .contains(Permissions::DELETE_VERSION) + { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to delete this file!".to_string(), + )); + } + } + + let mut transaction = pool.begin().await?; + + sqlx::query!( + " + DELETE FROM hashes + WHERE file_id = $1 + ", + row.id + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM files + WHERE files.id = $1 + ", + row.id, + ) + .execute(&mut *transaction) + .await?; + + let project_id: models::projects::ProjectId = + database::models::ids::ProjectId(row.project_id).into(); + file_host + .delete_file_version( + "", + &format!( + "data/{}/versions/{}/{}", + project_id, row.version_number, row.filename + ), + ) + .await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index 0ea035bc..fbabc2a1 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -3,29 +3,43 @@ use crate::database::models; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::version_item::{VersionBuilder, VersionFileBuilder}; use crate::file_hosting::FileHost; -use crate::models::mods::{ - Dependency, GameVersion, ModId, ModLoader, Version, VersionFile, VersionId, VersionType, +use crate::models::projects::{ + Dependency, GameVersion, Loader, ProjectId, Version, VersionFile, VersionId, VersionType, }; use crate::models::teams::Permissions; -use crate::routes::mod_creation::{CreateError, UploadedFile}; +use crate::routes::project_creation::{CreateError, UploadedFile}; +use crate::validate::{validate_file, ValidationResult}; use actix_multipart::{Field, Multipart}; use actix_web::web::Data; use actix_web::{post, HttpRequest, HttpResponse}; use futures::stream::StreamExt; +use lazy_static::lazy_static; +use regex::Regex; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; +use validator::Validate; -#[derive(Serialize, Deserialize, Clone)] +lazy_static! { + static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_\-.]*$").unwrap(); +} + +#[derive(Serialize, Deserialize, Validate, Clone)] pub struct InitialVersionData { - pub mod_id: Option, + #[serde(alias = "mod_id")] + pub project_id: Option, + #[validate(length(min = 1, max = 256))] pub file_parts: Vec, + #[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")] pub version_number: String, + #[validate(length(min = 3, max = 256))] pub version_title: String, + #[validate(length(max = 65536))] pub version_body: Option, + #[validate(length(min = 0, max = 256))] pub dependencies: Vec, pub game_versions: Vec, pub release_channel: VersionType, - pub loaders: Vec, + pub loaders: Vec, pub featured: bool, } @@ -34,42 +48,6 @@ struct InitialFileData { // TODO: hashes? } -pub fn check_version(version: &InitialVersionData) -> Result<(), CreateError> { - /* - # InitialVersionData - file_parts: Vec, 1..=256 - version_number: 1..=64, - version_title: 3..=256, - version_body: max of 64KiB, - game_versions: Vec, 1..=256 - release_channel: VersionType, - loaders: Vec, 1..=256 - */ - use super::mod_creation::check_length; - - version - .file_parts - .iter() - .try_for_each(|f| check_length(1..=256, "file part name", f))?; - - check_length(1..=64, "version number", &version.version_number)?; - check_length(3..=256, "version title", &version.version_title)?; - if let Some(body) = &version.version_body { - check_length(..65536, "version body", body)?; - } - - version - .game_versions - .iter() - .try_for_each(|v| check_length(1..=256, "game version", &v.0))?; - version - .loaders - .iter() - .try_for_each(|l| check_length(1..=256, "loader name", &l.0))?; - - Ok(()) -} - // under `/api/v1/version` #[post("version")] pub async fn version_create( @@ -91,7 +69,8 @@ pub async fn version_create( .await; if result.is_err() { - let undo_result = super::mod_creation::undo_uploads(&***file_host, &uploaded_files).await; + let undo_result = + super::project_creation::undo_uploads(&***file_host, &uploaded_files).await; let rollback_result = transaction.rollback().await; if let Err(e) = undo_result { @@ -119,6 +98,8 @@ async fn version_create_inner( let mut initial_version_data = None; let mut version_builder = None; + let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?; + let user = get_user_from_headers(req.headers(), &mut *transaction).await?; while let Some(item) = payload.next().await { @@ -139,34 +120,36 @@ async fn version_create_inner( let version_create_data: InitialVersionData = serde_json::from_slice(&data)?; initial_version_data = Some(version_create_data); let version_create_data = initial_version_data.as_ref().unwrap(); - if version_create_data.mod_id.is_none() { - return Err(CreateError::MissingValueError("Missing mod id".to_string())); + if version_create_data.project_id.is_none() { + return Err(CreateError::MissingValueError( + "Missing project id".to_string(), + )); } - check_version(version_create_data)?; + version_create_data.validate()?; - let mod_id: models::ModId = version_create_data.mod_id.unwrap().into(); + let project_id: models::ProjectId = version_create_data.project_id.unwrap().into(); - // Ensure that the mod this version is being added to exists + // Ensure that the project this version is being added to exists let results = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM mods WHERE id=$1)", - mod_id as models::ModId + project_id as models::ProjectId ) .fetch_one(&mut *transaction) .await?; if !results.exists.unwrap_or(false) { return Err(CreateError::InvalidInput( - "An invalid mod id was supplied".to_string(), + "An invalid project id was supplied".to_string(), )); } - // Check whether there is already a version of this mod with the + // Check whether there is already a version of this project with the // same version number let results = sqlx::query!( "SELECT EXISTS(SELECT 1 FROM versions WHERE (version_number=$1) AND (mod_id=$2))", version_create_data.version_number, - mod_id as models::ModId, + project_id as models::ProjectId, ) .fetch_one(&mut *transaction) .await?; @@ -178,15 +161,18 @@ async fn version_create_inner( } // Check that the user creating this version is a team member - // of the mod the version is being added to. - let team_member = - models::TeamMember::get_from_user_id_mod(mod_id, user.id.into(), &mut *transaction) - .await? - .ok_or_else(|| { - CreateError::CustomAuthenticationError( - "You don't have permission to upload this version!".to_string(), - ) - })?; + // of the project the version is being added to. + let team_member = models::TeamMember::get_from_user_id_project( + project_id, + user.id.into(), + &mut *transaction, + ) + .await? + .ok_or_else(|| { + CreateError::CustomAuthenticationError( + "You don't have permission to upload this version!".to_string(), + ) + })?; if !team_member .permissions @@ -206,13 +192,17 @@ async fn version_create_inner( .await? .expect("Release channel not found in database"); - let mut game_versions = Vec::with_capacity(version_create_data.game_versions.len()); - for v in &version_create_data.game_versions { - let id = models::categories::GameVersion::get_id(&v.0, &mut *transaction) - .await? - .ok_or_else(|| CreateError::InvalidGameVersion(v.0.clone()))?; - game_versions.push(id); - } + let game_versions = version_create_data + .game_versions + .iter() + .map(|x| { + all_game_versions + .iter() + .find(|y| y.version == x.0) + .ok_or_else(|| CreateError::InvalidGameVersion(x.0.clone())) + .map(|y| y.id) + }) + .collect::, CreateError>>()?; let mut loaders = Vec::with_capacity(version_create_data.loaders.len()); for l in &version_create_data.loaders { @@ -230,7 +220,7 @@ async fn version_create_inner( version_builder = Some(VersionBuilder { version_id: version_id.into(), - mod_id: version_create_data.mod_id.unwrap().into(), + project_id, author_id: user.id.into(), name: version_create_data.version_title.clone(), version_number: version_create_data.version_number.clone(), @@ -253,19 +243,38 @@ async fn version_create_inner( CreateError::InvalidInput(String::from("`data` field must come before file fields")) })?; - let file_builder = upload_file( + let project_type = sqlx::query!( + " + SELECT name FROM project_types pt + INNER JOIN mods ON mods.project_type = pt.id + WHERE mods.id = $1 + ", + version.project_id as models::ProjectId, + ) + .fetch_one(&mut *transaction) + .await? + .name; + + let version_data = initial_version_data + .clone() + .ok_or_else(|| CreateError::InvalidInput("`data` field is required".to_string()))?; + + upload_file( &mut field, file_host, uploaded_files, + &mut version.files, &cdn_url, &content_disposition, - version.mod_id.into(), + version.project_id.into(), &version.version_number, + &*project_type, + version_data.loaders, + version_data.game_versions, + &all_game_versions, + false, ) .await?; - - // Add the newly uploaded file to the existing or new version - version.files.push(file_builder); } let version_data = initial_version_data @@ -278,7 +287,7 @@ async fn version_create_inner( SELECT m.title FROM mods m WHERE id = $1 ", - builder.mod_id as crate::database::models::ids::ModId + builder.project_id as crate::database::models::ids::ProjectId ) .fetch_one(&mut *transaction) .await?; @@ -290,7 +299,7 @@ async fn version_create_inner( SELECT follower_id FROM mod_follows WHERE mod_id = $1 ", - builder.mod_id as crate::database::models::ids::ModId + builder.project_id as crate::database::models::ids::ProjectId ) .fetch_many(&mut *transaction) .try_filter_map(|e| async { @@ -300,17 +309,17 @@ async fn version_create_inner( .try_collect::>() .await?; - let mod_id: ModId = builder.mod_id.into(); + let project_id: ProjectId = builder.project_id.into(); let version_id: VersionId = builder.version_id.into(); NotificationBuilder { - title: "A mod you followed has been updated!".to_string(), + title: "A project you followed has been updated!".to_string(), text: format!( - "Mod {} has been updated to version {}", + "Project {} has been updated to version {}", result.title, version_data.version_number.clone() ), - link: format!("mod/{}/version/{}", mod_id, version_id), + link: format!("project/{}/version/{}", project_id, version_id), actions: vec![], } .insert_many(users, &mut *transaction) @@ -318,7 +327,7 @@ async fn version_create_inner( let response = Version { id: builder.version_id.into(), - mod_id: builder.mod_id.into(), + project_id: builder.project_id.into(), author_id: user.id, featured: builder.featured, name: builder.name.clone(), @@ -388,7 +397,8 @@ pub async fn upload_file_to_version( .await; if result.is_err() { - let undo_result = super::mod_creation::undo_uploads(&***file_host, &uploaded_files).await; + let undo_result = + super::project_creation::undo_uploads(&***file_host, &uploaded_files).await; let rollback_result = transaction.rollback().await; if let Err(e) = undo_result { @@ -419,16 +429,7 @@ async fn upload_file_to_version_inner( let user = get_user_from_headers(req.headers(), &mut *transaction).await?; - let result = sqlx::query!( - " - SELECT mod_id, version_number, author_id - FROM versions - WHERE id = $1 - ", - version_id as models::VersionId, - ) - .fetch_optional(&mut *transaction) - .await?; + let result = models::Version::get_full(version_id, &mut *transaction).await?; let version = match result { Some(v) => v, @@ -457,9 +458,23 @@ async fn upload_file_to_version_inner( )); } - let mod_id = ModId(version.mod_id as u64); + let project_id = ProjectId(version.project_id.0 as u64); let version_number = version.version_number; + let project_type = sqlx::query!( + " + SELECT name FROM project_types pt + INNER JOIN mods ON mods.project_type = pt.id + WHERE mods.id = $1 + ", + version.project_id as models::ProjectId, + ) + .fetch_one(&mut *transaction) + .await? + .name; + + let all_game_versions = models::categories::GameVersion::list(&mut *transaction).await?; + while let Some(item) = payload.next().await { let mut field: Field = item.map_err(CreateError::MultipartError)?; let content_disposition = field.content_disposition().ok_or_else(|| { @@ -485,19 +500,27 @@ async fn upload_file_to_version_inner( CreateError::InvalidInput(String::from("`data` field must come before file fields")) })?; - let file_builder = upload_file( + upload_file( &mut field, file_host, uploaded_files, + &mut file_builders, &cdn_url, &content_disposition, - mod_id, + project_id, &version_number, + &*project_type, + version.loaders.clone().into_iter().map(Loader).collect(), + version + .game_versions + .clone() + .into_iter() + .map(GameVersion) + .collect(), + &all_game_versions, + true, ) .await?; - - // TODO: Malware scan + file validation - file_builders.push(file_builder); } if file_builders.is_empty() { @@ -514,19 +537,26 @@ async fn upload_file_to_version_inner( } // This function is used for adding a file to a version, uploading the initial -// files for a version, and for uploading the initial version files for a mod +// files for a version, and for uploading the initial version files for a project +#[allow(clippy::too_many_arguments)] pub async fn upload_file( field: &mut Field, file_host: &dyn FileHost, uploaded_files: &mut Vec, + version_files: &mut Vec, cdn_url: &str, content_disposition: &actix_web::http::header::ContentDisposition, - mod_id: crate::models::ids::ModId, + project_id: crate::models::ids::ProjectId, version_number: &str, -) -> Result { + project_type: &str, + loaders: Vec, + game_versions: Vec, + all_game_versions: &[models::categories::GameVersion], + ignore_primary: bool, +) -> Result<(), CreateError> { let (file_name, file_extension) = get_name_ext(content_disposition)?; - let content_type = mod_file_type(file_extension) + let content_type = project_file_type(file_extension) .ok_or_else(|| CreateError::InvalidFileType(file_extension.to_string()))?; let mut data = Vec::new(); @@ -534,20 +564,32 @@ pub async fn upload_file( data.extend_from_slice(&chunk.map_err(CreateError::MultipartError)?); } - // Mod file size limit of 25MiB + // Project file size limit of 25MiB const FILE_SIZE_CAP: usize = 25 * (2 << 30); - // TODO: override file size cap for authorized users or mods + // TODO: override file size cap for authorized users or projects if data.len() >= FILE_SIZE_CAP { return Err(CreateError::InvalidInput( - String::from("Mod file exceeds the maximum of 25MiB. Contact a moderator or admin to request permission to upload larger files.") + String::from("Project file exceeds the maximum of 25MiB. Contact a moderator or admin to request permission to upload larger files.") )); } + let validation_result = validate_file( + data.as_slice(), + file_extension, + project_type, + loaders, + game_versions, + all_game_versions, + )?; + let upload_data = file_host .upload_file( content_type, - &format!("data/{}/versions/{}/{}", mod_id, version_number, file_name), + &format!( + "data/{}/versions/{}/{}", + project_id, version_number, file_name + ), data.to_vec(), ) .await?; @@ -558,7 +600,7 @@ pub async fn upload_file( }); // TODO: Malware scan + file validation - Ok(models::version_item::VersionFileBuilder { + version_files.push(models::version_item::VersionFileBuilder { filename: file_name.to_string(), url: format!("{}/{}", cdn_url, upload_data.file_name), hashes: vec![ @@ -575,12 +617,16 @@ pub async fn upload_file( hash: upload_data.content_sha512.into_bytes(), }, ], - primary: uploaded_files.len() == 1, - }) + primary: validation_result == ValidationResult::Pass + && version_files.iter().all(|x| !x.primary) + && !ignore_primary, + }); + + Ok(()) } -// Currently we only support jar mods; this may change in the future (datapacks?) -fn mod_file_type(ext: &str) -> Option<&str> { +// Currently we only support jar projects; this may change in the future (datapacks?) +fn project_file_type(ext: &str) -> Option<&str> { match ext { "jar" => Some("application/java-archive"), _ => None, diff --git a/src/routes/version_file.rs b/src/routes/version_file.rs new file mode 100644 index 00000000..360ec880 --- /dev/null +++ b/src/routes/version_file.rs @@ -0,0 +1,519 @@ +use super::ApiError; +use crate::auth::get_user_from_headers; +use crate::file_hosting::FileHost; +use crate::models; +use crate::models::projects::{GameVersion, Loader}; +use crate::models::teams::Permissions; +use crate::{database, Pepper}; +use actix_web::{delete, get, post, web, HttpRequest, HttpResponse}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::sync::Arc; + +#[derive(Deserialize)] +pub struct Algorithm { + #[serde(default = "default_algorithm")] + algorithm: String, +} + +fn default_algorithm() -> String { + "sha1".into() +} + +// under /api/v1/version_file/{hash} +#[get("{version_id}")] +pub async fn get_version_from_hash( + info: web::Path<(String,)>, + pool: web::Data, + algorithm: web::Query, +) -> Result { + let hash = info.into_inner().0.to_lowercase(); + + let result = sqlx::query!( + " + SELECT f.version_id version_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + WHERE h.algorithm = $2 AND h.hash = $1 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .fetch_optional(&**pool) + .await?; + + if let Some(id) = result { + let version_data = database::models::Version::get_full( + database::models::VersionId(id.version_id), + &**pool, + ) + .await?; + + if let Some(data) = version_data { + Ok(HttpResponse::Ok().json(super::versions::convert_version(data))) + } else { + Ok(HttpResponse::NotFound().body("")) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +#[derive(Serialize, Deserialize)] +pub struct DownloadRedirect { + pub url: String, +} + +// under /api/v1/version_file/{hash}/download +#[get("{version_id}/download")] +pub async fn download_version( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + algorithm: web::Query, + pepper: web::Data, +) -> Result { + let hash = info.into_inner().0.to_lowercase(); + let mut transaction = pool.begin().await?; + + let result = sqlx::query!( + " + SELECT f.url url, f.id id, f.version_id version_id, v.mod_id project_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + INNER JOIN versions v ON v.id = f.version_id + WHERE h.algorithm = $2 AND h.hash = $1 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .fetch_optional(&mut *transaction) + .await?; + + if let Some(id) = result { + download_version_inner( + database::models::VersionId(id.version_id), + database::models::ProjectId(id.project_id), + &req, + &mut transaction, + &pepper, + ) + .await?; + + transaction.commit().await?; + + Ok(HttpResponse::TemporaryRedirect() + .header("Location", &*id.url) + .json(DownloadRedirect { url: id.url })) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +async fn download_version_inner( + version_id: database::models::VersionId, + project_id: database::models::ProjectId, + req: &HttpRequest, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + pepper: &web::Data, +) -> Result<(), ApiError> { + let real_ip = req.connection_info(); + let ip_option = real_ip.borrow().remote_addr(); + + if let Some(ip) = ip_option { + let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest(); + + let download_exists = sqlx::query!( + "SELECT EXISTS(SELECT 1 FROM downloads WHERE version_id = $1 AND date > (CURRENT_DATE - INTERVAL '30 minutes ago') AND identifier = $2)", + version_id as database::models::VersionId, + hash, + ) + .fetch_one(&mut *transaction) + .await + ? + .exists.unwrap_or(false); + + if !download_exists { + sqlx::query!( + " + INSERT INTO downloads ( + version_id, identifier + ) + VALUES ( + $1, $2 + ) + ", + version_id as database::models::VersionId, + hash + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + UPDATE versions + SET downloads = downloads + 1 + WHERE id = $1 + ", + version_id as database::models::VersionId, + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + UPDATE mods + SET downloads = downloads + 1 + WHERE id = $1 + ", + project_id as database::models::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + } + + Ok(()) +} + +// under /api/v1/version_file/{hash} +#[delete("{version_id}")] +pub async fn delete_file( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + file_host: web::Data>, + algorithm: web::Query, +) -> Result { + let user = get_user_from_headers(req.headers(), &**pool).await?; + + let hash = info.into_inner().0.to_lowercase(); + + let result = sqlx::query!( + " + SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id project_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + INNER JOIN versions v ON v.id = f.version_id + WHERE h.algorithm = $2 AND h.hash = $1 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .fetch_optional(&**pool) + .await + ?; + + if let Some(row) = result { + if !user.role.is_mod() { + let team_member = database::models::TeamMember::get_from_user_id_version( + database::models::ids::VersionId(row.version_id), + user.id.into(), + &**pool, + ) + .await + .map_err(ApiError::DatabaseError)? + .ok_or_else(|| { + ApiError::CustomAuthenticationError( + "You don't have permission to delete this file!".to_string(), + ) + })?; + + if !team_member + .permissions + .contains(Permissions::DELETE_VERSION) + { + return Err(ApiError::CustomAuthenticationError( + "You don't have permission to delete this file!".to_string(), + )); + } + } + + let mut transaction = pool.begin().await?; + + sqlx::query!( + " + DELETE FROM hashes + WHERE file_id = $1 + ", + row.id + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + DELETE FROM files + WHERE files.id = $1 + ", + row.id, + ) + .execute(&mut *transaction) + .await?; + + let project_id: models::projects::ProjectId = + database::models::ids::ProjectId(row.project_id).into(); + file_host + .delete_file_version( + "", + &format!( + "data/{}/versions/{}/{}", + project_id, row.version_number, row.filename + ), + ) + .await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +#[derive(Deserialize)] +pub struct UpdateData { + pub hash: (String, String), + pub loaders: Vec, + pub game_versions: Vec, +} + +#[post("{version_id}/update")] +pub async fn get_update_from_hash( + info: web::Path<(String,)>, + pool: web::Data, + algorithm: web::Query, + update_data: web::Json, +) -> Result { + let hash = info.into_inner().0.to_lowercase(); + + // get version_id from hash + // get mod_id from hash + // get latest version satisfying conditions - if not found + + let result = sqlx::query!( + " + SELECT v.mod_id project_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + INNER JOIN versions v ON v.id = f.version_id + WHERE h.algorithm = $2 AND h.hash = $1 + ", + hash.as_bytes(), + algorithm.algorithm + ) + .fetch_optional(&**pool) + .await?; + + if let Some(id) = result { + let version_ids = database::models::Version::get_project_versions( + database::models::ProjectId(id.project_id), + Some( + update_data + .game_versions + .clone() + .into_iter() + .map(|x| x.0) + .collect(), + ), + Some( + update_data + .loaders + .clone() + .into_iter() + .map(|x| x.0) + .collect(), + ), + &**pool, + ) + .await?; + + if let Some(version_id) = version_ids.last() { + let version_data = database::models::Version::get_full(*version_id, &**pool).await?; + + if let Some(data) = version_data { + Ok(HttpResponse::Ok().json(super::versions::convert_version(data))) + } else { + Ok(HttpResponse::NotFound().body("")) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } + } else { + Ok(HttpResponse::NotFound().body("")) + } +} + +// Requests above with multiple versions below +#[derive(Deserialize)] +pub struct FileHashes { + pub algorithm: String, + pub hashes: Vec, +} + +// under /api/v2/version_files +#[post("/")] +pub async fn get_versions_from_hashes( + pool: web::Data, + file_data: web::Json, +) -> Result { + let hashes_parsed: Vec> = file_data + .hashes + .iter() + .map(|x| x.as_bytes().to_vec()) + .collect(); + + let result = sqlx::query!( + " + SELECT h.hash hash, h.algorithm algorithm, f.version_id version_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + WHERE h.algorithm = $2 AND h.hash IN (SELECT * FROM UNNEST($1::bytea[])) + ", + hashes_parsed.as_slice(), + file_data.algorithm + ) + .fetch_all(&**pool) + .await?; + + let versions_data = database::models::Version::get_many_full( + result + .iter() + .map(|x| database::models::VersionId(x.version_id)) + .collect(), + &**pool, + ) + .await?; + + let mut response = HashMap::new(); + + for row in result { + if let Some(version) = versions_data.iter().find(|x| x.id.0 == row.version_id) { + response.insert(row.hash, super::versions::convert_version(version.clone())); + } + } + + Ok(HttpResponse::Ok().json(response)) +} + +#[post("download")] +pub async fn download_files( + req: HttpRequest, + pool: web::Data, + file_data: web::Json, + pepper: web::Data, +) -> Result { + let hashes_parsed: Vec> = file_data + .hashes + .iter() + .map(|x| x.as_bytes().to_vec()) + .collect(); + + let mut transaction = pool.begin().await?; + + let result = sqlx::query!( + " + SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + INNER JOIN versions v ON v.id = f.version_id + WHERE h.algorithm = $2 AND h.hash IN (SELECT * FROM UNNEST($1::bytea[])) + ", + hashes_parsed.as_slice(), + file_data.algorithm + ) + .fetch_all(&mut *transaction) + .await?; + + let mut response = HashMap::new(); + + for row in result { + download_version_inner( + database::models::VersionId(row.version_id), + database::models::ProjectId(row.project_id), + &req, + &mut transaction, + &pepper, + ) + .await?; + response.insert(row.hash, row.url); + } + + Ok(HttpResponse::Ok().json(response)) +} + +#[derive(Deserialize)] +pub struct ManyUpdateData { + pub algorithm: String, + pub hashes: Vec, + pub loaders: Vec, + pub game_versions: Vec, +} + +#[post("update")] +pub async fn update_files( + pool: web::Data, + update_data: web::Json, +) -> Result { + let hashes_parsed: Vec> = update_data + .hashes + .iter() + .map(|x| x.as_bytes().to_vec()) + .collect(); + + let mut transaction = pool.begin().await?; + + let result = sqlx::query!( + " + SELECT f.url url, h.hash hash, h.algorithm algorithm, f.version_id version_id, v.mod_id project_id FROM hashes h + INNER JOIN files f ON h.file_id = f.id + INNER JOIN versions v ON v.id = f.version_id + WHERE h.algorithm = $2 AND h.hash IN (SELECT * FROM UNNEST($1::bytea[])) + ", + hashes_parsed.as_slice(), + update_data.algorithm + ) + .fetch_all(&mut *transaction) + .await?; + + let mut version_ids = Vec::new(); + + for row in &result { + let updated_versions = database::models::Version::get_project_versions( + database::models::ProjectId(row.project_id), + Some( + update_data + .game_versions + .clone() + .iter() + .map(|x| x.0.clone()) + .collect(), + ), + Some( + update_data + .loaders + .clone() + .iter() + .map(|x| x.0.clone()) + .collect(), + ), + &**pool, + ) + .await?; + + if let Some(latest_version) = updated_versions.last() { + version_ids.push(*latest_version); + } + } + + let versions = database::models::Version::get_many_full(version_ids, &**pool).await?; + + let mut response = HashMap::new(); + + for row in &result { + if let Some(version) = versions.iter().find(|x| x.id.0 == row.version_id) { + response.insert( + row.hash.clone(), + super::versions::convert_version(version.clone()), + ); + } + } + + Ok(HttpResponse::Ok().json(response)) +} diff --git a/src/routes/versions.rs b/src/routes/versions.rs index 55ba322e..2f5669a6 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -1,15 +1,15 @@ use super::ApiError; use crate::auth::get_user_from_headers; -use crate::file_hosting::FileHost; +use crate::database; use crate::models; -use crate::models::mods::{Dependency, DependencyType}; +use crate::models::projects::{Dependency, DependencyType}; use crate::models::teams::Permissions; -use crate::{database, Pepper}; use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; +use lazy_static::lazy_static; +use regex::Regex; use serde::{Deserialize, Serialize}; use sqlx::PgPool; -use std::borrow::Borrow; -use std::sync::Arc; +use validator::Validate; #[derive(Serialize, Deserialize, Clone)] pub struct VersionListFilters { @@ -20,23 +20,18 @@ pub struct VersionListFilters { #[get("version")] pub async fn version_list( - info: web::Path<(models::ids::ModId,)>, + info: web::Path<(String,)>, web::Query(filters): web::Query, pool: web::Data, ) -> Result { - let id = info.into_inner().0.into(); + let string = info.into_inner().0; - let mod_exists = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM mods WHERE id = $1)", - id as database::models::ModId, - ) - .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .exists; + let result = database::models::Project::get_from_slug_or_project_id(string, &**pool).await?; - if mod_exists.unwrap_or(false) { - let version_ids = database::models::Version::get_mod_versions( + if let Some(project) = result { + let id = project.id; + + let version_ids = database::models::Version::get_project_versions( id, filters .game_versions @@ -48,12 +43,9 @@ pub async fn version_list( .map(|x| serde_json::from_str(x).unwrap_or_default()), &**pool, ) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; - let mut versions = database::models::Version::get_many_full(version_ids, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let mut versions = database::models::Version::get_many_full(version_ids, &**pool).await?; let mut response = versions .iter() @@ -87,8 +79,8 @@ pub async fn version_list( versions .iter() .find(|version| { - version.game_versions.contains(&filter.0) - && version.loaders.contains(&filter.1) + version.game_versions.contains(&filter.0.version) + && version.loaders.contains(&filter.1.loader) }) .map(|version| response.push(convert_version(version.clone()))) .unwrap_or(()); @@ -124,9 +116,7 @@ pub async fn versions_get( .into_iter() .map(|x| x.into()) .collect(); - let versions_data = database::models::Version::get_many_full(version_ids, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let versions_data = database::models::Version::get_many_full(version_ids, &**pool).await?; let mut versions = Vec::new(); @@ -143,9 +133,7 @@ pub async fn version_get( pool: web::Data, ) -> Result { let id = info.into_inner().0; - let version_data = database::models::Version::get_full(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let version_data = database::models::Version::get_full(id.into(), &**pool).await?; if let Some(data) = version_data { Ok(HttpResponse::Ok().json(convert_version(data))) @@ -154,12 +142,14 @@ pub async fn version_get( } } -fn convert_version(data: database::models::version_item::QueryVersion) -> models::mods::Version { - use models::mods::VersionType; +pub fn convert_version( + data: database::models::version_item::QueryVersion, +) -> models::projects::Version { + use models::projects::VersionType; - models::mods::Version { + models::projects::Version { id: data.id.into(), - mod_id: data.mod_id.into(), + project_id: data.project_id.into(), author_id: data.author_id.into(), featured: data.featured, @@ -180,7 +170,7 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models .files .into_iter() .map(|f| { - models::mods::VersionFile { + models::projects::VersionFile { url: f.url, filename: f.filename, // FIXME: Hashes are currently stored as an ascii byte slice instead @@ -206,25 +196,33 @@ fn convert_version(data: database::models::version_item::QueryVersion) -> models game_versions: data .game_versions .into_iter() - .map(models::mods::GameVersion) + .map(models::projects::GameVersion) .collect(), loaders: data .loaders .into_iter() - .map(models::mods::ModLoader) + .map(models::projects::Loader) .collect(), } } -#[derive(Serialize, Deserialize)] +lazy_static! { + static ref RE_URL_SAFE: Regex = Regex::new(r"^[a-zA-Z0-9_-]*$").unwrap(); +} + +#[derive(Serialize, Deserialize, Validate)] pub struct EditVersion { + #[validate(length(min = 3, max = 256))] pub name: Option, + #[validate(length(min = 1, max = 64), regex = "RE_URL_SAFE")] pub version_number: Option, + #[validate(length(max = 65536))] pub changelog: Option, - pub version_type: Option, + pub version_type: Option, + #[validate(length(min = 1, max = 256))] pub dependencies: Option>, - pub game_versions: Option>, - pub loaders: Option>, + pub game_versions: Option>, + pub loaders: Option>, pub featured: Option, pub primary_file: Option<(String, String)>, } @@ -238,12 +236,12 @@ pub async fn version_edit( ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; + new_version.validate()?; + let version_id = info.into_inner().0; let id = version_id.into(); - let result = database::models::Version::get_full(id, &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let result = database::models::Version::get_full(id, &**pool).await?; if let Some(version_item) = result { let team_member = database::models::TeamMember::get_from_user_id_version( @@ -269,18 +267,9 @@ pub async fn version_edit( )); } - let mut transaction = pool - .begin() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let mut transaction = pool.begin().await?; if let Some(name) = &new_version.name { - if name.len() > 256 || name.len() < 3 { - return Err(ApiError::InvalidInputError( - "The version name must be within 3-256 characters!".to_string(), - )); - } - sqlx::query!( " UPDATE versions @@ -291,17 +280,10 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } if let Some(number) = &new_version.version_number { - if number.len() > 64 || number.is_empty() { - return Err(ApiError::InvalidInputError( - "The version number must be within 1-64 characters!".to_string(), - )); - } - sqlx::query!( " UPDATE versions @@ -312,8 +294,7 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } if let Some(version_type) = &new_version.version_type { @@ -338,8 +319,7 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } if let Some(dependencies) = &new_version.dependencies { @@ -350,8 +330,7 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; for dependency in dependencies { let dependency_id: database::models::ids::VersionId = @@ -367,8 +346,7 @@ pub async fn version_edit( dependency.dependency_type.as_str() ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } } @@ -380,8 +358,7 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; for game_version in game_versions { let game_version_id = database::models::categories::GameVersion::get_id( @@ -404,8 +381,7 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } } @@ -417,8 +393,7 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; for loader in loaders { let loader_id = @@ -439,8 +414,7 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } } @@ -455,8 +429,7 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } if let Some(primary_file) = &new_version.primary_file { @@ -470,8 +443,7 @@ pub async fn version_edit( primary_file.0 ) .fetch_optional(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? + .await? .ok_or_else(|| { ApiError::InvalidInputError(format!( "Specified file with hash {} does not exist.", @@ -488,8 +460,7 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; sqlx::query!( " @@ -500,18 +471,10 @@ pub async fn version_edit( result.id, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } if let Some(body) = &new_version.changelog { - if body.len() > 65536 { - return Err(ApiError::InvalidInputError( - "The version changelog must be less than 65536 characters long!" - .to_string(), - )); - } - sqlx::query!( " UPDATE versions @@ -522,15 +485,11 @@ pub async fn version_edit( id as database::models::ids::VersionId, ) .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .await?; } - transaction - .commit() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - Ok(HttpResponse::Ok().body("")) + transaction.commit().await?; + Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthenticationError( "You do not have permission to edit this version!".to_string(), @@ -574,261 +533,10 @@ pub async fn version_delete( } } - let result = database::models::Version::remove_full(id.into(), &**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + let result = database::models::Version::remove_full(id.into(), &**pool).await?; if result.is_some() { - Ok(HttpResponse::Ok().body("")) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - -#[derive(Deserialize)] -pub struct Algorithm { - #[serde(default = "default_algorithm")] - algorithm: String, -} - -fn default_algorithm() -> String { - "sha1".into() -} - -// under /api/v1/version_file/{hash} -#[get("{version_id}")] -pub async fn get_version_from_hash( - info: web::Path<(String,)>, - pool: web::Data, - algorithm: web::Query, -) -> Result { - let hash = info.into_inner().0; - - let result = sqlx::query!( - " - SELECT f.version_id version_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - WHERE h.algorithm = $2 AND h.hash = $1 - ", - hash.as_bytes(), - algorithm.algorithm - ) - .fetch_optional(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - if let Some(id) = result { - let version_data = database::models::Version::get_full( - database::models::VersionId(id.version_id), - &**pool, - ) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - if let Some(data) = version_data { - Ok(HttpResponse::Ok().json(convert_version(data))) - } else { - Ok(HttpResponse::NotFound().body("")) - } - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - -#[derive(Serialize, Deserialize)] -pub struct DownloadRedirect { - pub url: String, -} - -// under /api/v1/version_file/{hash}/download -#[allow(clippy::await_holding_refcell_ref)] -#[get("{version_id}/download")] -pub async fn download_version( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - algorithm: web::Query, - pepper: web::Data, -) -> Result { - let hash = info.into_inner().0; - - let result = sqlx::query!( - " - SELECT f.url url, f.id id, f.version_id version_id, v.mod_id mod_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v ON v.id = f.version_id - WHERE h.algorithm = $2 AND h.hash = $1 - ", - hash.as_bytes(), - algorithm.algorithm - ) - .fetch_optional(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - if let Some(id) = result { - let real_ip = req.connection_info(); - let ip_option = real_ip.borrow().remote_addr(); - - if let Some(ip) = ip_option { - let hash = sha1::Sha1::from(format!("{}{}", ip, pepper.pepper)).hexdigest(); - - let download_exists = sqlx::query!( - "SELECT EXISTS(SELECT 1 FROM downloads WHERE version_id = $1 AND date > (CURRENT_DATE - INTERVAL '30 minutes ago') AND identifier = $2)", - id.version_id, - hash, - ) - .fetch_one(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))? - .exists.unwrap_or(false); - - if !download_exists { - sqlx::query!( - " - INSERT INTO downloads ( - version_id, identifier - ) - VALUES ( - $1, $2 - ) - ", - id.version_id, - hash - ) - .execute(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - sqlx::query!( - " - UPDATE versions - SET downloads = downloads + 1 - WHERE id = $1 - ", - id.version_id, - ) - .execute(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - sqlx::query!( - " - UPDATE mods - SET downloads = downloads + 1 - WHERE id = $1 - ", - id.mod_id, - ) - .execute(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - } - } - Ok(HttpResponse::TemporaryRedirect() - .header("Location", &*id.url) - .json(DownloadRedirect { url: id.url })) - } else { - Ok(HttpResponse::NotFound().body("")) - } -} - -// under /api/v1/version_file/{hash} -#[delete("{version_id}")] -pub async fn delete_file( - req: HttpRequest, - info: web::Path<(String,)>, - pool: web::Data, - file_host: web::Data>, - algorithm: web::Query, -) -> Result { - let user = get_user_from_headers(req.headers(), &**pool).await?; - - let hash = info.into_inner().0; - - let result = sqlx::query!( - " - SELECT f.id id, f.version_id version_id, f.filename filename, v.version_number version_number, v.mod_id mod_id FROM hashes h - INNER JOIN files f ON h.file_id = f.id - INNER JOIN versions v ON v.id = f.version_id - WHERE h.algorithm = $2 AND h.hash = $1 - ", - hash.as_bytes(), - algorithm.algorithm - ) - .fetch_optional(&**pool) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - if let Some(row) = result { - if !user.role.is_mod() { - let team_member = database::models::TeamMember::get_from_user_id_version( - database::models::ids::VersionId(row.version_id), - user.id.into(), - &**pool, - ) - .await - .map_err(ApiError::DatabaseError)? - .ok_or_else(|| { - ApiError::CustomAuthenticationError( - "You don't have permission to delete this file!".to_string(), - ) - })?; - - if !team_member - .permissions - .contains(Permissions::DELETE_VERSION) - { - return Err(ApiError::CustomAuthenticationError( - "You don't have permission to delete this file!".to_string(), - )); - } - } - - let mut transaction = pool - .begin() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - sqlx::query!( - " - DELETE FROM hashes - WHERE file_id = $1 - ", - row.id - ) - .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - sqlx::query!( - " - DELETE FROM files - WHERE files.id = $1 - ", - row.id, - ) - .execute(&mut *transaction) - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - let mod_id: models::mods::ModId = database::models::ids::ModId(row.mod_id).into(); - file_host - .delete_file_version( - "", - &format!( - "data/{}/versions/{}/{}", - mod_id, row.version_number, row.filename - ), - ) - .await?; - - transaction - .commit() - .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; - - Ok(HttpResponse::Ok().body("")) + Ok(HttpResponse::NoContent().body("")) } else { Ok(HttpResponse::NotFound().body("")) } diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index 8b872924..2a44246b 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -2,32 +2,32 @@ use futures::{StreamExt, TryStreamExt}; use log::info; use super::IndexingError; -use crate::models::mods::SideType; -use crate::search::UploadSearchMod; +use crate::models::projects::SideType; +use crate::search::UploadSearchProject; use sqlx::postgres::PgPool; use std::borrow::Cow; -// TODO: only loaders for recent versions? For mods that have moved from forge to fabric -pub async fn index_local(pool: PgPool) -> Result, IndexingError> { - info!("Indexing local mods!"); +// TODO: only loaders for recent versions? For projects that have moved from forge to fabric +pub async fn index_local(pool: PgPool) -> Result, IndexingError> { + info!("Indexing local projects!"); - let mut docs_to_add: Vec = vec![]; + let mut docs_to_add: Vec = vec![]; - let mut mods = sqlx::query!( + let mut projects = sqlx::query!( " SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.status, m.slug, m.license, m.client_side, m.server_side FROM mods m " ).fetch(&pool); - while let Some(result) = mods.next().await { - if let Ok(mod_data) = result { - let status = crate::models::mods::ModStatus::from_str( + while let Some(result) = projects.next().await { + if let Ok(project_data) = result { + let status = crate::models::projects::ProjectStatus::from_str( &sqlx::query!( " - SELECT status FROM statuses - WHERE id = $1 - ", - mod_data.status, + SELECT status FROM statuses + WHERE id = $1 + ", + project_data.status, ) .fetch_one(&pool) .await? @@ -46,7 +46,7 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE WHERE versions.mod_id = $1 ORDER BY gv.created ASC ", - mod_data.id + project_data.id ) .fetch_many(&pool) .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) @@ -60,7 +60,7 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE INNER JOIN loaders ON loaders.id = lv.loader_id WHERE versions.mod_id = $1 ", - mod_data.id + project_data.id ) .fetch_many(&pool) .try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.loader))) }) @@ -74,7 +74,7 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE INNER JOIN categories c ON mc.joining_category_id=c.id WHERE mc.joining_mod_id = $1 ", - mod_data.id + project_data.id ) .fetch_many(&pool) .try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.category))) }) @@ -90,22 +90,21 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE WHERE tm.team_id = $2 AND tm.role = $1 ", crate::models::teams::OWNER_ROLE, - mod_data.team_id, + project_data.team_id, ) .fetch_one(&pool) .await?; let mut icon_url = "".to_string(); - if let Some(url) = mod_data.icon_url { + if let Some(url) = project_data.icon_url { icon_url = url; } - let mod_id = crate::models::ids::ModId(mod_data.id as u64); - let author_id = crate::models::ids::UserId(user.id as u64); + let project_id = crate::models::ids::ProjectId(project_data.id as u64); // TODO: is this correct? This just gets the latest version of - // minecraft that this mod has a version that supports; it doesn't + // minecraft that this project has a version that supports; it doesn't // take betas or other info into account. let latest_version = versions .last() @@ -116,10 +115,10 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE let client_side = SideType::from_str( &sqlx::query!( " - SELECT name FROM side_types - WHERE id = $1 - ", - mod_data.client_side, + SELECT name FROM side_types + WHERE id = $1 + ", + project_data.client_side, ) .fetch_one(&pool) .await? @@ -129,10 +128,10 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE let server_side = SideType::from_str( &sqlx::query!( " - SELECT name FROM side_types - WHERE id = $1 - ", - mod_data.server_side, + SELECT name FROM side_types + WHERE id = $1 + ", + project_data.server_side, ) .fetch_one(&pool) .await? @@ -140,33 +139,31 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE ); let license = crate::database::models::categories::License::get( - crate::database::models::LicenseId(mod_data.license), + crate::database::models::LicenseId(project_data.license), &pool, ) .await?; - docs_to_add.push(UploadSearchMod { - mod_id: format!("local-{}", mod_id), - title: mod_data.title, - description: mod_data.description, + docs_to_add.push(UploadSearchProject { + project_id: format!("local-{}", project_id), + title: project_data.title, + description: project_data.description, categories, versions, - follows: mod_data.follows, - downloads: mod_data.downloads, - page_url: format!("https://modrinth.com/mod/{}", mod_id), + follows: project_data.follows, + downloads: project_data.downloads, icon_url, author: user.username, - author_url: format!("https://modrinth.com/user/{}", author_id), - date_created: mod_data.published, - created_timestamp: mod_data.published.timestamp(), - date_modified: mod_data.updated, - modified_timestamp: mod_data.updated.timestamp(), + date_created: project_data.published, + created_timestamp: project_data.published.timestamp(), + date_modified: project_data.updated, + modified_timestamp: project_data.updated.timestamp(), latest_version, license: license.short, client_side: client_side.to_string(), server_side: server_side.to_string(), host: Cow::Borrowed("modrinth"), - slug: mod_data.slug, + slug: project_data.slug, }); } } @@ -175,10 +172,10 @@ pub async fn index_local(pool: PgPool) -> Result, IndexingE } pub async fn query_one( - id: crate::database::models::ModId, + id: crate::database::models::ProjectId, exec: &mut sqlx::PgConnection, -) -> Result { - let mod_data = sqlx::query!( +) -> Result { + let project_data = sqlx::query!( " SELECT m.id, m.title, m.description, m.downloads, m.follows, m.icon_url, m.body_url, m.published, m.updated, m.team_id, m.slug, m.license, m.client_side, m.server_side FROM mods m @@ -195,7 +192,7 @@ pub async fn query_one( WHERE versions.mod_id = $1 ORDER BY gv.created ASC ", - mod_data.id + project_data.id ) .fetch_many(&mut *exec) .try_filter_map(|e| async { Ok(e.right().map(|c| c.version)) }) @@ -209,7 +206,7 @@ pub async fn query_one( INNER JOIN loaders ON loaders.id = lv.loader_id WHERE versions.mod_id = $1 ", - mod_data.id + project_data.id ) .fetch_many(&mut *exec) .try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.loader))) }) @@ -223,7 +220,7 @@ pub async fn query_one( INNER JOIN categories c ON mc.joining_category_id=c.id WHERE mc.joining_mod_id = $1 ", - mod_data.id + project_data.id ) .fetch_many(&mut *exec) .try_filter_map(|e| async { Ok(e.right().map(|c| Cow::Owned(c.category))) }) @@ -239,22 +236,21 @@ pub async fn query_one( WHERE tm.team_id = $2 AND tm.role = $1 ", crate::models::teams::OWNER_ROLE, - mod_data.team_id, + project_data.team_id, ) .fetch_one(&mut *exec) .await?; let mut icon_url = "".to_string(); - if let Some(url) = mod_data.icon_url { + if let Some(url) = project_data.icon_url { icon_url = url; } - let mod_id = crate::models::ids::ModId(mod_data.id as u64); - let author_id = crate::models::ids::UserId(user.id as u64); + let project_id = crate::models::ids::ProjectId(project_data.id as u64); // TODO: is this correct? This just gets the latest version of - // minecraft that this mod has a version that supports; it doesn't + // minecraft that this project has a version that supports; it doesn't // take betas or other info into account. let latest_version = versions .last() @@ -265,10 +261,10 @@ pub async fn query_one( let client_side = SideType::from_str( &sqlx::query!( " - SELECT name FROM side_types - WHERE id = $1 - ", - mod_data.client_side, + SELECT name FROM side_types + WHERE id = $1 + ", + project_data.client_side, ) .fetch_one(&mut *exec) .await? @@ -278,10 +274,10 @@ pub async fn query_one( let server_side = SideType::from_str( &sqlx::query!( " - SELECT name FROM side_types - WHERE id = $1 - ", - mod_data.server_side, + SELECT name FROM side_types + WHERE id = $1 + ", + project_data.server_side, ) .fetch_one(&mut *exec) .await? @@ -289,32 +285,30 @@ pub async fn query_one( ); let license = crate::database::models::categories::License::get( - crate::database::models::LicenseId(mod_data.license), + crate::database::models::LicenseId(project_data.license), &mut *exec, ) .await?; - Ok(UploadSearchMod { - mod_id: format!("local-{}", mod_id), - title: mod_data.title, - description: mod_data.description, + Ok(UploadSearchProject { + project_id: format!("local-{}", project_id), + title: project_data.title, + description: project_data.description, categories, versions, - follows: mod_data.follows, - downloads: mod_data.downloads, - page_url: format!("https://modrinth.com/mod/{}", mod_id), + follows: project_data.follows, + downloads: project_data.downloads, icon_url, author: user.username, - author_url: format!("https://modrinth.com/user/{}", author_id), - date_created: mod_data.published, - created_timestamp: mod_data.published.timestamp(), - date_modified: mod_data.updated, - modified_timestamp: mod_data.updated.timestamp(), + date_created: project_data.published, + created_timestamp: project_data.published.timestamp(), + date_modified: project_data.updated, + modified_timestamp: project_data.updated.timestamp(), latest_version, license: license.short, client_side: client_side.to_string(), server_side: server_side.to_string(), host: Cow::Borrowed("modrinth"), - slug: mod_data.slug, + slug: project_data.slug, }) } diff --git a/src/search/indexing/mod.rs b/src/search/indexing/mod.rs index b0646a20..f3b4daa7 100644 --- a/src/search/indexing/mod.rs +++ b/src/search/indexing/mod.rs @@ -2,7 +2,7 @@ pub mod local_import; pub mod queue; -use crate::search::{SearchConfig, UploadSearchMod}; +use crate::search::{SearchConfig, UploadSearchProject}; use local_import::index_local; use meilisearch_sdk::client::Client; use meilisearch_sdk::indexes::Index; @@ -27,14 +27,13 @@ pub enum IndexingError { EnvError(#[from] dotenv::Error), } -// The chunk size for adding mods to the indexing database. If the request size +// The chunk size for adding projects to the indexing database. If the request size // is too large (>10MiB) then the request fails with an error. This chunk size -// assumes a max average size of 1KiB per mod to avoid this cap. +// assumes a max average size of 1KiB per project to avoid this cap. const MEILISEARCH_CHUNK_SIZE: usize = 10000; #[derive(Debug)] pub struct IndexingSettings { - pub index_external: bool, pub index_local: bool, } @@ -42,31 +41,24 @@ impl IndexingSettings { #[allow(dead_code)] pub fn from_env() -> Self { let index_local = true; - let index_external = dotenv::var("INDEX_CURSEFORGE") - .ok() - .and_then(|b| b.parse::().ok()) - .unwrap_or(false); - Self { - index_external, - index_local, - } + Self { index_local } } } -pub async fn index_mods( +pub async fn index_projects( pool: PgPool, settings: IndexingSettings, config: &SearchConfig, ) -> Result<(), IndexingError> { - let mut docs_to_add: Vec = vec![]; + let mut docs_to_add: Vec = vec![]; if settings.index_local { docs_to_add.append(&mut index_local(pool.clone()).await?); } // Write Indices - add_mods(docs_to_add, config).await?; + add_projects(docs_to_add, config).await?; Ok(()) } @@ -74,12 +66,12 @@ pub async fn index_mods( pub async fn reset_indices(config: &SearchConfig) -> Result<(), IndexingError> { let client = Client::new(&*config.address, &*config.key); - client.delete_index("relevance_mods").await?; - client.delete_index("downloads_mods").await?; - client.delete_index("follows_mods").await?; - client.delete_index("alphabetically_mods").await?; - client.delete_index("updated_mods").await?; - client.delete_index("newest_mods").await?; + client.delete_index("relevance_projects").await?; + client.delete_index("downloads_projects").await?; + client.delete_index("follows_projects").await?; + client.delete_index("updated_projects").await?; + client.delete_index("newest_projects").await?; + client.delete_index("alphabetically_projects").await?; Ok(()) } @@ -87,7 +79,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr let client = Client::new(&*config.address, &*config.key); // Relevance Index - update_index(&client, "relevance_mods", { + update_index(&client, "relevance_projects", { let mut relevance_rules = default_rules(); relevance_rules.push_back("desc(downloads)".to_string()); relevance_rules.into() @@ -95,7 +87,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr .await?; // Downloads Index - update_index(&client, "downloads_mods", { + update_index(&client, "downloads_projects", { let mut downloads_rules = default_rules(); downloads_rules.push_front("desc(downloads)".to_string()); downloads_rules.into() @@ -103,7 +95,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr .await?; // Follows Index - update_index(&client, "follows_mods", { + update_index(&client, "follows_projects", { let mut follows_rules = default_rules(); follows_rules.push_front("desc(follows)".to_string()); follows_rules.into() @@ -111,15 +103,15 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr .await?; // Alphabetically Index - update_index(&client, "alphabetically_mods", { + update_index(&client, "alphabetically_projects", { let mut alphabetically_rules = default_rules(); alphabetically_rules.push_front("desc(title)".to_string()); alphabetically_rules.into() }) - .await?; + .await?; // Updated Index - update_index(&client, "updated_mods", { + update_index(&client, "updated_projects", { let mut updated_rules = default_rules(); updated_rules.push_front("desc(modified_timestamp)".to_string()); updated_rules.into() @@ -127,7 +119,7 @@ pub async fn reconfigure_indices(config: &SearchConfig) -> Result<(), IndexingEr .await?; // Created Index - update_index(&client, "newest_mods", { + update_index(&client, "newest_projects", { let mut newest_rules = default_rules(); newest_rules.push_front("desc(created_timestamp)".to_string()); newest_rules.into() @@ -147,7 +139,7 @@ async fn update_index<'a>( Err(meilisearch_sdk::errors::Error::MeiliSearchError { error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound, .. - }) => client.create_index(name, Some("mod_id")).await?, + }) => client.create_index(name, Some("project_id")).await?, Err(e) => { return Err(IndexingError::IndexDBError(e)); } @@ -171,7 +163,7 @@ async fn create_index<'a>( .. }) => { // Only create index and set settings if the index doesn't already exist - let index = client.create_index(name, Some("mod_id")).await?; + let index = client.create_index(name, Some("project_id")).await?; index .set_settings(&default_settings().with_ranking_rules(rules())) @@ -186,72 +178,72 @@ async fn create_index<'a>( } } -async fn add_to_index(index: Index<'_>, mods: &[UploadSearchMod]) -> Result<(), IndexingError> { +async fn add_to_index(index: Index<'_>, mods: &[UploadSearchProject]) -> Result<(), IndexingError> { for chunk in mods.chunks(MEILISEARCH_CHUNK_SIZE) { - index.add_documents(chunk, Some("mod_id")).await?; + index.add_documents(chunk, Some("project_id")).await?; } Ok(()) } -pub async fn add_mods( - mods: Vec, +pub async fn add_projects( + projects: Vec, config: &SearchConfig, ) -> Result<(), IndexingError> { let client = Client::new(&*config.address, &*config.key); // Relevance Index - let relevance_index = create_index(&client, "relevance_mods", || { + let relevance_index = create_index(&client, "relevance_projects", || { let mut relevance_rules = default_rules(); relevance_rules.push_back("desc(downloads)".to_string()); relevance_rules.into() }) .await?; - add_to_index(relevance_index, &mods).await?; + add_to_index(relevance_index, &projects).await?; // Downloads Index - let downloads_index = create_index(&client, "downloads_mods", || { + let downloads_index = create_index(&client, "downloads_projects", || { let mut downloads_rules = default_rules(); downloads_rules.push_front("desc(downloads)".to_string()); downloads_rules.into() }) .await?; - add_to_index(downloads_index, &mods).await?; + add_to_index(downloads_index, &projects).await?; // Follows Index - let follows_index = create_index(&client, "follows_mods", || { + let follows_index = create_index(&client, "follows_projects", || { let mut follows_rules = default_rules(); follows_rules.push_front("desc(follows)".to_string()); follows_rules.into() }) .await?; - add_to_index(follows_index, &mods).await?; + add_to_index(follows_index, &projects).await?; // Alphabetically Index - let alphabetically_index = create_index(&client, "alphabetically_mods", || { + let alphabetically_index = create_index(&client, "alphabetically_projects", || { let mut alphabetically_rules = default_rules(); alphabetically_rules.push_front("desc(title)".to_string()); alphabetically_rules.into() }) - .await?; - add_to_index(alphabetically_index, &mods).await?; + .await?; + add_to_index(alphabetically_index, &projects).await?; // Updated Index - let updated_index = create_index(&client, "updated_mods", || { + let updated_index = create_index(&client, "updated_projects", || { let mut updated_rules = default_rules(); updated_rules.push_front("desc(modified_timestamp)".to_string()); updated_rules.into() }) .await?; - add_to_index(updated_index, &mods).await?; + add_to_index(updated_index, &projects).await?; // Created Index - let newest_index = create_index(&client, "newest_mods", || { + let newest_index = create_index(&client, "newest_projects", || { let mut newest_rules = default_rules(); newest_rules.push_front("desc(created_timestamp)".to_string()); newest_rules.into() }) .await?; - add_to_index(newest_index, &mods).await?; + add_to_index(newest_index, &projects).await?; Ok(()) } @@ -271,7 +263,7 @@ fn default_rules() -> VecDeque { fn default_settings() -> Settings { let displayed_attributes = vec![ - "mod_id".to_string(), + "project_id".to_string(), "slug".to_string(), "author".to_string(), "title".to_string(), @@ -280,16 +272,13 @@ fn default_settings() -> Settings { "versions".to_string(), "downloads".to_string(), "follows".to_string(), - "page_url".to_string(), "icon_url".to_string(), - "author_url".to_string(), "date_created".to_string(), "date_modified".to_string(), "latest_version".to_string(), "license".to_string(), "client_side".to_string(), "server_side".to_string(), - "host".to_string(), ]; let searchable_attributes = vec![ @@ -325,7 +314,7 @@ fn default_settings() -> Settings { // This isn't currenly used, but I wrote it and it works, so I'm // keeping this mess in case someone needs it in the future. #[allow(dead_code)] -pub fn sort_mods(a: &str, b: &str) -> std::cmp::Ordering { +pub fn sort_projects(a: &str, b: &str) -> std::cmp::Ordering { use std::cmp::Ordering; let cmp = a.contains('.').cmp(&b.contains('.')); diff --git a/src/search/indexing/queue.rs b/src/search/indexing/queue.rs index 809e39c3..20106e24 100644 --- a/src/search/indexing/queue.rs +++ b/src/search/indexing/queue.rs @@ -1,4 +1,4 @@ -use super::{add_mods, IndexingError, UploadSearchMod}; +use super::{add_projects, IndexingError, UploadSearchProject}; use crate::search::SearchConfig; use std::sync::Mutex; @@ -7,7 +7,7 @@ pub struct CreationQueue { // and I don't think this can deadlock. This queue requires fast // writes and then a single potentially slower read/write that // empties the queue. - queue: Mutex>, + queue: Mutex>, } impl CreationQueue { @@ -17,11 +17,11 @@ impl CreationQueue { } } - pub fn add(&self, search_mod: UploadSearchMod) { + pub fn add(&self, search_project: UploadSearchProject) { // Can only panic if mutex is poisoned - self.queue.lock().unwrap().push(search_mod); + self.queue.lock().unwrap().push(search_project); } - pub fn take(&self) -> Vec { + pub fn take(&self) -> Vec { std::mem::replace(&mut *self.queue.lock().unwrap(), Vec::with_capacity(10)) } } @@ -31,5 +31,5 @@ pub async fn index_queue( config: &SearchConfig, ) -> Result<(), IndexingError> { let queue = queue.take(); - add_mods(queue, config).await + add_projects(queue, config).await } diff --git a/src/search/mod.rs b/src/search/mod.rs index af84c2ea..9a1cb6d5 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -1,5 +1,5 @@ use crate::models::error::ApiError; -use crate::models::mods::SearchRequest; +use crate::models::projects::SearchRequest; use actix_web::http::StatusCode; use actix_web::web::HttpResponse; use chrono::{DateTime, Utc}; @@ -57,11 +57,11 @@ pub struct SearchConfig { pub key: String, } -/// A mod document used for uploading mods to meilisearch's indices. +/// A project document used for uploading projects to meilisearch's indices. /// This contains some extra data that is not returned by search results. #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct UploadSearchMod { - pub mod_id: String, +pub struct UploadSearchProject { + pub project_id: String, pub slug: Option, pub author: String, pub title: String, @@ -70,17 +70,15 @@ pub struct UploadSearchMod { pub versions: Vec, pub follows: i32, pub downloads: i32, - pub page_url: String, pub icon_url: String, - pub author_url: String, pub latest_version: Cow<'static, str>, pub license: String, pub client_side: String, pub server_side: String, - /// RFC 3339 formatted creation date of the mod + /// RFC 3339 formatted creation date of the project pub date_created: DateTime, - /// Unix timestamp of the creation date of the mod + /// Unix timestamp of the creation date of the project pub created_timestamp: i64, /// RFC 3339 formatted date/time of last major modification (update) pub date_modified: DateTime, @@ -92,15 +90,15 @@ pub struct UploadSearchMod { #[derive(Serialize, Deserialize, Debug)] pub struct SearchResults { - pub hits: Vec, + pub hits: Vec, pub offset: usize, pub limit: usize, pub total_hits: usize, } #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct ResultSearchMod { - pub mod_id: String, +pub struct ResultSearchProject { + pub project_id: String, pub slug: Option, pub author: String, pub title: String, @@ -110,39 +108,34 @@ pub struct ResultSearchMod { pub versions: Vec, pub downloads: i32, pub follows: i32, - pub page_url: String, pub icon_url: String, - pub author_url: String, - /// RFC 3339 formatted creation date of the mod + /// RFC 3339 formatted creation date of the project pub date_created: String, - /// RFC 3339 formatted modification date of the mod + /// RFC 3339 formatted modification date of the project pub date_modified: String, pub latest_version: String, pub license: String, pub client_side: String, pub server_side: String, - - /// The host of the mod: Either `modrinth` or `curseforge` - pub host: String, } -impl Document for UploadSearchMod { +impl Document for UploadSearchProject { type UIDType = String; fn get_uid(&self) -> &Self::UIDType { - &self.mod_id + &self.project_id } } -impl Document for ResultSearchMod { +impl Document for ResultSearchProject { type UIDType = String; fn get_uid(&self) -> &Self::UIDType { - &self.mod_id + &self.project_id } } -pub async fn search_for_mod( +pub async fn search_for_project( info: &SearchRequest, config: &SearchConfig, ) -> Result { @@ -160,12 +153,12 @@ pub async fn search_for_mod( let limit = info.limit.as_deref().unwrap_or("10").parse()?; let index = match index { - "relevance" => "relevance_mods", - "downloads" => "downloads_mods", - "follows" => "follows_mods", - "alphabetically" => "alphabetically_mods", - "updated" => "updated_mods", - "newest" => "newest_mods", + "relevance" => "relevance_projects", + "downloads" => "downloads_projects", + "follows" => "follows_projects", + "updated" => "updated_projects", + "newest" => "newest_projects", + "alphabetically" => "alphabetically_projects", i => return Err(SearchError::InvalidIndex(i.to_string())), }; @@ -203,7 +196,7 @@ pub async fn search_for_mod( query.with_facet_filters(&why_must_you_do_this); } - let results = query.execute::().await?; + let results = query.execute::().await?; Ok(SearchResults { hits: results.hits.into_iter().map(|r| r.result).collect(), diff --git a/src/validate/fabric.rs b/src/validate/fabric.rs new file mode 100644 index 00000000..b15066c0 --- /dev/null +++ b/src/validate/fabric.rs @@ -0,0 +1,48 @@ +use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use std::io::Cursor; +use zip::ZipArchive; + +pub struct FabricValidator {} + +impl super::Validator for FabricValidator { + fn get_file_extensions<'a>(&self) -> &'a [&'a str] { + &["jar", "zip"] + } + + fn get_project_types<'a>(&self) -> &'a [&'a str] { + &["mod"] + } + + fn get_supported_loaders<'a>(&self) -> &'a [&'a str] { + &["fabric"] + } + + fn get_supported_game_versions(&self) -> SupportedGameVersions { + // Time since release of 18w49a, the first fabric version + SupportedGameVersions::PastDate(DateTime::::from_utc( + NaiveDateTime::from_timestamp(1543969469, 0), + Utc, + )) + } + + fn validate( + &self, + archive: &mut ZipArchive>, + ) -> Result { + archive.by_name("fabric.mod.json")?; + + if !archive + .file_names() + .any(|name| name.ends_with("refmap.json") || name.ends_with(".class")) + { + return Ok(ValidationResult::Warning( + "Fabric mod file is a source file!".to_string(), + )); + } + + //TODO: Check if file is a dev JAR? + + Ok(ValidationResult::Pass) + } +} diff --git a/src/validate/forge.rs b/src/validate/forge.rs new file mode 100644 index 00000000..bd036764 --- /dev/null +++ b/src/validate/forge.rs @@ -0,0 +1,86 @@ +use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; +use chrono::{DateTime, NaiveDateTime, Utc}; +use std::io::Cursor; +use zip::ZipArchive; + +pub struct ForgeValidator {} + +impl super::Validator for ForgeValidator { + fn get_file_extensions<'a>(&self) -> &'a [&'a str] { + &["jar", "zip"] + } + + fn get_project_types<'a>(&self) -> &'a [&'a str] { + &["mod"] + } + + fn get_supported_loaders<'a>(&self) -> &'a [&'a str] { + &["forge"] + } + + fn get_supported_game_versions(&self) -> SupportedGameVersions { + // Time since release of 1.13, the first forge version which uses the new TOML system + SupportedGameVersions::PastDate(DateTime::::from_utc( + NaiveDateTime::from_timestamp(1540122067, 0), + Utc, + )) + } + + fn validate( + &self, + archive: &mut ZipArchive>, + ) -> Result { + archive.by_name("META-INF/mods.toml")?; + + if !archive.file_names().any(|name| name.ends_with(".class")) { + return Ok(ValidationResult::Warning( + "Forge mod file is a source file!".to_string(), + )); + } + + //TODO: Check if file is a dev JAR? + + Ok(ValidationResult::Pass) + } +} + +pub struct LegacyForgeValidator {} + +impl super::Validator for LegacyForgeValidator { + fn get_file_extensions<'a>(&self) -> &'a [&'a str] { + &["jar", "zip"] + } + + fn get_project_types<'a>(&self) -> &'a [&'a str] { + &["mod"] + } + + fn get_supported_loaders<'a>(&self) -> &'a [&'a str] { + &["forge"] + } + + fn get_supported_game_versions(&self) -> SupportedGameVersions { + // Times between versions 1.5.2 to 1.12.2, which all use the legacy way of defining mods + SupportedGameVersions::Range( + DateTime::::from_utc(NaiveDateTime::from_timestamp(1366818300, 0), Utc), + DateTime::::from_utc(NaiveDateTime::from_timestamp(1505810340, 0), Utc), + ) + } + + fn validate( + &self, + archive: &mut ZipArchive>, + ) -> Result { + archive.by_name("mcmod.info")?; + + if !archive.file_names().any(|name| name.ends_with(".class")) { + return Ok(ValidationResult::Warning( + "Forge mod file is a source file!".to_string(), + )); + } + + //TODO: Check if file is a dev JAR? + + Ok(ValidationResult::Pass) + } +} diff --git a/src/validate/mod.rs b/src/validate/mod.rs new file mode 100644 index 00000000..9154a6aa --- /dev/null +++ b/src/validate/mod.rs @@ -0,0 +1,117 @@ +use crate::models::projects::{GameVersion, Loader}; +use crate::validate::fabric::FabricValidator; +use crate::validate::forge::{ForgeValidator, LegacyForgeValidator}; +use crate::validate::pack::PackValidator; +use chrono::{DateTime, Utc}; +use std::io::Cursor; +use thiserror::Error; +use zip::ZipArchive; + +mod fabric; +mod forge; +mod pack; + +#[derive(Error, Debug)] +pub enum ValidationError { + #[error("Unable to read Zip Archive: {0}")] + ZipError(#[from] zip::result::ZipError), + #[error("IO Error: {0}")] + IoError(#[from] std::io::Error), + #[error("Error while validating JSON: {0}")] + SerDeError(#[from] serde_json::Error), + #[error("Invalid Input: {0}")] + InvalidInputError(String), +} + +#[derive(Eq, PartialEq)] +pub enum ValidationResult { + /// File should be marked as primary + Pass, + /// File should not be marked primary, the reason for which is inside the String + Warning(String), +} + +pub enum SupportedGameVersions { + All, + PastDate(DateTime), + Range(DateTime, DateTime), + Custom(Vec), +} + +pub trait Validator: Sync { + fn get_file_extensions<'a>(&self) -> &'a [&'a str]; + fn get_project_types<'a>(&self) -> &'a [&'a str]; + fn get_supported_loaders<'a>(&self) -> &'a [&'a str]; + fn get_supported_game_versions(&self) -> SupportedGameVersions; + fn validate( + &self, + archive: &mut ZipArchive>, + ) -> Result; +} + +static VALIDATORS: [&dyn Validator; 4] = [ + &PackValidator {}, + &FabricValidator {}, + &ForgeValidator {}, + &LegacyForgeValidator {}, +]; + +/// The return value is whether this file should be marked as primary or not, based on the analysis of the file +pub fn validate_file( + data: &[u8], + file_extension: &str, + project_type: &str, + loaders: Vec, + game_versions: Vec, + all_game_versions: &[crate::database::models::categories::GameVersion], +) -> Result { + let reader = std::io::Cursor::new(data); + let mut zip = zip::ZipArchive::new(reader)?; + + for validator in &VALIDATORS { + if validator.get_file_extensions().contains(&file_extension) + && validator.get_project_types().contains(&project_type) + && loaders + .iter() + .any(|x| validator.get_supported_loaders().contains(&&*x.0)) + && game_version_supported( + &game_versions, + all_game_versions, + validator.get_supported_game_versions(), + ) + { + return validator.validate(&mut zip); + } + } + + Ok(ValidationResult::Pass) +} + +fn game_version_supported( + game_versions: &[GameVersion], + all_game_versions: &[crate::database::models::categories::GameVersion], + supported_game_versions: SupportedGameVersions, +) -> bool { + match supported_game_versions { + SupportedGameVersions::All => true, + SupportedGameVersions::PastDate(date) => game_versions.iter().any(|x| { + all_game_versions + .iter() + .find(|y| y.version == x.0) + .map(|x| x.date > date) + .unwrap_or(false) + }), + SupportedGameVersions::Range(before, after) => game_versions.iter().any(|x| { + all_game_versions + .iter() + .find(|y| y.version == x.0) + .map(|x| x.date > before && x.date < after) + .unwrap_or(false) + }), + SupportedGameVersions::Custom(versions) => { + versions.iter().any(|x| game_versions.contains(x)) + } + } +} + +//todo: fabric/forge validators for 1.8+ respectively diff --git a/src/validate/pack.rs b/src/validate/pack.rs new file mode 100644 index 00000000..9d857440 --- /dev/null +++ b/src/validate/pack.rs @@ -0,0 +1,97 @@ +use crate::models::projects::SideType; +use crate::validate::{SupportedGameVersions, ValidationError, ValidationResult}; +use serde::{Deserialize, Serialize}; +use std::io::{Cursor, Read}; +use zip::ZipArchive; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PackFormat { + pub game: String, + pub format_version: i32, + pub version_id: String, + pub name: String, + pub summary: Option, + pub dependencies: std::collections::HashMap, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PackFile { + pub path: String, + pub hashes: std::collections::HashMap, + pub env: Environment, + pub downloads: Vec, +} + +#[derive(Serialize, Deserialize)] +pub struct Environment { + pub client: SideType, + pub server: SideType, +} + +#[derive(Serialize, Deserialize, Clone, Hash, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum PackDependency { + Forge, + FabricLoader, + Minecraft, +} + +impl std::fmt::Display for PackDependency { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "{}", self.as_str()) + } +} + +impl PackDependency { + // These are constant, so this can remove unneccessary allocations (`to_string`) + pub fn as_str(&self) -> &'static str { + match self { + PackDependency::Forge => "forge", + PackDependency::FabricLoader => "fabric-loader", + PackDependency::Minecraft => "minecraft", + } + } +} + +pub struct PackValidator {} + +impl super::Validator for PackValidator { + fn get_file_extensions<'a>(&self) -> &'a [&'a str] { + &["zip"] + } + + fn get_project_types<'a>(&self) -> &'a [&'a str] { + &["modpack"] + } + + fn get_supported_loaders<'a>(&self) -> &'a [&'a str] { + &["forge", "fabric"] + } + + fn get_supported_game_versions(&self) -> SupportedGameVersions { + SupportedGameVersions::All + } + + fn validate( + &self, + archive: &mut ZipArchive>, + ) -> Result { + let mut file = archive.by_name("index.json")?; + + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + let pack: PackFormat = serde_json::from_str(&*contents)?; + + if pack.game != *"minecraft" { + return Err(ValidationError::InvalidInputError(format!( + "Game {0} does not exist!", + pack.game + ))); + } + + Ok(ValidationResult::Pass) + } +}