diff --git a/Cargo.lock b/Cargo.lock index d1ab10fe..1fccb3c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,11 +45,10 @@ dependencies = [ [[package]] name = "actix-cors" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30dbd116ef7532f56e2f6d7c511736ea0b124d914ee8820a5271247bf89f06aa" +checksum = "414360eed71ba2d5435b185ba43ecbe281dfab5df3898286d6b7be8074372c92" dependencies = [ - "actix-service", "actix-utils", "actix-web", "derive_more", @@ -61,9 +60,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.0.0" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3fdd63b9cfeaf92eeeece719dabbddddb420a57d3fd171ce1490ecfb7086b1" +checksum = "a5885cb81a0d4d0d322864bea1bb6c2a8144626b4fdc625d4c51eba197e7797a" dependencies = [ "actix-codec", "actix-rt", @@ -140,9 +139,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf3f2183be1241ed4dd22611850b85d38de0b08a09f1f7bcccbd0809084b359" +checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" dependencies = [ "actix-macros", "futures-core", @@ -151,20 +150,20 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9e7472ac180abb0a8e592b653744345983a7a14f44691c8394a799d0df4dbbf" +checksum = "0da34f8e659ea1b077bb4637948b815cd3768ad5a188fdcd74ff4d84240cd824" dependencies = [ "actix-rt", "actix-service", "actix-utils", "futures-core", "futures-util", - "log", "mio", "num_cpus", "socket2", "tokio", + "tracing", ] [[package]] @@ -224,7 +223,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "socket2", - "time 0.3.7", + "time 0.3.9", "url", ] @@ -300,9 +299,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.55" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "async-channel" @@ -317,9 +322,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.52" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2", "quote", @@ -555,7 +560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ "percent-encoding", - "time 0.3.7", + "time 0.3.9", "version_check", ] @@ -577,9 +582,9 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cpufeatures" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" dependencies = [ "libc", ] @@ -610,9 +615,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if", "crossbeam-utils", @@ -620,9 +625,9 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd435b205a4842da59efd07628f921c096bc1cc0a156835b4fa0bcb9a19bcce" +checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" dependencies = [ "cfg-if", "crossbeam-utils", @@ -630,9 +635,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" dependencies = [ "cfg-if", "lazy_static", @@ -660,9 +665,9 @@ dependencies = [ [[package]] name = "curl" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de97b894edd5b5bcceef8b78d7da9b75b1d2f2f9a910569d0bde3dd31d84939" +checksum = "37d855aeef205b43f65a5001e0997d81f8efca7badad4fad7d897aa7f0d0651f" dependencies = [ "curl-sys", "libc", @@ -675,9 +680,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.52+curl-7.81.0" +version = "0.4.53+curl-7.82.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b8c2d1023ea5fded5b7b892e4b8e95f70038a421126a056761a84246a28971" +checksum = "8092905a5a9502c312f223b2775f57ec5c5b715f9a15ee9d2a8591d1364a0352" dependencies = [ "cc", "libc", @@ -777,9 +782,9 @@ dependencies = [ [[package]] name = "dirs-sys" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", @@ -1051,18 +1056,18 @@ dependencies = [ [[package]] name = "gumdrop" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46571f5d540478cf70d2a42dd0d6d8e9f4b9cc7531544b93311e657b86568a0b" +checksum = "5bc700f989d2f6f0248546222d9b4258f5b02a171a431f8285a81c08142629e3" dependencies = [ "gumdrop_derive", ] [[package]] name = "gumdrop_derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915ef07c710d84733522461de2a734d4d62a3fd39a4d4f404c2f385ef8618d05" +checksum = "729f9bd3449d77e7831a18abfb7ba2f99ee813dfd15b8c2167c9a54ba20aa99d" dependencies = [ "proc-macro2", "quote", @@ -1071,9 +1076,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f1f717ddc7b2ba36df7e871fd88db79326551d3d6f1fc406fbfd28b582ff8e" +checksum = "62eeb471aa3e3c9197aa4bfeabfe02982f6dc96f750486c0bb0009ac58b26d2b" dependencies = [ "bytes", "fnv", @@ -1191,9 +1196,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.17" +version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" dependencies = [ "bytes", "futures-channel", @@ -1270,15 +1275,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" [[package]] name = "isahc" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d140e84730d325378912ede32d7cd53ef1542725503b3353e5ec8113c7c6f588" +checksum = "437f8808009c031df3c1d532c8fd7e3d73239dfe522ebf0b94b5e34d5d01044b" dependencies = [ "async-channel", "castaway", @@ -1301,6 +1306,15 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "iso8601-duration" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b51dd97fa24074214b9eb14da518957573f4dec3189112610ae1ccec9ac464" +dependencies = [ + "nom 5.1.2", +] + [[package]] name = "itertools" version = "0.10.3" @@ -1393,10 +1407,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] -name = "libc" -version = "0.2.119" +name = "lexical-core" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efaa7b300f3b5fe8eb6bf21ce3895e1751d9665086af2d64b42f19701015ff4f" [[package]] name = "libnghttp2-sys" @@ -1410,9 +1437,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5435b8549c16d423ed0c03dbaafe57cf6c3344744f1242520d59c9d8ecec66" +checksum = "6f35facd4a5673cb5a48822be2be1d4236c1c99cb4113cab7061ac720d5bf859" dependencies = [ "cc", "libc", @@ -1449,9 +1476,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" dependencies = [ "cfg-if", ] @@ -1492,14 +1519,19 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "meilisearch-sdk" -version = "0.6.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8972f69aef330566ece2a76e61ebb9383565b03c45aeb95dbd66ad672186497" +checksum = "4eae404a5052ee03460ad87998e00cc78e5c68ec3eb23f673f1c13007d150697" dependencies = [ + "async-trait", + "futures", "isahc", + "iso8601-duration", + "js-sys", "log", "serde", "serde_json", + "time 0.3.9", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1544,14 +1576,15 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" dependencies = [ "libc", "log", "miow", "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", "winapi", ] @@ -1566,9 +1599,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +checksum = "09bf6f32a3afefd0b587ee42ed19acd945c6d1f3b5424040f50b2f24ab16be77" dependencies = [ "lazy_static", "libc", @@ -1584,13 +1617,23 @@ dependencies = [ [[package]] name = "nom" -version = "7.1.0" +version = "5.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ "memchr", "minimal-lexical", - "version_check", ] [[package]] @@ -1633,18 +1676,18 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" dependencies = [ "libc", ] [[package]] name = "once_cell" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "opaque-debug" @@ -1751,9 +1794,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" [[package]] name = "percent-encoding" @@ -1862,9 +1905,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" dependencies = [ "proc-macro2", ] @@ -1942,28 +1985,29 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "8ae183fc1b06c149f0c1793e1eb447c8b04bfe46d48e9e48bfb8d2d7ed64ecf0" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "7776223e2696f1aa4c6b0170e83212f47296a00424305117d013dfe86fb0fe55" dependencies = [ "getrandom 0.2.5", "redox_syscall", + "thiserror", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" dependencies = [ "aho-corasick", "memchr", @@ -1987,9 +2031,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f242f1488a539a79bac6dbe7c8609ae43b7914b7736210f239a37cccb32525" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" dependencies = [ "base64", "bytes", @@ -2072,7 +2116,7 @@ dependencies = [ "serde-xml-rs", "serde_derive", "sha2", - "time 0.3.7", + "time 0.3.9", "tokio", "tokio-stream", "url", @@ -2352,7 +2396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4" dependencies = [ "itertools", - "nom", + "nom 7.1.1", "unicode_categories", ] @@ -2451,6 +2495,12 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.2" @@ -2475,9 +2525,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" +checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54" dependencies = [ "proc-macro2", "quote", @@ -2500,9 +2550,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] @@ -2539,21 +2589,22 @@ dependencies = [ [[package]] name = "time" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" dependencies = [ "itoa", "libc", "num_threads", + "serde", "time-macros", ] [[package]] name = "time-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tinyvec" @@ -2657,9 +2708,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6c650a8ef0cd2dd93736f033d21cbd1224c5a967aa0c258d00fcf7dafef9b9f" +checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" dependencies = [ "cfg-if", "log", @@ -2670,9 +2721,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" dependencies = [ "proc-macro2", "quote", @@ -2681,9 +2732,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23" +checksum = "aa31669fa42c09c34d94d8165dd2012e8ff3c66aca50f3bb226b68f216f2706c" dependencies = [ "lazy_static", ] @@ -2862,6 +2913,12 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.79" @@ -3058,9 +3115,9 @@ checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" [[package]] name = "winreg" -version = "0.7.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] diff --git a/Cargo.toml b/Cargo.toml index 465949ac..59d16ee5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.2.0" #Team members, please add your emails and usernames authors = ["geometrically ", "Redblueflame ", "Aeledfyr ", "Charalampos Fanoulis ", "AppleTheGolden "] edition = "2018" +license = "AGPL-3.0" [[bin]] name = "labrinth" @@ -19,7 +20,7 @@ tokio-stream = "0.1.8" actix-multipart = "0.4.0" actix-cors = "0.6.0" -meilisearch-sdk = "0.6.0" +meilisearch-sdk = "0.15.0" reqwest = { version = "0.11.9", features = ["json"] } yaserde = "0.6.0" diff --git a/Dockerfile b/Dockerfile index 7012c429..98701076 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.55.0 as build +FROM rust:1.59.0 as build ENV PKG_CONFIG_ALLOW_CROSS=1 WORKDIR /usr/src/labrinth diff --git a/docker-compose.yml b/docker-compose.yml index eb50a418..4b9e4a70 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: POSTGRES_PASSWORD: labrinth POSTGRES_HOST_AUTH_METHOD: trust meilisearch: - image: getmeili/meilisearch:v0.19.0 + image: getmeili/meilisearch:v0.25.0 restart: on-failure ports: - "7700:7700" diff --git a/sqlx-data.json b/sqlx-data.json index 18e95d60..912c8a16 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -163,6 +163,19 @@ "nullable": [] } }, + "06a92b638c77276f36185788748191e7731a2cce874ecca4af913d0d0412d223": { + "query": "\n UPDATE versions\n SET downloads = $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, "06c2d67bcbc95baa4b7e5865ec9adec7f068c1dfd3f859c29465b8d8a40343e0": { "query": "\n SELECT m.id FROM mods m\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.accepted = TRUE\n WHERE tm.user_id = $1\n ", "describe": { @@ -851,6 +864,19 @@ ] } }, + "33a965c7dc615d3b701c05299889357db8dd36d378850625d2602ba471af4885": { + "query": "\n UPDATE mods\n SET downloads = downloads + $1\n WHERE (id = $2)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int8" + ] + }, + "nullable": [] + } + }, "33fc96ac71cfa382991cfb153e89da1e9f43ebf5367c28b30c336b758222307b": { "query": "\n DELETE FROM loaders_versions\n WHERE loaders_versions.version_id = $1\n ", "describe": { @@ -4102,6 +4128,18 @@ "nullable": [] } }, + "bd0d1da185dc7d21ccbbfde86fc093ce9eda7dd7e07f7a53882d427010fd58ca": { + "query": "\n DELETE FROM dependencies WHERE dependent_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + } + }, "bdaab7da16d07169c29d96330fcc17ef2fb87fdfbadca23b7289c64420ac3a04": { "query": "\n SELECT id, user_id, role, permissions, accepted\n FROM team_members\n WHERE (team_id = $1 AND user_id = $2)\n ", "describe": { @@ -5127,18 +5165,6 @@ ] } }, - "e1f910af9153befd7faed7ae9db87d2886f211cb43f61b6bb093ac2bfb8a0555": { - "query": "\n DELETE FROM dependencies WHERE dependent_id = $1 AND dependency_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [] - } - }, "e29da865af4a0a110275b9756394546a3bb88bff40e18c66029651f515caed98": { "query": "\n SELECT f.id id FROM files f\n WHERE f.version_id = $1\n ", "describe": { diff --git a/src/database/models/ids.rs b/src/database/models/ids.rs index 33cfbef4..a5b2a6f3 100644 --- a/src/database/models/ids.rs +++ b/src/database/models/ids.rs @@ -28,7 +28,7 @@ macro_rules! generate_ids { retry_count += 1; if retry_count > ID_RETRY_COUNT { - return Err(DatabaseError::RandomIdError); + return Err(DatabaseError::RandomId); } } diff --git a/src/database/models/mod.rs b/src/database/models/mod.rs index e12d56ab..e6a1dc3b 100644 --- a/src/database/models/mod.rs +++ b/src/database/models/mod.rs @@ -24,16 +24,16 @@ pub use version_item::VersionFile; #[derive(Error, Debug)] pub enum DatabaseError { #[error("Error while interacting with the database: {0}")] - DatabaseError(#[from] sqlx::error::Error), + Database(#[from] sqlx::error::Error), #[error("Error while trying to generate random ID")] - RandomIdError, + RandomId, #[error( "Invalid identifier: Category/version names must contain only ASCII \ alphanumeric characters or '_-'." )] InvalidIdentifier(String), #[error("Invalid permissions bitflag!")] - BitflagError, + Bitflag, #[error("A database request failed")] Other(String), } diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index f1343dec..d9c72cea 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -124,7 +124,7 @@ impl TeamMember { accepted: m.accepted, }))) } else { - Ok(Some(Err(super::DatabaseError::BitflagError))) + Ok(Some(Err(super::DatabaseError::Bitflag))) } } else { Ok(None) @@ -186,7 +186,7 @@ impl TeamMember { }, }))) } else { - Ok(Some(Err(super::DatabaseError::BitflagError))) + Ok(Some(Err(super::DatabaseError::Bitflag))) } } else { Ok(None) @@ -234,7 +234,7 @@ impl TeamMember { accepted: m.accepted, }))) } else { - Ok(Some(Err(super::DatabaseError::BitflagError))) + Ok(Some(Err(super::DatabaseError::Bitflag))) } } else { Ok(None) @@ -282,7 +282,7 @@ impl TeamMember { accepted: m.accepted, }))) } else { - Ok(Some(Err(super::DatabaseError::BitflagError))) + Ok(Some(Err(super::DatabaseError::Bitflag))) } } else { Ok(None) @@ -326,7 +326,7 @@ impl TeamMember { user_id, role: m.role, permissions: Permissions::from_bits(m.permissions as u64) - .ok_or(super::DatabaseError::BitflagError)?, + .ok_or(super::DatabaseError::Bitflag)?, accepted: m.accepted, })) } else { @@ -362,7 +362,7 @@ impl TeamMember { user_id, role: m.role, permissions: Permissions::from_bits(m.permissions as u64) - .ok_or(super::DatabaseError::BitflagError)?, + .ok_or(super::DatabaseError::Bitflag)?, accepted: m.accepted, })) } else { @@ -510,7 +510,7 @@ impl TeamMember { user_id, role: m.role, permissions: Permissions::from_bits(m.permissions as u64) - .ok_or(super::DatabaseError::BitflagError)?, + .ok_or(super::DatabaseError::Bitflag)?, accepted: m.accepted, })) } else { @@ -546,7 +546,7 @@ impl TeamMember { user_id, role: m.role, permissions: Permissions::from_bits(m.permissions as u64) - .ok_or(super::DatabaseError::BitflagError)?, + .ok_or(super::DatabaseError::Bitflag)?, accepted: m.accepted, })) } else { diff --git a/src/database/models/version_item.rs b/src/database/models/version_item.rs index 0ccc9820..b2166fe1 100644 --- a/src/database/models/version_item.rs +++ b/src/database/models/version_item.rs @@ -460,7 +460,7 @@ impl Version { sqlx::query!( " - DELETE FROM dependencies WHERE dependent_id = $1 AND dependency_id = $1 + DELETE FROM dependencies WHERE dependent_id = $1 ", id as VersionId, ) diff --git a/src/main.rs b/src/main.rs index 6dbe0591..758d30e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,8 +31,6 @@ struct Config { #[options(no_short, help = "Skip indexing on startup")] skip_first_index: bool, - #[options(no_short, help = "Reset the settings of the indices")] - reconfigure_indices: bool, #[options(no_short, help = "Reset the documents in the indices")] reset_indices: bool, @@ -79,12 +77,6 @@ async fn main() -> std::io::Result<()> { .await .unwrap(); return Ok(()); - } else if config.reconfigure_indices { - info!("Reconfiguring indices"); - search::indexing::reconfigure_indices(&search_config) - .await - .unwrap(); - return Ok(()); } // Allow manually skipping the initial indexing for quicker iteration @@ -252,18 +244,18 @@ async fn main() -> std::io::Result<()> { if let Some(header) = req.headers().get("CF-Connecting-IP") { - header.to_str().map_err(|_| { - ARError::IdentificationError - })? + header + .to_str() + .map_err(|_| ARError::Identification)? } else { connection_info .peer_addr() - .ok_or(ARError::IdentificationError)? + .ok_or(ARError::Identification)? } } else { connection_info .peer_addr() - .ok_or(ARError::IdentificationError)? + .ok_or(ARError::Identification)? }, ); diff --git a/src/models/projects.rs b/src/models/projects.rs index db374a9d..30660da0 100644 --- a/src/models/projects.rs +++ b/src/models/projects.rs @@ -475,13 +475,14 @@ pub struct Loader(pub String); #[derive(Serialize, Deserialize)] pub struct SearchRequest { pub query: Option, - /// Must match a json 2 deep array of strings `[["categories:misc"]]` - // TODO: We may want to have a better representation of this, so that - // we are less likely to break backwards compatibility - pub facets: Option, - pub filters: Option, - pub version: Option, pub offset: Option, pub index: Option, pub limit: Option, + + pub new_filters: Option, + + // Deprecated values below. WILL BE REMOVED V3! + pub facets: Option, + pub filters: Option, + pub version: Option, } diff --git a/src/ratelimit/errors.rs b/src/ratelimit/errors.rs index 5afd33fe..ab9ffd01 100644 --- a/src/ratelimit/errors.rs +++ b/src/ratelimit/errors.rs @@ -1,4 +1,5 @@ //! Errors that can occur during middleware processing stage +use crate::models::error::ApiError; use actix_web::ResponseError; use log::*; use thiserror::Error; @@ -11,14 +12,14 @@ use thiserror::Error; pub enum ARError { /// Read/Write error on store #[error("read/write operatiion failed: {0}")] - ReadWriteError(String), + ReadWrite(String), /// Identifier error #[error("client identification failed")] - IdentificationError, + Identification, /// Limited Error #[error("You are being ratelimited. Please wait {reset} seconds. {remaining}/{max_requests} remaining.")] - LimitedError { + Limited { max_requests: usize, remaining: usize, reset: u64, @@ -28,7 +29,7 @@ pub enum ARError { impl ResponseError for ARError { fn error_response(&self) -> actix_web::HttpResponse { match self { - Self::LimitedError { + Self::Limited { max_requests, remaining, reset, @@ -44,10 +45,17 @@ impl ResponseError for ARError { )); response .insert_header(("x-ratelimit-reset", reset.to_string())); - response.body(self.to_string()) + response.json(ApiError { + error: "ratelimit_error", + description: &self.to_string(), + }) } - _ => actix_web::HttpResponse::build(self.status_code()) - .body(self.to_string()), + _ => actix_web::HttpResponse::build(self.status_code()).json( + ApiError { + error: "ratelimit_error", + description: &self.to_string(), + }, + ), } } } diff --git a/src/ratelimit/memory.rs b/src/ratelimit/memory.rs index 49c63985..6df93b07 100644 --- a/src/ratelimit/memory.rs +++ b/src/ratelimit/memory.rs @@ -107,13 +107,11 @@ impl Handler for MemoryStoreActor { let new_val = val_mut.0; ActorResponse::Update(Box::pin(future::ready(Ok(new_val)))) } - None => { - return ActorResponse::Update(Box::pin(future::ready(Err( - ARError::ReadWriteError( - "memory store: read failed!".to_string(), - ), - )))) - } + None => ActorResponse::Update(Box::pin(future::ready(Err( + ARError::ReadWrite( + "memory store: read failed!".to_string(), + ), + )))), }, ActorMessage::Get(key) => { if self.inner.contains_key(&key) { @@ -121,7 +119,7 @@ impl Handler for MemoryStoreActor { Some(c) => c, None => { return ActorResponse::Get(Box::pin(future::ready( - Err(ARError::ReadWriteError( + Err(ARError::ReadWrite( "memory store: read failed!".to_string(), )), ))) @@ -138,7 +136,7 @@ impl Handler for MemoryStoreActor { Some(d) => d, None => { return ActorResponse::Expire(Box::pin(future::ready( - Err(ARError::ReadWriteError( + Err(ARError::ReadWrite( "memory store: read failed!".to_string(), )), ))) @@ -156,7 +154,7 @@ impl Handler for MemoryStoreActor { Some(c) => c, None => { return ActorResponse::Remove(Box::pin(future::ready( - Err(ARError::ReadWriteError( + Err(ARError::ReadWrite( "memory store: remove failed!".to_string(), )), ))) diff --git a/src/ratelimit/middleware.rs b/src/ratelimit/middleware.rs index 5a05b293..f70fab2c 100644 --- a/src/ratelimit/middleware.rs +++ b/src/ratelimit/middleware.rs @@ -1,4 +1,3 @@ -//! RateLimiter middleware for actix application use crate::ratelimit::errors::ARError; use crate::ratelimit::{ActorMessage, ActorResponse}; use actix::dev::*; @@ -19,28 +18,9 @@ use std::{ time::Duration, }; -/// Type that implements the ratelimit middleware. -/// -/// This accepts _interval_ which specifies the -/// window size, _max_requests_ which specifies the maximum number of requests in that window, and -/// _store_ which is essentially a data store used to store client access information. Entry is removed from -/// the store after _interval_. -/// -/// # Example -/// ```rust -/// # use std::time::Duration; -/// use actix_ratelimit::{MemoryStore, MemoryStoreActor}; -/// use actix_ratelimit::RateLimiter; -/// -/// #[actix_rt::main] -/// async fn main() { -/// let store = MemoryStore::new(); -/// let ratelimiter = RateLimiter::new( -/// MemoryStoreActor::from(store.clone()).start()) -/// .with_interval(Duration::from_secs(60)) -/// .with_max_requests(100); -/// } -/// ``` +type RateLimiterIdentifier = + Rc Result + 'static>>; + pub struct RateLimiter where T: Handler + Send + Sync + 'static, @@ -49,7 +29,7 @@ where interval: Duration, max_requests: usize, store: Addr, - identifier: Rc Result>>, + identifier: RateLimiterIdentifier, ignore_ips: Vec, } @@ -62,9 +42,8 @@ where pub fn new(store: Addr) -> Self { let identifier = |req: &ServiceRequest| { let connection_info = req.connection_info(); - let ip = connection_info - .peer_addr() - .ok_or(ARError::IdentificationError)?; + let ip = + connection_info.peer_addr().ok_or(ARError::Identification)?; Ok(String::from(ip)) }; RateLimiter { @@ -144,8 +123,7 @@ where // Exists here for the sole purpose of knowing the max_requests and interval from RateLimiter max_requests: usize, interval: u64, - identifier: - Rc Result + 'static>>, + identifier: RateLimiterIdentifier, ignore_ips: Vec, } @@ -187,7 +165,7 @@ where let remaining: ActorResponse = store .send(ActorMessage::Get(String::from(&identifier))) .await - .map_err(|_| ARError::IdentificationError)?; + .map_err(|_| ARError::Identification)?; match remaining { ActorResponse::Get(opt) => { let opt = opt.await?; @@ -199,7 +177,7 @@ where ))) .await .map_err(|_| { - ARError::ReadWriteError( + ARError::ReadWrite( "Setting timeout".to_string(), ) })?; @@ -209,7 +187,7 @@ where }; if c == 0 { info!("Limit exceeded for client: {}", &identifier); - Err(ARError::LimitedError { + Err(ARError::Limited { max_requests, remaining: c, reset: reset.as_secs(), @@ -224,7 +202,7 @@ where }) .await .map_err(|_| { - ARError::ReadWriteError( + ARError::ReadWrite( "Decrementing ratelimit".to_string(), ) })?; @@ -270,7 +248,7 @@ where }) .await .map_err(|_| { - ARError::ReadWriteError( + ARError::ReadWrite( "Creating store entry".to_string(), ) })?; diff --git a/src/routes/auth.rs b/src/routes/auth.rs index baa85be0..6f84cefe 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -19,61 +19,51 @@ pub fn config(cfg: &mut ServiceConfig) { #[derive(Error, Debug)] pub enum AuthorizationError { #[error("Environment Error")] - EnvError(#[from] dotenv::Error), + Env(#[from] dotenv::Error), #[error("An unknown database error occured: {0}")] - SqlxDatabaseError(#[from] sqlx::Error), + SqlxDatabase(#[from] sqlx::Error), #[error("Database Error: {0}")] - DatabaseError(#[from] crate::database::models::DatabaseError), + Database(#[from] crate::database::models::DatabaseError), #[error("Error while parsing JSON: {0}")] - SerDeError(#[from] serde_json::Error), + SerDe(#[from] serde_json::Error), #[error("Error while communicating to GitHub OAuth2")] - GithubError(#[from] reqwest::Error), + Github(#[from] reqwest::Error), #[error("Invalid Authentication credentials")] - InvalidCredentialsError, + InvalidCredentials, #[error("Authentication Error: {0}")] - AuthenticationError(#[from] crate::util::auth::AuthenticationError), + Authentication(#[from] crate::util::auth::AuthenticationError), #[error("Error while decoding Base62")] - DecodingError(#[from] DecodingError), + Decoding(#[from] DecodingError), } impl actix_web::ResponseError for AuthorizationError { fn status_code(&self) -> StatusCode { match self { - AuthorizationError::EnvError(..) => { + AuthorizationError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR, + AuthorizationError::SqlxDatabase(..) => { StatusCode::INTERNAL_SERVER_ERROR } - AuthorizationError::SqlxDatabaseError(..) => { + AuthorizationError::Database(..) => { StatusCode::INTERNAL_SERVER_ERROR } - AuthorizationError::DatabaseError(..) => { - StatusCode::INTERNAL_SERVER_ERROR - } - AuthorizationError::SerDeError(..) => StatusCode::BAD_REQUEST, - AuthorizationError::GithubError(..) => { - StatusCode::FAILED_DEPENDENCY - } - AuthorizationError::InvalidCredentialsError => { - StatusCode::UNAUTHORIZED - } - AuthorizationError::DecodingError(..) => StatusCode::BAD_REQUEST, - AuthorizationError::AuthenticationError(..) => { - StatusCode::UNAUTHORIZED - } + AuthorizationError::SerDe(..) => StatusCode::BAD_REQUEST, + AuthorizationError::Github(..) => StatusCode::FAILED_DEPENDENCY, + AuthorizationError::InvalidCredentials => StatusCode::UNAUTHORIZED, + AuthorizationError::Decoding(..) => StatusCode::BAD_REQUEST, + AuthorizationError::Authentication(..) => StatusCode::UNAUTHORIZED, } } fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()).json(ApiError { error: match self { - AuthorizationError::EnvError(..) => "environment_error", - AuthorizationError::SqlxDatabaseError(..) => "database_error", - AuthorizationError::DatabaseError(..) => "database_error", - AuthorizationError::SerDeError(..) => "invalid_input", - AuthorizationError::GithubError(..) => "github_error", - AuthorizationError::InvalidCredentialsError => { - "invalid_credentials" - } - AuthorizationError::DecodingError(..) => "decoding_error", - AuthorizationError::AuthenticationError(..) => { + AuthorizationError::Env(..) => "environment_error", + AuthorizationError::SqlxDatabase(..) => "database_error", + AuthorizationError::Database(..) => "database_error", + AuthorizationError::SerDe(..) => "invalid_input", + AuthorizationError::Github(..) => "github_error", + AuthorizationError::InvalidCredentials => "invalid_credentials", + AuthorizationError::Decoding(..) => "decoding_error", + AuthorizationError::Authentication(..) => { "authentication_error" } }, @@ -159,7 +149,7 @@ pub async fn auth_callback( let duration = result.expires.signed_duration_since(now); if duration.num_seconds() < 0 { - return Err(AuthorizationError::InvalidCredentialsError); + return Err(AuthorizationError::InvalidCredentials); } sqlx::query!( @@ -256,6 +246,6 @@ pub async fn auth_callback( .append_header(("Location", &*redirect_url)) .json(AuthorizationInit { url: redirect_url })) } else { - Err(AuthorizationError::InvalidCredentialsError) + Err(AuthorizationError::InvalidCredentials) } } diff --git a/src/routes/maven.rs b/src/routes/maven.rs index a04a482f..6d31753d 100644 --- a/src/routes/maven.rs +++ b/src/routes/maven.rs @@ -114,7 +114,7 @@ pub async fn maven_metadata( Ok(HttpResponse::Ok() .content_type("text/xml") - .body(yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?)) + .body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?)) } fn find_file<'a>( @@ -211,9 +211,9 @@ pub async fn version_file( name: project.inner.title, description: project.inner.description, }; - return Ok(HttpResponse::Ok().content_type("text/xml").body( - yaserde::ser::to_string(&respdata).map_err(ApiError::XmlError)?, - )); + return Ok(HttpResponse::Ok() + .content_type("text/xml") + .body(yaserde::ser::to_string(&respdata).map_err(ApiError::Xml)?)); } else if let Some(selected_file) = find_file(&project_id, &project, &version, &file) { diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 425b24ff..5ac9128d 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -159,66 +159,66 @@ pub fn reports_config(cfg: &mut web::ServiceConfig) { #[derive(thiserror::Error, Debug)] pub enum ApiError { #[error("Environment Error")] - EnvError(#[from] dotenv::Error), + Env(#[from] dotenv::Error), #[error("Error while uploading file")] - FileHostingError(#[from] FileHostingError), + FileHosting(#[from] FileHostingError), #[error("Database Error: {0}")] - DatabaseError(#[from] crate::database::models::DatabaseError), + Database(#[from] crate::database::models::DatabaseError), #[error("Database Error: {0}")] - SqlxDatabaseError(#[from] sqlx::Error), + SqlxDatabase(#[from] sqlx::Error), #[error("Internal server error: {0}")] - XmlError(String), + Xml(String), #[error("Deserialization error: {0}")] - JsonError(#[from] serde_json::Error), + Json(#[from] serde_json::Error), #[error("Authentication Error: {0}")] - AuthenticationError(#[from] crate::util::auth::AuthenticationError), + Authentication(#[from] crate::util::auth::AuthenticationError), #[error("Authentication Error: {0}")] - CustomAuthenticationError(String), + CustomAuthentication(String), #[error("Invalid Input: {0}")] - InvalidInputError(String), + InvalidInput(String), #[error("Error while validating input: {0}")] - ValidationError(String), + Validation(String), #[error("Search Error: {0}")] - SearchError(#[from] meilisearch_sdk::errors::Error), + Search(#[from] meilisearch_sdk::errors::Error), #[error("Indexing Error: {0}")] - IndexingError(#[from] crate::search::indexing::IndexingError), + Indexing(#[from] crate::search::indexing::IndexingError), } impl actix_web::ResponseError for ApiError { fn status_code(&self) -> actix_web::http::StatusCode { match self { - ApiError::EnvError(..) => { + ApiError::Env(..) => { actix_web::http::StatusCode::INTERNAL_SERVER_ERROR } - ApiError::DatabaseError(..) => { + ApiError::Database(..) => { actix_web::http::StatusCode::INTERNAL_SERVER_ERROR } - ApiError::SqlxDatabaseError(..) => { + ApiError::SqlxDatabase(..) => { actix_web::http::StatusCode::INTERNAL_SERVER_ERROR } - ApiError::AuthenticationError(..) => { + ApiError::Authentication(..) => { actix_web::http::StatusCode::UNAUTHORIZED } - ApiError::CustomAuthenticationError(..) => { + ApiError::CustomAuthentication(..) => { actix_web::http::StatusCode::UNAUTHORIZED } - ApiError::XmlError(..) => { + ApiError::Xml(..) => { actix_web::http::StatusCode::INTERNAL_SERVER_ERROR } - ApiError::JsonError(..) => actix_web::http::StatusCode::BAD_REQUEST, - ApiError::SearchError(..) => { + ApiError::Json(..) => actix_web::http::StatusCode::BAD_REQUEST, + ApiError::Search(..) => { actix_web::http::StatusCode::INTERNAL_SERVER_ERROR } - ApiError::IndexingError(..) => { + ApiError::Indexing(..) => { actix_web::http::StatusCode::INTERNAL_SERVER_ERROR } - ApiError::FileHostingError(..) => { + ApiError::FileHosting(..) => { actix_web::http::StatusCode::INTERNAL_SERVER_ERROR } - ApiError::InvalidInputError(..) => { + ApiError::InvalidInput(..) => { actix_web::http::StatusCode::BAD_REQUEST } - ApiError::ValidationError(..) => { + ApiError::Validation(..) => { actix_web::http::StatusCode::BAD_REQUEST } } @@ -228,18 +228,18 @@ impl actix_web::ResponseError for ApiError { actix_web::HttpResponse::build(self.status_code()).json( 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", - ApiError::XmlError(..) => "xml_error", - ApiError::JsonError(..) => "json_error", - ApiError::SearchError(..) => "search_error", - ApiError::IndexingError(..) => "indexing_error", - ApiError::FileHostingError(..) => "file_hosting_error", - ApiError::InvalidInputError(..) => "invalid_input", - ApiError::ValidationError(..) => "invalid_input", + ApiError::Env(..) => "environment_error", + ApiError::SqlxDatabase(..) => "database_error", + ApiError::Database(..) => "database_error", + ApiError::Authentication(..) => "unauthorized", + ApiError::CustomAuthentication(..) => "unauthorized", + ApiError::Xml(..) => "xml_error", + ApiError::Json(..) => "json_error", + ApiError::Search(..) => "search_error", + ApiError::Indexing(..) => "indexing_error", + ApiError::FileHosting(..) => "file_hosting_error", + ApiError::InvalidInput(..) => "invalid_input", + ApiError::Validation(..) => "invalid_input", }, description: &self.to_string(), }, diff --git a/src/routes/notifications.rs b/src/routes/notifications.rs index d0382467..69de2357 100644 --- a/src/routes/notifications.rs +++ b/src/routes/notifications.rs @@ -105,7 +105,7 @@ pub async fn notification_delete( Ok(HttpResponse::NoContent().body("")) } else { - Err(ApiError::CustomAuthenticationError( + Err(ApiError::CustomAuthentication( "You are not authorized to delete this notification!" .to_string(), )) diff --git a/src/routes/project_creation.rs b/src/routes/project_creation.rs index 39cfd0bc..3371c117 100644 --- a/src/routes/project_creation.rs +++ b/src/routes/project_creation.rs @@ -295,7 +295,7 @@ Get logged in user pub async fn project_create_inner( req: HttpRequest, mut payload: Multipart, - mut transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, file_host: &dyn FileHost, uploaded_files: &mut Vec, ) -> Result { @@ -535,7 +535,7 @@ pub async fn project_create_inner( all_game_versions.clone(), version_data.primary_file.is_some(), version_data.primary_file.as_deref() == Some(name), - &mut transaction, + transaction, ) .await?; } diff --git a/src/routes/projects.rs b/src/routes/projects.rs index 84a6d5a7..32e7f52c 100644 --- a/src/routes/projects.rs +++ b/src/routes/projects.rs @@ -102,9 +102,10 @@ pub async fn dependency_list( ) -> Result { let string = info.into_inner().0; - let result = - database::models::Project::get_full_from_slug_or_project_id(&string, &**pool) - .await?; + let result = 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(); @@ -152,7 +153,7 @@ pub async fn dependency_list( database::Project::get_many_full( dependencies .iter() - .map(|x| if x.0.is_none() { + .filter_map(|x| if x.0.is_none() { if let Some(mod_dependency_id) = x.2 { Some(mod_dependency_id) } else { @@ -161,12 +162,11 @@ pub async fn dependency_list( } else { x.1 }) - .flatten() .collect(), &**pool, ), database::Version::get_many_full( - dependencies.iter().map(|x| x.0).flatten().collect(), + dependencies.iter().filter_map(|x| x.0).collect(), &**pool, ) ); @@ -282,7 +282,7 @@ pub async fn project_edit( let user = get_user_from_headers(req.headers(), &**pool).await?; new_project.validate().map_err(|err| { - ApiError::ValidationError(validation_errors_to_string(err, None)) + ApiError::Validation(validation_errors_to_string(err, None)) })?; let string = info.into_inner().0; @@ -315,7 +315,7 @@ pub async fn project_edit( if let Some(title) = &new_project.title { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the title of this project!" .to_string(), )); @@ -336,7 +336,7 @@ pub async fn project_edit( if let Some(description) = &new_project.description { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the description of this project!" .to_string(), )); @@ -357,7 +357,7 @@ pub async fn project_edit( if let Some(status) = &new_project.status { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the status of this project!" .to_string(), )); @@ -367,7 +367,7 @@ pub async fn project_edit( || status == &ProjectStatus::Approved) && !user.role.is_mod() { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to set this status" .to_string(), )); @@ -375,7 +375,7 @@ pub async fn project_edit( if status == &ProjectStatus::Processing { if project_item.versions.is_empty() { - return Err(ApiError::InvalidInputError(String::from( + return Err(ApiError::InvalidInput(String::from( "Project submitted for review with no initial versions", ))); } @@ -420,7 +420,7 @@ pub async fn project_edit( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "No database entry for status provided.".to_string(), ) })?; @@ -457,7 +457,7 @@ pub async fn project_edit( if let Some(categories) = &new_project.categories { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the categories of this project!" .to_string(), )); @@ -481,7 +481,7 @@ pub async fn project_edit( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError(format!( + ApiError::InvalidInput(format!( "Category {} does not exist.", category.clone() )) @@ -502,7 +502,7 @@ pub async fn project_edit( if let Some(issues_url) = &new_project.issues_url { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the issues URL of this project!" .to_string(), )); @@ -523,7 +523,7 @@ pub async fn project_edit( if let Some(source_url) = &new_project.source_url { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the source URL of this project!" .to_string(), )); @@ -544,7 +544,7 @@ pub async fn project_edit( if let Some(wiki_url) = &new_project.wiki_url { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the wiki URL of this project!" .to_string(), )); @@ -565,7 +565,7 @@ pub async fn project_edit( if let Some(license_url) = &new_project.license_url { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the license URL of this project!" .to_string(), )); @@ -586,7 +586,7 @@ pub async fn project_edit( if let Some(discord_url) = &new_project.discord_url { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the discord URL of this project!" .to_string(), )); @@ -607,7 +607,7 @@ pub async fn project_edit( if let Some(slug) = &new_project.slug { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the slug of this project!" .to_string(), )); @@ -629,7 +629,7 @@ pub async fn project_edit( .await?; if results.exists.unwrap_or(true) { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "Slug collides with other project's id!" .to_string(), )); @@ -652,7 +652,7 @@ pub async fn project_edit( if let Some(new_side) = &new_project.client_side { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the side type of this mod!" .to_string(), )); @@ -680,7 +680,7 @@ pub async fn project_edit( if let Some(new_side) = &new_project.server_side { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the side type of this project!" .to_string(), )); @@ -708,7 +708,7 @@ pub async fn project_edit( if let Some(license) = &new_project.license_id { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the license of this project!" .to_string(), )); @@ -736,7 +736,7 @@ pub async fn project_edit( if let Some(donations) = &new_project.donation_urls { if !perms.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the donation links of this project!" .to_string(), )); @@ -760,7 +760,7 @@ pub async fn project_edit( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError(format!( + ApiError::InvalidInput(format!( "Platform {} does not exist.", donation.id.clone() )) @@ -784,7 +784,7 @@ pub async fn project_edit( if !user.role.is_mod() && project_item.status != ProjectStatus::Approved { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the moderation message of this project!" .to_string(), )); @@ -809,7 +809,7 @@ pub async fn project_edit( if !user.role.is_mod() && project_item.status != ProjectStatus::Approved { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the moderation message body of this project!" .to_string(), )); @@ -830,7 +830,7 @@ pub async fn project_edit( if let Some(body) = &new_project.body { if !perms.contains(Permissions::EDIT_BODY) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the body of this project!" .to_string(), )); @@ -852,7 +852,7 @@ pub async fn project_edit( transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { - Err(ApiError::CustomAuthenticationError( + Err(ApiError::CustomAuthentication( "You do not have permission to edit this project!".to_string(), )) } @@ -889,7 +889,7 @@ pub async fn project_icon_edit( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; @@ -901,15 +901,15 @@ pub async fn project_icon_edit( &**pool, ) .await - .map_err(ApiError::DatabaseError)? + .map_err(ApiError::Database)? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to edit this project's icon." .to_string(), )); @@ -958,7 +958,7 @@ pub async fn project_icon_edit( Ok(HttpResponse::NoContent().body("")) } else { - Err(ApiError::InvalidInputError(format!( + Err(ApiError::InvalidInput(format!( "Invalid format for project icon: {}", ext.ext ))) @@ -981,7 +981,7 @@ pub async fn delete_project_icon( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; @@ -993,15 +993,15 @@ pub async fn delete_project_icon( &**pool, ) .await - .map_err(ApiError::DatabaseError)? + .map_err(ApiError::Database)? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to edit this project's icon." .to_string(), )); @@ -1057,7 +1057,7 @@ pub async fn add_gallery_item( crate::util::ext::get_image_content_type(&*ext.ext) { item.validate().map_err(|err| { - ApiError::ValidationError(validation_errors_to_string(err, None)) + ApiError::Validation(validation_errors_to_string(err, None)) })?; let cdn_url = dotenv::var("CDN_URL")?; @@ -1071,7 +1071,7 @@ pub async fn add_gallery_item( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; @@ -1083,15 +1083,15 @@ pub async fn add_gallery_item( &**pool, ) .await - .map_err(ApiError::DatabaseError)? + .map_err(ApiError::Database)? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to edit this project's gallery." .to_string(), )); @@ -1143,7 +1143,7 @@ pub async fn add_gallery_item( Ok(HttpResponse::NoContent().body("")) } else { - Err(ApiError::InvalidInputError(format!( + Err(ApiError::InvalidInput(format!( "Invalid format for gallery image: {}", ext.ext ))) @@ -1182,7 +1182,7 @@ pub async fn edit_gallery_item( let string = info.into_inner().0; item.validate().map_err(|err| { - ApiError::ValidationError(validation_errors_to_string(err, None)) + ApiError::Validation(validation_errors_to_string(err, None)) })?; let project_item = database::models::Project::get_from_slug_or_project_id( @@ -1191,7 +1191,7 @@ pub async fn edit_gallery_item( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; @@ -1203,15 +1203,15 @@ pub async fn edit_gallery_item( &**pool, ) .await - .map_err(ApiError::DatabaseError)? + .map_err(ApiError::Database)? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to edit this project's gallery." .to_string(), )); @@ -1229,7 +1229,7 @@ pub async fn edit_gallery_item( .fetch_optional(&mut *transaction) .await? .ok_or_else(|| { - ApiError::InvalidInputError(format!( + ApiError::InvalidInput(format!( "Gallery item at URL {} is not part of the project's gallery.", item.url )) @@ -1319,7 +1319,7 @@ pub async fn delete_gallery_item( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; @@ -1331,15 +1331,15 @@ pub async fn delete_gallery_item( &**pool, ) .await - .map_err(ApiError::DatabaseError)? + .map_err(ApiError::Database)? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; if !team_member.permissions.contains(Permissions::EDIT_DETAILS) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to edit this project's gallery." .to_string(), )); @@ -1357,7 +1357,7 @@ pub async fn delete_gallery_item( .fetch_optional(&mut *transaction) .await? .ok_or_else(|| { - ApiError::InvalidInputError(format!( + ApiError::InvalidInput(format!( "Gallery item at URL {} is not part of the project's gallery.", item.url )) @@ -1403,7 +1403,7 @@ pub async fn project_delete( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; @@ -1416,9 +1416,9 @@ pub async fn project_delete( &**pool, ) .await - .map_err(ApiError::DatabaseError)? + .map_err(ApiError::Database)? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; @@ -1427,7 +1427,7 @@ pub async fn project_delete( .permissions .contains(Permissions::DELETE_PROJECT) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to delete this project!".to_string(), )); } @@ -1463,7 +1463,7 @@ pub async fn project_follow( database::models::Project::get_from_slug_or_project_id(string, &**pool) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; @@ -1512,7 +1512,7 @@ pub async fn project_follow( Ok(HttpResponse::NoContent().body("")) } else { - Err(ApiError::InvalidInputError( + Err(ApiError::InvalidInput( "You are already following this project!".to_string(), )) } @@ -1531,7 +1531,7 @@ pub async fn project_unfollow( database::models::Project::get_from_slug_or_project_id(string, &**pool) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The specified project does not exist!".to_string(), ) })?; @@ -1580,7 +1580,7 @@ pub async fn project_unfollow( Ok(HttpResponse::NoContent().body("")) } else { - Err(ApiError::InvalidInputError( + Err(ApiError::InvalidInput( "You are not following this project!".to_string(), )) } diff --git a/src/routes/reports.rs b/src/routes/reports.rs index 6f884459..847a802f 100644 --- a/src/routes/reports.rs +++ b/src/routes/reports.rs @@ -31,7 +31,7 @@ pub async fn report_create( let mut bytes = web::BytesMut::new(); while let Some(item) = body.next().await { bytes.extend_from_slice(&item.map_err(|_| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "Error while parsing request payload!".to_string(), ) })?); @@ -46,7 +46,7 @@ pub async fn report_create( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError(format!( + ApiError::InvalidInput(format!( "Invalid report type: {}", new_report.report_type )) @@ -91,7 +91,7 @@ pub async fn report_create( ) } ItemType::Unknown => { - return Err(ApiError::InvalidInputError(format!( + return Err(ApiError::InvalidInput(format!( "Invalid report item type: {}", new_report.item_type.as_str() ))) diff --git a/src/routes/tags.rs b/src/routes/tags.rs index b1319e16..4f43be01 100644 --- a/src/routes/tags.rs +++ b/src/routes/tags.rs @@ -74,7 +74,7 @@ pub async fn category_create( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "Specified project type does not exist!".to_string(), ) })?; diff --git a/src/routes/teams.rs b/src/routes/teams.rs index 3ca335a5..793e7997 100644 --- a/src/routes/teams.rs +++ b/src/routes/teams.rs @@ -38,7 +38,7 @@ pub async fn team_members_get_project( &**pool, ) .await - .map_err(ApiError::DatabaseError)?; + .map_err(ApiError::Database)?; if team_member.is_some() { let team_members: Vec<_> = members_data @@ -80,7 +80,7 @@ pub async fn team_members_get( let team_member = TeamMember::get_from_user_id(id.into(), user.id.into(), &**pool) .await - .map_err(ApiError::DatabaseError)?; + .map_err(ApiError::Database)?; if team_member.is_some() { let team_members: Vec<_> = members_data @@ -119,7 +119,7 @@ pub async fn join_team( if let Some(member) = member { if member.accepted { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "You are already a member of this team".to_string(), )); } @@ -138,7 +138,7 @@ pub async fn join_team( transaction.commit().await?; } else { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "There is no pending request from this team".to_string(), )); } @@ -175,26 +175,26 @@ pub async fn add_team_member( TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool) .await? .ok_or_else(|| { - ApiError::CustomAuthenticationError( + ApiError::CustomAuthentication( "You don't have permission to edit members of this team" .to_string(), ) })?; if !member.permissions.contains(Permissions::MANAGE_INVITES) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to invite users to this team" .to_string(), )); } if !member.permissions.contains(new_member.permissions) { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "The new member has permissions that you don't have".to_string(), )); } if new_member.role == crate::models::teams::OWNER_ROLE { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "The `Owner` role is restricted to one person".to_string(), )); } @@ -207,11 +207,11 @@ pub async fn add_team_member( if let Some(req) = request { if req.accepted { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "The user is already a member of that team".to_string(), )); } else { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "There is already a pending member request for this user" .to_string(), )); @@ -221,9 +221,7 @@ pub async fn add_team_member( crate::database::models::User::get(member.user_id, &**pool) .await? .ok_or_else(|| { - ApiError::InvalidInputError( - "An invalid User ID specified".to_string(), - ) + ApiError::InvalidInput("An invalid User ID specified".to_string()) })?; let new_id = @@ -312,7 +310,7 @@ pub async fn edit_team_member( TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) .await? .ok_or_else(|| { - ApiError::CustomAuthenticationError( + ApiError::CustomAuthentication( "You don't have permission to edit members of this team" .to_string(), ) @@ -321,7 +319,7 @@ pub async fn edit_team_member( TeamMember::get_from_user_id_pending(id, user_id, &**pool) .await? .ok_or_else(|| { - ApiError::CustomAuthenticationError( + ApiError::CustomAuthentication( "You don't have permission to edit members of this team" .to_string(), ) @@ -330,13 +328,13 @@ pub async fn edit_team_member( let mut transaction = pool.begin().await?; if &*edit_member_db.role == crate::models::teams::OWNER_ROLE { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "The owner of a team cannot be edited".to_string(), )); } if !member.permissions.contains(Permissions::EDIT_MEMBER) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to edit members of this team" .to_string(), )); @@ -344,7 +342,7 @@ pub async fn edit_team_member( if let Some(new_permissions) = edit_member.permissions { if !member.permissions.contains(new_permissions) { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "The new permissions have permissions that you don't have" .to_string(), )); @@ -352,7 +350,7 @@ pub async fn edit_team_member( } if edit_member.role.as_deref() == Some(crate::models::teams::OWNER_ROLE) { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "The `Owner` role is restricted to one person".to_string(), )); } @@ -394,7 +392,7 @@ pub async fn transfer_ownership( ) .await? .ok_or_else(|| { - ApiError::CustomAuthenticationError( + ApiError::CustomAuthentication( "You don't have permission to edit members of this team" .to_string(), ) @@ -406,20 +404,20 @@ pub async fn transfer_ownership( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "The new owner specified does not exist".to_string(), ) })?; if member.role != crate::models::teams::OWNER_ROLE { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to edit the ownership of this team" .to_string(), )); } if !new_member.accepted { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "You can only transfer ownership to members who are currently in your team".to_string(), )); } @@ -466,7 +464,7 @@ pub async fn remove_team_member( TeamMember::get_from_user_id(id, current_user.id.into(), &**pool) .await? .ok_or_else(|| { - ApiError::CustomAuthenticationError( + ApiError::CustomAuthentication( "You don't have permission to edit members of this team" .to_string(), ) @@ -478,7 +476,7 @@ pub async fn remove_team_member( if let Some(delete_member) = delete_member { if delete_member.role == crate::models::teams::OWNER_ROLE { // The owner cannot be removed from a team - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "The owner can't be removed from a team".to_string(), )); } @@ -492,7 +490,7 @@ pub async fn remove_team_member( { TeamMember::delete(id, user_id, &**pool).await?; } else { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have permission to remove a member from this team".to_string(), )); } @@ -505,7 +503,7 @@ pub async fn remove_team_member( // permission can remove it. TeamMember::delete(id, user_id, &**pool).await?; } else { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have permission to cancel a team invite" .to_string(), )); diff --git a/src/routes/updates.rs b/src/routes/updates.rs index f4bd3803..8d8ab1e3 100644 --- a/src/routes/updates.rs +++ b/src/routes/updates.rs @@ -24,12 +24,12 @@ pub async fn forge_updates( &id, &**pool, ) .await? - .ok_or_else(|| ApiError::InvalidInputError(ERROR.to_string()))?; + .ok_or_else(|| ApiError::InvalidInput(ERROR.to_string()))?; let user_option = get_user_from_headers(req.headers(), &**pool).await.ok(); if !is_authorized(&project, &user_option, &pool).await? { - return Err(ApiError::InvalidInputError(ERROR.to_string())); + return Err(ApiError::InvalidInput(ERROR.to_string())); } let version_ids = database::models::Version::get_project_versions( diff --git a/src/routes/users.rs b/src/routes/users.rs index bb7c49f8..b16ec173 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -166,7 +166,7 @@ pub async fn user_edit( let user = get_user_from_headers(req.headers(), &**pool).await?; new_user.validate().map_err(|err| { - ApiError::ValidationError(validation_errors_to_string(err, None)) + ApiError::Validation(validation_errors_to_string(err, None)) })?; let id_option = crate::database::models::User::get_id_from_username_or_id( @@ -201,7 +201,7 @@ pub async fn user_edit( .execute(&mut *transaction) .await?; } else { - return Err(ApiError::InvalidInputError(format!( + return Err(ApiError::InvalidInput(format!( "Username {} is taken!", username ))); @@ -252,7 +252,7 @@ pub async fn user_edit( if let Some(role) = &new_user.role { if !user.role.is_mod() { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit the role of this user!" .to_string(), )); @@ -276,7 +276,7 @@ pub async fn user_edit( transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { - Err(ApiError::CustomAuthenticationError( + Err(ApiError::CustomAuthentication( "You do not have permission to edit this user!".to_string(), )) } @@ -313,7 +313,7 @@ pub async fn user_icon_edit( if let Some(id) = id_option { if user.id != id.into() && !user.role.is_mod() { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to edit this user's icon." .to_string(), )); @@ -374,7 +374,7 @@ pub async fn user_icon_edit( Ok(HttpResponse::NotFound().body("")) } } else { - Err(ApiError::InvalidInputError(format!( + Err(ApiError::InvalidInput(format!( "Invalid format for user icon: {}", ext.ext ))) @@ -407,24 +407,18 @@ pub async fn user_delete( if let Some(id) = id_option { if !user.role.is_mod() && user.id != id.into() { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have permission to delete this user!".to_string(), )); } let mut transaction = pool.begin().await?; - let result; - if &*removal_type.removal_type == "full" { - result = crate::database::models::User::remove_full( - id, - &mut transaction, - ) - .await?; + let result = if &*removal_type.removal_type == "full" { + crate::database::models::User::remove_full(id, &mut transaction) + .await? } else { - result = - crate::database::models::User::remove(id, &mut transaction) - .await?; + crate::database::models::User::remove(id, &mut transaction).await? }; transaction.commit().await?; @@ -454,7 +448,7 @@ pub async fn user_follows( if let Some(id) = id_option { if !user.role.is_mod() && user.id != id.into() { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have permission to see the projects this user follows!".to_string(), )); } @@ -504,7 +498,7 @@ pub async fn user_notifications( if let Some(id) = id_option { if !user.role.is_mod() && user.id != id.into() { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have permission to see the notifications of this user!".to_string(), )); } diff --git a/src/routes/v1/reports.rs b/src/routes/v1/reports.rs index 6d71a7bc..b08df023 100644 --- a/src/routes/v1/reports.rs +++ b/src/routes/v1/reports.rs @@ -64,7 +64,7 @@ pub async fn report_create( let mut bytes = web::BytesMut::new(); while let Some(item) = body.next().await { bytes.extend_from_slice(&item.map_err(|_| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "Error while parsing request payload!".to_string(), ) })?); @@ -79,7 +79,7 @@ pub async fn report_create( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError(format!( + ApiError::InvalidInput(format!( "Invalid report type: {}", new_report.report_type )) @@ -124,7 +124,7 @@ pub async fn report_create( ) } ItemType::Unknown => { - return Err(ApiError::InvalidInputError(format!( + return Err(ApiError::InvalidInput(format!( "Invalid report item type: {}", new_report.item_type.as_str() ))) diff --git a/src/routes/v1/tags.rs b/src/routes/v1/tags.rs index 1f31fe66..850e16b4 100644 --- a/src/routes/v1/tags.rs +++ b/src/routes/v1/tags.rs @@ -38,7 +38,7 @@ pub async fn category_create( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "Specified project type does not exist!".to_string(), ) })?; diff --git a/src/routes/v1/teams.rs b/src/routes/v1/teams.rs index d4d6a28a..24489a1d 100644 --- a/src/routes/v1/teams.rs +++ b/src/routes/v1/teams.rs @@ -42,7 +42,7 @@ pub async fn team_members_get( &**pool, ) .await - .map_err(ApiError::DatabaseError)?; + .map_err(ApiError::Database)?; if team_member.is_some() { let team_members: Vec = members_data diff --git a/src/routes/v1/users.rs b/src/routes/v1/users.rs index 523f153f..52e42869 100644 --- a/src/routes/v1/users.rs +++ b/src/routes/v1/users.rs @@ -66,7 +66,7 @@ pub async fn user_follows( if let Some(id) = id_option { if !user.role.is_mod() && user.id != id.into() { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have permission to see the projects this user follows!".to_string(), )); } diff --git a/src/routes/v1/versions.rs b/src/routes/v1/versions.rs index f5f10ee6..52abd694 100644 --- a/src/routes/v1/versions.rs +++ b/src/routes/v1/versions.rs @@ -269,7 +269,7 @@ pub async fn download_version( ) .fetch_optional(&**pool) .await - .map_err(|e| ApiError::DatabaseError(e.into()))?; + .map_err(|e| ApiError::Database(e.into()))?; if let Some(id) = result { Ok(HttpResponse::TemporaryRedirect() @@ -316,9 +316,9 @@ pub async fn delete_file( &**pool, ) .await - .map_err(ApiError::DatabaseError)? + .map_err(ApiError::Database)? .ok_or_else(|| { - ApiError::CustomAuthenticationError( + ApiError::CustomAuthentication( "You don't have permission to delete this file!" .to_string(), ) @@ -328,7 +328,7 @@ pub async fn delete_file( .permissions .contains(Permissions::DELETE_VERSION) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to delete this file!" .to_string(), )); diff --git a/src/routes/version_creation.rs b/src/routes/version_creation.rs index a3facbda..7a217820 100644 --- a/src/routes/version_creation.rs +++ b/src/routes/version_creation.rs @@ -103,7 +103,7 @@ pub async fn version_create( async fn version_create_inner( req: HttpRequest, mut payload: Multipart, - mut transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, file_host: &dyn FileHost, uploaded_files: &mut Vec, ) -> Result { @@ -322,7 +322,7 @@ async fn version_create_inner( all_game_versions.clone(), version_data.primary_file.is_some(), version_data.primary_file.as_deref() == Some(name), - &mut transaction, + transaction, ) .await?; } @@ -486,7 +486,7 @@ async fn upload_file_to_version_inner( req: HttpRequest, mut payload: Multipart, client: Data, - mut transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, file_host: &dyn FileHost, uploaded_files: &mut Vec, version_id: models::VersionId, @@ -597,7 +597,7 @@ async fn upload_file_to_version_inner( all_game_versions.clone(), true, false, - &mut transaction, + transaction, ) .await?; } diff --git a/src/routes/version_file.rs b/src/routes/version_file.rs index e92793fd..5a1ba107 100644 --- a/src/routes/version_file.rs +++ b/src/routes/version_file.rs @@ -135,9 +135,9 @@ pub async fn delete_file( &**pool, ) .await - .map_err(ApiError::DatabaseError)? + .map_err(ApiError::Database)? .ok_or_else(|| { - ApiError::CustomAuthenticationError( + ApiError::CustomAuthentication( "You don't have permission to delete this file!" .to_string(), ) @@ -147,7 +147,7 @@ pub async fn delete_file( .permissions .contains(Permissions::DELETE_VERSION) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You don't have permission to delete this file!" .to_string(), )); @@ -169,7 +169,7 @@ pub async fn delete_file( .await?; if files.len() < 2 { - return Err(ApiError::InvalidInputError( + return Err(ApiError::InvalidInput( "Versions must have at least one file uploaded to them" .to_string(), )); diff --git a/src/routes/versions.rs b/src/routes/versions.rs index 959a146e..840a5263 100644 --- a/src/routes/versions.rs +++ b/src/routes/versions.rs @@ -28,9 +28,10 @@ pub async fn version_list( ) -> Result { let string = info.into_inner().0; - let result = - database::models::Project::get_full_from_slug_or_project_id(&string, &**pool) - .await?; + let result = 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(); @@ -187,6 +188,7 @@ pub struct EditVersion { pub loaders: Option>, pub featured: Option, pub primary_file: Option<(String, String)>, + pub downloads: Option, } #[patch("{id}")] @@ -199,7 +201,7 @@ pub async fn version_edit( let user = get_user_from_headers(req.headers(), &**pool).await?; new_version.validate().map_err(|err| { - ApiError::ValidationError(validation_errors_to_string(err, None)) + ApiError::Validation(validation_errors_to_string(err, None)) })?; let version_id = info.into_inner().0; @@ -227,7 +229,7 @@ pub async fn version_edit( if let Some(perms) = permissions { if !perms.contains(Permissions::UPLOAD_VERSION) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have the permissions to edit this version!" .to_string(), )); @@ -321,7 +323,7 @@ pub async fn version_edit( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "No database entry for game version provided." .to_string(), ) @@ -358,7 +360,7 @@ pub async fn version_edit( ) .await? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "No database entry for loader provided." .to_string(), ) @@ -404,7 +406,7 @@ pub async fn version_edit( .fetch_optional(&**pool) .await? .ok_or_else(|| { - ApiError::InvalidInputError(format!( + ApiError::InvalidInput(format!( "Specified file with hash {} does not exist.", primary_file.1.clone() )) @@ -447,10 +449,38 @@ pub async fn version_edit( .await?; } + if let Some(downloads) = &new_version.downloads { + sqlx::query!( + " + UPDATE versions + SET downloads = $1 + WHERE (id = $2) + ", + *downloads as i32, + id as database::models::ids::VersionId, + ) + .execute(&mut *transaction) + .await?; + + let diff = *downloads - (version_item.downloads as u32); + + sqlx::query!( + " + UPDATE mods + SET downloads = downloads + $1 + WHERE (id = $2) + ", + diff as i32, + version_item.project_id as database::models::ids::ProjectId, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { - Err(ApiError::CustomAuthenticationError( + Err(ApiError::CustomAuthentication( "You do not have permission to edit this version!".to_string(), )) } @@ -503,7 +533,7 @@ pub async fn version_count_patch( .execute(pool.as_ref()), ) .await - .map_err(ApiError::SqlxDatabaseError)?; + .map_err(ApiError::SqlxDatabase)?; Ok(HttpResponse::Ok().body("")) } @@ -524,9 +554,9 @@ pub async fn version_delete( &**pool, ) .await - .map_err(ApiError::DatabaseError)? + .map_err(ApiError::Database)? .ok_or_else(|| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "You do not have permission to delete versions in this team".to_string(), ) })?; @@ -535,7 +565,7 @@ pub async fn version_delete( .permissions .contains(Permissions::DELETE_VERSION) { - return Err(ApiError::CustomAuthenticationError( + return Err(ApiError::CustomAuthentication( "You do not have permission to delete versions in this team" .to_string(), )); diff --git a/src/search/indexing/mod.rs b/src/search/indexing/mod.rs index 3bacaaba..6c9b45cc 100644 --- a/src/search/indexing/mod.rs +++ b/src/search/indexing/mod.rs @@ -8,23 +8,24 @@ use meilisearch_sdk::client::Client; use meilisearch_sdk::indexes::Index; use meilisearch_sdk::settings::Settings; use sqlx::postgres::PgPool; -use std::collections::{HashMap, VecDeque}; use thiserror::Error; #[derive(Error, Debug)] pub enum IndexingError { #[error("Error while connecting to the MeiliSearch database")] - IndexDBError(#[from] meilisearch_sdk::errors::Error), + Indexing(#[from] meilisearch_sdk::errors::Error), #[error("Error while serializing or deserializing JSON: {0}")] - SerdeError(#[from] serde_json::Error), + Serde(#[from] serde_json::Error), #[error("Error while parsing a timestamp: {0}")] - ParseDateError(#[from] chrono::format::ParseError), + ParseDate(#[from] chrono::format::ParseError), #[error("Database Error: {0}")] - SqlxError(#[from] sqlx::error::Error), + Sqlx(#[from] sqlx::error::Error), #[error("Database Error: {0}")] - DatabaseError(#[from] crate::database::models::DatabaseError), + Database(#[from] crate::database::models::DatabaseError), #[error("Environment Error")] - EnvError(#[from] dotenv::Error), + Env(#[from] dotenv::Error), + #[error("Error while awaiting index creation task")] + Task, } // The chunk size for adding projects to the indexing database. If the request size @@ -57,8 +58,8 @@ pub async fn index_projects( if settings.index_local { docs_to_add.append(&mut index_local(pool.clone()).await?); } - // Write Indices + // Write Indices add_projects(docs_to_add, config).await?; Ok(()) @@ -67,122 +68,91 @@ pub async fn index_projects( pub async fn reset_indices(config: &SearchConfig) -> Result<(), IndexingError> { let client = config.make_client(); - 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?; - Ok(()) -} - -async fn update_index_helper<'a>( - client: &'a Client<'a>, - name: &'static str, - rule: &'static str, -) -> Result, IndexingError> { - update_index(client, name, { - let mut rules = default_rules(); - rules.push_back(rule); - rules.into() - }) - .await -} - -pub async fn reconfigure_indices( - config: &SearchConfig, -) -> Result<(), IndexingError> { - let client = config.make_client(); - - // Relevance Index - update_index_helper(&client, "relevance_projects", "desc(downloads)") - .await?; - update_index_helper(&client, "downloads_projects", "desc(downloads)") - .await?; - update_index_helper(&client, "follows_projects", "desc(follows)").await?; - update_index_helper( - &client, - "updated_projects", - "desc(modified_timestamp)", - ) - .await?; - update_index_helper(&client, "newest_projects", "desc(created_timestamp)") - .await?; + client.delete_index("projects").await?; + client.delete_index("projects_filtered").await?; Ok(()) } -async fn update_index<'a>( - client: &'a Client<'a>, - name: &'a str, - rules: Vec<&'static str>, -) -> Result, IndexingError> { - let index = match client.get_index(name).await { - Ok(index) => index, - Err(meilisearch_sdk::errors::Error::MeiliSearchError { - error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound, - .. - }) => client.create_index(name, Some("project_id")).await?, - Err(e) => { - return Err(IndexingError::IndexDBError(e)); - } - }; - index - .set_settings(&default_settings().with_ranking_rules(rules)) - .await?; - Ok(index) -} - -async fn create_index<'a>( - client: &'a Client<'a>, +async fn create_index( + client: &Client, name: &'static str, - rules: impl FnOnce() -> Vec<&'static str>, -) -> Result, IndexingError> { + custom_rules: Option<&'static [&'static str]>, +) -> Result { + client + .delete_index(name) + .await? + .wait_for_completion(client, None, None) + .await?; + match client.get_index(name).await { // TODO: update index settings on startup (or delete old indices on startup) - Ok(index) => Ok(index), - Err(meilisearch_sdk::errors::Error::MeiliSearchError { - error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound, - .. - }) => { + Ok(index) => { + index + .set_settings(&default_settings()) + .await? + .wait_for_completion(client, None, None) + .await?; + + Ok(index) + } + Err(meilisearch_sdk::errors::Error::Meilisearch( + meilisearch_sdk::errors::MeilisearchError { + error_code: meilisearch_sdk::errors::ErrorCode::IndexNotFound, + .. + }, + )) => { // Only create index and set settings if the index doesn't already exist - let index = client.create_index(name, Some("project_id")).await?; + let task = client.create_index(name, Some("project_id")).await?; + let task = task.wait_for_completion(client, None, None).await?; + let index = task + .try_make_index(client) + .map_err(|_| IndexingError::Task)?; + + let mut settings = default_settings(); + + if let Some(custom_rules) = custom_rules { + settings = settings.with_ranking_rules(custom_rules); + } index - .set_settings(&default_settings().with_ranking_rules(rules())) + .set_settings(&settings) + .await? + .wait_for_completion(client, None, None) .await?; Ok(index) } Err(e) => { log::warn!("Unhandled error while creating index: {}", e); - Err(IndexingError::IndexDBError(e)) + Err(IndexingError::Indexing(e)) } } } async fn add_to_index( - index: Index<'_>, + client: &Client, + index: Index, mods: &[UploadSearchProject], ) -> Result<(), IndexingError> { for chunk in mods.chunks(MEILISEARCH_CHUNK_SIZE) { - index.add_documents(chunk, Some("project_id")).await?; + index + .add_documents(chunk, Some("project_id")) + .await? + .wait_for_completion(client, None, None) + .await?; } Ok(()) } -async fn create_and_add_to_index<'a>( - client: &'a Client<'a>, - projects: &'a [UploadSearchProject], +async fn create_and_add_to_index( + client: &Client, + projects: &[UploadSearchProject], name: &'static str, - rule: &'static str, + custom_rules: Option<&'static [&'static str]>, ) -> Result<(), IndexingError> { - let index = create_index(client, name, || { - let mut relevance_rules = default_rules(); - relevance_rules.push_back(rule); - relevance_rules.into() - }) - .await?; - add_to_index(index, projects).await?; + let index = create_index(client, name, custom_rules).await?; + add_to_index(client, index, projects).await?; Ok(()) } @@ -192,65 +162,32 @@ pub async fn add_projects( ) -> Result<(), IndexingError> { let client = config.make_client(); + create_and_add_to_index(&client, &projects, "projects", None).await?; + create_and_add_to_index( &client, &projects, - "relevance_projects", - "desc(downloads)", - ) - .await?; - create_and_add_to_index( - &client, - &projects, - "downloads_projects", - "desc(downloads)", - ) - .await?; - create_and_add_to_index( - &client, - &projects, - "follows_projects", - "desc(follows)", - ) - .await?; - create_and_add_to_index( - &client, - &projects, - "updated_projects", - "desc(modified_timestamp)", - ) - .await?; - create_and_add_to_index( - &client, - &projects, - "newest_projects", - "desc(created_timestamp)", + "projects_filtered", + Some(&[ + "sort", + "words", + "typo", + "proximity", + "attribute", + "exactness", + ]), ) .await?; Ok(()) } -//region Utils -fn default_rules() -> VecDeque<&'static str> { - vec![ - "typo", - "words", - "proximity", - "attribute", - "wordsPosition", - "exactness", - ] - .into() -} - fn default_settings() -> Settings { Settings::new() .with_displayed_attributes(DEFAULT_DISPLAYED_ATTRIBUTES) .with_searchable_attributes(DEFAULT_SEARCHABLE_ATTRIBUTES) - .with_stop_words(Vec::::new()) - .with_synonyms(HashMap::>::new()) - .with_attributes_for_faceting(DEFAULT_ATTRIBUTES_FOR_FACETING) + .with_sortable_attributes(DEFAULT_SORTABLE_ATTRIBUTES) + .with_filterable_attributes(DEFAULT_ATTRIBUTES_FOR_FACETING) } const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ @@ -275,72 +212,22 @@ const DEFAULT_DISPLAYED_ATTRIBUTES: &[&str] = &[ ]; const DEFAULT_SEARCHABLE_ATTRIBUTES: &[&str] = - &["title", "description", "categories", "versions", "author"]; + &["title", "description", "author"]; const DEFAULT_ATTRIBUTES_FOR_FACETING: &[&str] = &[ "categories", - "host", "versions", "license", "client_side", "server_side", "project_type", + "downloads", + "follows", + "author", + "title", + "date_created", + "date_modified", ]; -//endregion -// This shouldn't be relied on for proper sorting, but it makes an -// attempt at getting proper sorting for Mojang's versions. -// This isn't currently 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_projects(a: &str, b: &str) -> std::cmp::Ordering { - use std::cmp::Ordering; - - let cmp = a.contains('.').cmp(&b.contains('.')); - if cmp != Ordering::Equal { - return cmp; - } - let mut a = a.split(&['.', '-'] as &[char]); - let mut b = b.split(&['.', '-'] as &[char]); - let a = (a.next(), a.next(), a.next(), a.next()); - let b = (b.next(), b.next(), b.next(), b.next()); - if a.0 == b.0 { - let cmp = - a.1.map(|s| s.chars().all(|c| c.is_ascii_digit())) - .cmp(&b.1.map(|s| s.chars().all(|c| c.is_ascii_digit()))); - if cmp != Ordering::Equal { - return cmp; - } - if a.1 == b.1 { - let cmp = - a.2.map(|s| s.chars().all(|c| c.is_ascii_digit())) - .unwrap_or(true) - .cmp( - &b.2.map(|s| s.chars().all(|c| c.is_ascii_digit())) - .unwrap_or(true), - ); - if cmp != Ordering::Equal { - return cmp; - } - if a.2 == b.2 { - match (a.3.is_some(), b.3.is_some()) { - (false, false) => Ordering::Equal, - (false, true) => Ordering::Greater, - (true, false) => Ordering::Less, - (true, true) => a.3.cmp(&b.3), - } - } else { - a.2.cmp(&b.2) - } - } else { - a.1.cmp(&b.1) - } - } else { - match (a.0 == Some("1"), b.0 == Some("1")) { - (false, false) => a.0.cmp(&b.0), - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - (true, true) => unreachable!(), - } - } -} +const DEFAULT_SORTABLE_ATTRIBUTES: &[&str] = + &["downloads", "follows", "date_created", "date_modified"]; diff --git a/src/search/mod.rs b/src/search/mod.rs index a8914567..b6692594 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -15,13 +15,13 @@ pub mod indexing; #[derive(Error, Debug)] pub enum SearchError { #[error("MeiliSearch Error: {0}")] - MeiliSearchError(#[from] meilisearch_sdk::errors::Error), + MeiliSearch(#[from] meilisearch_sdk::errors::Error), #[error("Error while serializing or deserializing JSON: {0}")] - SerdeError(#[from] serde_json::Error), + Serde(#[from] serde_json::Error), #[error("Error while parsing an integer: {0}")] - IntParsingError(#[from] std::num::ParseIntError), + IntParsing(#[from] std::num::ParseIntError), #[error("Environment Error")] - EnvError(#[from] dotenv::Error), + Env(#[from] dotenv::Error), #[error("Invalid index to sort by: {0}")] InvalidIndex(String), } @@ -29,10 +29,10 @@ pub enum SearchError { impl actix_web::ResponseError for SearchError { fn status_code(&self) -> StatusCode { match self { - SearchError::EnvError(..) => StatusCode::INTERNAL_SERVER_ERROR, - SearchError::MeiliSearchError(..) => StatusCode::BAD_REQUEST, - SearchError::SerdeError(..) => StatusCode::BAD_REQUEST, - SearchError::IntParsingError(..) => StatusCode::BAD_REQUEST, + SearchError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR, + SearchError::MeiliSearch(..) => StatusCode::BAD_REQUEST, + SearchError::Serde(..) => StatusCode::BAD_REQUEST, + SearchError::IntParsing(..) => StatusCode::BAD_REQUEST, SearchError::InvalidIndex(..) => StatusCode::BAD_REQUEST, } } @@ -40,10 +40,10 @@ impl actix_web::ResponseError for SearchError { fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()).json(ApiError { error: match self { - SearchError::EnvError(..) => "environment_error", - SearchError::MeiliSearchError(..) => "meilisearch_error", - SearchError::SerdeError(..) => "invalid_input", - SearchError::IntParsingError(..) => "invalid_input", + SearchError::Env(..) => "environment_error", + SearchError::MeiliSearch(..) => "meilisearch_error", + SearchError::Serde(..) => "invalid_input", + SearchError::IntParsing(..) => "invalid_input", SearchError::InvalidIndex(..) => "invalid_input", }, description: &self.to_string(), @@ -149,62 +149,85 @@ pub async fn search_for_project( ) -> Result { let client = Client::new(&*config.address, &*config.key); - let filters: Cow<_> = - match (info.filters.as_deref(), info.version.as_deref()) { - (Some(f), Some(v)) => format!("({}) AND ({})", f, v).into(), - (Some(f), None) => f.into(), - (None, Some(v)) => v.into(), - (None, None) => "".into(), - }; - let offset = info.offset.as_deref().unwrap_or("0").parse()?; let index = info.index.as_deref().unwrap_or("relevance"); let limit = info.limit.as_deref().unwrap_or("10").parse()?; - let index = match index { - "relevance" => "relevance_projects", - "downloads" => "downloads_projects", - "follows" => "follows_projects", - "updated" => "updated_projects", - "newest" => "newest_projects", + let sort = match index { + "relevance" => ("projects", ["downloads:desc"]), + "downloads" => ("projects_filtered", ["downloads:desc"]), + "follows" => ("projects_filtered", ["follows:desc"]), + "updated" => ("projects_filtered", ["date_created:desc"]), + "newest" => ("projects_filtered", ["date_modified:desc"]), i => return Err(SearchError::InvalidIndex(i.to_string())), }; - let meilisearch_index = client.get_index(index).await?; - let mut query = meilisearch_index.search(); + let meilisearch_index = client.get_index(sort.0).await?; - query.with_limit(min(100, limit)).with_offset(offset); + let mut filter_string = String::new(); - if let Some(search) = info.query.as_deref() { - if !search.is_empty() { - query.with_query(search); + let results = { + let mut query = meilisearch_index.search(); + + query + .with_limit(min(100, limit)) + .with_offset(offset) + .with_query(info.query.as_deref().unwrap_or_default()) + .with_sort(&sort.1); + + if let Some(new_filters) = info.new_filters.as_deref() { + query.with_filter(new_filters); + } else { + let facets = if let Some(facets) = &info.facets { + Some(serde_json::from_str::>>(facets)?) + } else { + None + }; + + let filters: Cow<_> = + match (info.filters.as_deref(), info.version.as_deref()) { + (Some(f), Some(v)) => format!("({}) AND ({})", f, v).into(), + (Some(f), None) => f.into(), + (None, Some(v)) => v.into(), + (None, None) => "".into(), + }; + + if let Some(facets) = facets { + filter_string.push('('); + for (index, facet_list) in facets.iter().enumerate() { + filter_string.push('('); + + for (facet_index, facet) in facet_list.iter().enumerate() { + filter_string.push_str(&facet.replace(':', " = ")); + + if facet_index != (facet_list.len() - 1) { + filter_string.push_str(" OR ") + } + } + + filter_string.push(')'); + + if index != (facets.len() - 1) { + filter_string.push_str(" AND ") + } + } + filter_string.push(')'); + + if !filters.is_empty() { + filter_string.push_str(&format!(" AND ({})", filter_string)) + } + } else { + filter_string.push_str(&*filters); + } + + println!("{}", filter_string); + if !filter_string.is_empty() { + query.with_filter(&filter_string); + } } - } - if !filters.is_empty() { - query.with_filters(&filters); - } - - // So the meilisearch sdk's lifetimes are... broken, to say the least - // They are overspecified and almost always wrong, and would generally - // just be better if they didn't specify them at all. - - // They also decided to have this take a &[&[&str]], which is impossible - // to construct efficiently. Instead it should take impl Iterator, - // &[impl AsRef<[&str]>], or one of many other proper solutions to that issue. - - let why_meilisearch; - let why_must_you_do_this; - if let Some(facets) = &info.facets { - why_meilisearch = serde_json::from_str::>>(facets)?; - why_must_you_do_this = why_meilisearch - .iter() - .map(|v| v as &[_]) - .collect::>(); - query.with_facet_filters(&why_must_you_do_this); - } - - let results = query.execute::().await?; + query.execute::().await? + }; Ok(SearchResults { hits: results.hits.into_iter().map(|r| r.result).collect(), diff --git a/src/util/auth.rs b/src/util/auth.rs index 9b8042db..7061d378 100644 --- a/src/util/auth.rs +++ b/src/util/auth.rs @@ -12,15 +12,15 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum AuthenticationError { #[error("An unknown database error occurred")] - SqlxDatabaseError(#[from] sqlx::Error), + Sqlx(#[from] sqlx::Error), #[error("Database Error: {0}")] - DatabaseError(#[from] crate::database::models::DatabaseError), + Database(#[from] crate::database::models::DatabaseError), #[error("Error while parsing JSON: {0}")] - SerdeError(#[from] serde_json::Error), + SerDe(#[from] serde_json::Error), #[error("Error while communicating to GitHub OAuth2: {0}")] - GithubError(#[from] reqwest::Error), + Github(#[from] reqwest::Error), #[error("Invalid Authentication Credentials")] - InvalidCredentialsError, + InvalidCredentials, } #[derive(Serialize, Deserialize, Debug)] @@ -73,7 +73,7 @@ where created: result.created, role: Role::from_string(&result.role), }), - None => Err(AuthenticationError::InvalidCredentialsError), + None => Err(AuthenticationError::InvalidCredentials), } } pub async fn get_user_from_headers<'a, 'b, E>( @@ -85,9 +85,9 @@ where { let token = headers .get("Authorization") - .ok_or(AuthenticationError::InvalidCredentialsError)? + .ok_or(AuthenticationError::InvalidCredentials)? .to_str() - .map_err(|_| AuthenticationError::InvalidCredentialsError)?; + .map_err(|_| AuthenticationError::InvalidCredentials)?; Ok(get_user_from_token(token, executor).await?) } @@ -104,7 +104,7 @@ where if user.role.is_mod() { Ok(user) } else { - Err(AuthenticationError::InvalidCredentialsError) + Err(AuthenticationError::InvalidCredentials) } } @@ -119,7 +119,7 @@ where match user.role { Role::Admin => Ok(user), - _ => Err(AuthenticationError::InvalidCredentialsError), + _ => Err(AuthenticationError::InvalidCredentials), } } diff --git a/src/util/routes.rs b/src/util/routes.rs index b785b5ab..99baea41 100644 --- a/src/util/routes.rs +++ b/src/util/routes.rs @@ -15,10 +15,10 @@ pub async fn read_from_payload( let mut bytes = BytesMut::new(); while let Some(item) = payload.next().await { if bytes.len() >= cap { - return Err(ApiError::InvalidInputError(String::from(err_msg))); + return Err(ApiError::InvalidInput(String::from(err_msg))); } else { bytes.extend_from_slice(&item.map_err(|_| { - ApiError::InvalidInputError( + ApiError::InvalidInput( "Unable to parse bytes in payload sent!".to_string(), ) })?); diff --git a/src/util/validate.rs b/src/util/validate.rs index a280cc19..34ad7fcd 100644 --- a/src/util/validate.rs +++ b/src/util/validate.rs @@ -34,7 +34,7 @@ pub fn validation_errors_to_string( *errors.clone(), Some(format!( "of list {} with index {}", - index, field + field, index )), )); } diff --git a/src/validate/fabric.rs b/src/validate/fabric.rs index 1293c601..5cc79fd0 100644 --- a/src/validate/fabric.rs +++ b/src/validate/fabric.rs @@ -33,7 +33,7 @@ impl super::Validator for FabricValidator { archive: &mut ZipArchive>, ) -> Result { archive.by_name("fabric.mod.json").map_err(|_| { - ValidationError::InvalidInputError( + ValidationError::InvalidInput( "No fabric.mod.json present for Fabric file.".into(), ) })?; diff --git a/src/validate/forge.rs b/src/validate/forge.rs index b98a55e4..993e2c6e 100644 --- a/src/validate/forge.rs +++ b/src/validate/forge.rs @@ -33,7 +33,7 @@ impl super::Validator for ForgeValidator { archive: &mut ZipArchive>, ) -> Result { archive.by_name("META-INF/mods.toml").map_err(|_| { - ValidationError::InvalidInputError( + ValidationError::InvalidInput( "No mods.toml present for Forge file.".into(), ) })?; diff --git a/src/validate/mod.rs b/src/validate/mod.rs index e83c50e9..b8b74ffa 100644 --- a/src/validate/mod.rs +++ b/src/validate/mod.rs @@ -14,15 +14,15 @@ mod pack; #[derive(Error, Debug)] pub enum ValidationError { #[error("Unable to read Zip Archive: {0}")] - ZipError(#[from] zip::result::ZipError), + Zip(#[from] zip::result::ZipError), #[error("IO Error: {0}")] - IoError(#[from] std::io::Error), + Io(#[from] std::io::Error), #[error("Error while validating JSON: {0}")] - SerdeError(#[from] serde_json::Error), + SerDe(#[from] serde_json::Error), #[error("Invalid Input: {0}")] - InvalidInputError(std::borrow::Cow<'static, str>), + InvalidInput(std::borrow::Cow<'static, str>), #[error("Error while managing threads")] - BlockingError(#[from] actix_web::error::BlockingError), + Blocking(#[from] actix_web::error::BlockingError), } #[derive(Eq, PartialEq)] @@ -93,7 +93,7 @@ pub async fn validate_file( } if visited { - Err(ValidationError::InvalidInputError( + Err(ValidationError::InvalidInput( format!( "File extension {} is invalid for input file", file_extension diff --git a/src/validate/pack.rs b/src/validate/pack.rs index 17cc9039..dd6ac52c 100644 --- a/src/validate/pack.rs +++ b/src/validate/pack.rs @@ -138,7 +138,7 @@ impl super::Validator for PackValidator { ) -> Result { let mut file = archive.by_name("modrinth.index.json").map_err(|_| { - ValidationError::InvalidInputError( + ValidationError::InvalidInput( "Pack manifest is missing.".into(), ) })?; @@ -149,20 +149,20 @@ impl super::Validator for PackValidator { let pack: PackFormat = serde_json::from_str(&contents)?; pack.validate().map_err(|err| { - ValidationError::InvalidInputError( + ValidationError::InvalidInput( validation_errors_to_string(err, None).into(), ) })?; if pack.game != "minecraft" { - return Err(ValidationError::InvalidInputError( + return Err(ValidationError::InvalidInput( format!("Game {0} does not exist!", pack.game).into(), )); } for file in pack.files { if file.hashes.get(&FileHash::Sha1).is_none() { - return Err(ValidationError::InvalidInputError( + return Err(ValidationError::InvalidInput( "All pack files must provide a SHA1 hash!".into(), )); } @@ -171,7 +171,7 @@ impl super::Validator for PackValidator { .components() .next() .ok_or_else(|| { - ValidationError::InvalidInputError( + ValidationError::InvalidInput( "Invalid pack file path!".into(), ) })?; @@ -179,7 +179,7 @@ impl super::Validator for PackValidator { match path { Component::CurDir | Component::Normal(_) => {} _ => { - return Err(ValidationError::InvalidInputError( + return Err(ValidationError::InvalidInput( "Invalid pack file path!".into(), )) }