diff --git a/Cargo.lock b/Cargo.lock index b77ba10..98b6561 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,7 +136,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -145,6 +145,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + [[package]] name = "async-recursion" version = "1.1.1" @@ -153,7 +164,32 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", +] + +[[package]] +name = "async-stripe" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecbddf002ad7a13d2041eadf1b234cb3f57653ffdd901a01bc3f1c65aa77440" +dependencies = [ + "chrono", + "futures-util", + "hex", + "hmac", + "http-types", + "hyper 0.14.32", + "hyper-tls 0.5.0", + "serde", + "serde_json", + "serde_path_to_error", + "serde_qs 0.10.1", + "sha2", + "smart-default", + "smol_str", + "thiserror 1.0.69", + "tokio", + "uuid 0.8.2", ] [[package]] @@ -164,7 +200,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -210,14 +246,14 @@ checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288" dependencies = [ "axum-core", "axum-macros", - "base64", + "base64 0.22.1", "bytes", "form_urlencoded", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "itoa", "matchit", @@ -248,8 +284,8 @@ checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", @@ -270,10 +306,10 @@ dependencies = [ "axum-core", "bytes", "cookie", - "fastrand", + "fastrand 2.3.0", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", "mime", "multer", @@ -293,7 +329,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -323,6 +359,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.22.1" @@ -430,7 +472,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.100", ] [[package]] @@ -506,7 +548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3e7381ca451b439579a09feb1d41d4b07c0e903bf8c74602b9e95219c40e47b" dependencies = [ "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-rustls", "hyper-util", "secrecy", @@ -541,6 +583,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] @@ -599,7 +642,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -653,6 +696,15 @@ dependencies = [ "xdg", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -781,7 +833,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -805,7 +857,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.100", ] [[package]] @@ -816,7 +868,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -873,7 +925,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -928,6 +980,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "exr" version = "1.73.0" @@ -971,6 +1029,15 @@ dependencies = [ "regex", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1074,6 +1141,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -1082,7 +1164,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -1130,6 +1212,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -1218,6 +1311,25 @@ dependencies = [ "web-time", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.8" @@ -1229,7 +1341,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.3.1", "indexmap", "slab", "tokio", @@ -1277,6 +1389,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex_fmt" version = "0.3.0" @@ -1304,6 +1422,17 @@ dependencies = [ "match_token", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.3.1" @@ -1315,6 +1444,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1322,7 +1462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.3.1", ] [[package]] @@ -1333,8 +1473,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -1344,6 +1484,27 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "base64 0.13.1", + "futures-lite", + "http 0.2.12", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs 0.8.5", + "serde_urlencoded", + "url", +] + [[package]] name = "httparse" version = "1.10.1" @@ -1365,6 +1526,30 @@ dependencies = [ "libm", ] +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.6.0" @@ -1374,9 +1559,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.8", + "http 1.3.1", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1393,8 +1578,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http", - "hyper", + "http 1.3.1", + "hyper 1.6.0", "hyper-util", "rustls", "rustls-native-certs", @@ -1404,6 +1589,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -1412,7 +1610,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "native-tls", "tokio", @@ -1429,9 +1627,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", "pin-project-lite", "socket2", "tokio", @@ -1577,7 +1775,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -1672,6 +1870,21 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -1680,7 +1893,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -1853,7 +2066,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -1949,7 +2162,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 1.3.1", "httparse", "memchr", "mime", @@ -2048,7 +2261,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2140,7 +2353,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2167,6 +2380,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2251,7 +2470,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2305,7 +2524,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2334,7 +2553,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2361,7 +2580,7 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d" dependencies = [ - "base64", + "base64 0.22.1", "indexmap", "quick-xml", "serde", @@ -2393,7 +2612,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76ff0abab4a9b844b93ef7b81f1efc0a366062aaef2cd702c76256b5dc075c54" dependencies = [ - "base64", + "base64 0.22.1", "byteorder", "bytes", "fallible-iterator 0.2.0", @@ -2444,7 +2663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.100", ] [[package]] @@ -2472,7 +2691,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -2496,7 +2715,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "221b7eace1aef8c95d65dbe09fb7a1a43d006045394a89afba6997721fcb7708" dependencies = [ - "base64", + "base64 0.22.1", "image", "qrcodegen", ] @@ -2546,6 +2765,19 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -2567,6 +2799,16 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2587,6 +2829,15 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -2605,6 +2856,15 @@ dependencies = [ "getrandom 0.3.2", ] +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rav1e" version = "0.7.1" @@ -2766,18 +3026,18 @@ version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.8", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-rustls", - "hyper-tls", + "hyper-tls 0.6.0", "hyper-util", "ipnet", "js-sys", @@ -3009,7 +3269,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -3034,6 +3294,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_qs" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cac3f1e2ca2fe333923a1ae72caca910b98ed0630bb35ef6f8c8517d6e81afa" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -3150,6 +3432,26 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +[[package]] +name = "smart-default" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "smol_str" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad6c857cbab2627dcf01ec85a623ca4e7dcb5691cbaa3d7fb7653671f0d09c9" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.5.8" @@ -3229,6 +3531,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.100" @@ -3257,7 +3570,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -3329,7 +3642,7 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ - "fastrand", + "fastrand 2.3.0", "getrandom 0.3.2", "once_cell", "rustix", @@ -3384,6 +3697,7 @@ name = "tetratto" version = "2.1.0" dependencies = [ "ammonia", + "async-stripe", "axum", "axum-extra", "cf-turnstile", @@ -3414,7 +3728,7 @@ version = "2.1.0" dependencies = [ "async-recursion", "base16ct", - "base64", + "base64 0.22.1", "bb8-postgres", "bitflags 2.9.0", "md-5", @@ -3452,7 +3766,7 @@ dependencies = [ "rand 0.9.1", "serde", "sha2", - "uuid", + "uuid 1.16.0", ] [[package]] @@ -3481,7 +3795,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -3492,7 +3806,7 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -3597,7 +3911,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -3754,8 +4068,8 @@ dependencies = [ "bitflags 2.9.0", "bytes", "futures-util", - "http", - "http-body", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", "http-range-header", "httpdate", @@ -3791,7 +4105,7 @@ dependencies = [ "axum", "forwarded-header-value", "governor", - "http", + "http 1.3.1", "pin-project", "thiserror 2.0.12", "tower", @@ -3818,7 +4132,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -3875,7 +4189,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.3.1", "httparse", "log", "rand 0.8.5", @@ -4006,6 +4320,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -4038,6 +4353,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.15", +] + [[package]] name = "uuid" version = "1.16.0" @@ -4082,6 +4406,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -4101,6 +4431,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -4144,7 +4480,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.100", "wasm-bindgen-shared", ] @@ -4179,7 +4515,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4541,7 +4877,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", "synstructure", ] @@ -4562,7 +4898,7 @@ checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] @@ -4582,7 +4918,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", "synstructure", ] @@ -4611,7 +4947,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.100", ] [[package]] diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 6538f33..9653006 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -40,3 +40,10 @@ redis = { version = "0.30.0", features = [ "tokio-comp", ], optional = true } tower_governor = "0.7.0" +async-stripe = { version = "0.41.0", features = [ + "events", + "checkout", + "webhook-events", + "billing", + "runtime-tokio-hyper", +] } diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 63502ca..e410ed1 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -8,7 +8,10 @@ use std::{ use tera::Context; use tetratto_core::{ config::Config, - model::auth::{DefaultTimelineChoice, User}, + model::{ + auth::{DefaultTimelineChoice, User}, + permissions::FinePermission, + }, PUBSUB_ENABLED, }; use tetratto_l10n::LangFile; @@ -36,7 +39,6 @@ pub const ROOT: &str = include_str!("./public/html/root.html"); pub const MACROS: &str = include_str!("./public/html/macros.html"); pub const COMPONENTS: &str = include_str!("./public/html/components.html"); -pub const MISC_INDEX: &str = include_str!("./public/html/misc/index.html"); pub const MISC_ERROR: &str = include_str!("./public/html/misc/error.html"); pub const MISC_NOTIFICATIONS: &str = include_str!("./public/html/misc/notifications.html"); pub const MISC_MARKDOWN: &str = include_str!("./public/html/misc/markdown.html"); @@ -232,8 +234,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"macros.html"(crate::assets::MACROS) --config=config); write_template!(html_path->"components.html"(crate::assets::COMPONENTS) --config=config); - write_template!(html_path->"misc/index.html"(crate::assets::MISC_INDEX) -d "misc" --config=config); - write_template!(html_path->"misc/error.html"(crate::assets::MISC_ERROR) --config=config); + write_template!(html_path->"misc/error.html"(crate::assets::MISC_ERROR) -d "misc" --config=config); write_template!(html_path->"misc/notifications.html"(crate::assets::MISC_NOTIFICATIONS) --config=config); write_template!(html_path->"misc/markdown.html"(crate::assets::MISC_MARKDOWN) --config=config); write_template!(html_path->"misc/requests.html"(crate::assets::MISC_REQUESTS) --config=config); @@ -312,6 +313,9 @@ pub(crate) async fn init_dirs(config: &Config) { create_dir_if_not_exists!( &PathBufD::current().extend(&[config.dirs.media.as_str(), "community_banners"]) ); + create_dir_if_not_exists!( + &PathBufD::current().extend(&[config.dirs.media.as_str(), "uploads"]) + ); write_if_track!(images_path->"default-avatar.svg"(DEFAULT_AVATAR) --config=config); write_if_track!(images_path->"default-banner.svg"(DEFAULT_BANNER) --config=config); @@ -346,6 +350,10 @@ pub(crate) async fn initial_context( if let Some(ua) = user { ctx.insert("is_helper", &ua.permissions.check_helper()); ctx.insert("is_manager", &ua.permissions.check_manager()); + ctx.insert( + "is_supporter", + &ua.permissions.check(FinePermission::SUPPORTER), + ); ctx.insert("home", ua.settings.default_timeline.relative_url()); } else { ctx.insert("is_helper", &false); diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index e370617..b731547 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -31,6 +31,7 @@ version = "1.0.0" "general:label.file_report" = "File report" "general:label.account_banned" = "Account banned" "general:label.account_banned_body" = "Your account has been banned for violating our policies." +"general:label.better_with_account" = "It's better with an account! Login or sign up to explore more." "dialog:action.okay" = "Ok" "dialog:action.continue" = "Continue" diff --git a/crates/app/src/public/html/communities/settings.html b/crates/app/src/public/html/communities/settings.html index a9d6417..820e4c0 100644 --- a/crates/app/src/public/html/communities/settings.html +++ b/crates/app/src/public/html/communities/settings.html @@ -559,6 +559,9 @@ MANAGE_PINS: 1 << 7, MANAGE_COMMUNITY: 1 << 8, MANAGE_QUESTIONS: 1 << 9, + MANAGE_CHANNELS: 1 << 10, + MANAGE_MESSAGES: 1 << 11, + MANAGE_EMOJIS: 1 << 12, }, ], ); diff --git a/crates/app/src/public/html/macros.html b/crates/app/src/public/html/macros.html index 25e215f..2ee65c8 100644 --- a/crates/app/src/public/html/macros.html +++ b/crates/app/src/public/html/macros.html @@ -119,10 +119,17 @@ {%- endmacro %} {% macro timelines_nav(selected="") -%}
+ {% if user %} {{ icon "newspaper" }} {{ text "general:link.home" }} + {% else %} + + {{ icon "earth" }} + {{ text "general:link.all" }} + + {% endif %} {{ icon "trending-up" }} diff --git a/crates/app/src/public/html/misc/index.html b/crates/app/src/public/html/misc/index.html deleted file mode 100644 index 600d98e..0000000 --- a/crates/app/src/public/html/misc/index.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "root.html" %} {% block head %} -{{ config.name }} -{% endblock %} {% block body %} -
- -
-{% endblock %} diff --git a/crates/app/src/public/html/mod/profile.html b/crates/app/src/public/html/mod/profile.html index 3705d11..d16612d 100644 --- a/crates/app/src/public/html/mod/profile.html +++ b/crates/app/src/public/html/mod/profile.html @@ -232,6 +232,10 @@ SUPPORTER: 1 << 19, MANAGE_REQUESTS: 1 << 20, MANAGE_QUESTIONS: 1 << 21, + MANAGE_CHANNELS: 1 << 22, + MANAGE_MESSAGES: 1 << 23, + MANAGE_UPLOADS: 1 << 24, + MANAGE_EMOJIS: 1 << 25, }, ], ); diff --git a/crates/app/src/public/html/profile/settings.html b/crates/app/src/public/html/profile/settings.html index 01d0006..0c1a9a8 100644 --- a/crates/app/src/public/html/profile/settings.html +++ b/crates/app/src/public/html/profile/settings.html @@ -38,6 +38,34 @@
+ {% if config.stripe %} +
+
+ {{ icon "star" }} + Supporter status +
+ +
+ {% if is_supporter %} +

You are a supporter! Thank you for all that you do. You can manage your billing information below. Please use your email address you supplied when paying to login to the billing portal.

+ Manage billing + {% else %} +

You're not currently a supporter! No pressure, but it helps us do some pretty cool things! As a supporter, you'll get:

+ +
    +
  • Vanity badge on profile
  • +
  • Ability to upload gif avatars/banners
  • +
  • Ability to use community emojis outside of their community (soon)
  • +
  • Ability to upload and use gif emojis (soon)
  • +
+ + Become a supporter + Please use your real email when completing payment. It is required to manage your billing settings. + {% endif %} +
+
+ {% endif %} +
Home timeline @@ -894,6 +922,7 @@ const theme_settings = document.getElementById("theme_settings"); ui.refresh_container(account_settings, [ + "supporter_card", "home_timeline", "notifications", "change_password", diff --git a/crates/app/src/public/html/timelines/all.html b/crates/app/src/public/html/timelines/all.html index 8d84ce2..dd33a4f 100644 --- a/crates/app/src/public/html/timelines/all.html +++ b/crates/app/src/public/html/timelines/all.html @@ -4,6 +4,26 @@
{{ macros::timelines_nav(selected="all") }} {{ macros::timelines_secondary_nav(posts="/all", questions="/all/questions") }} + {% if not user %} +
+
+ {{ icon "heart" }} + {{ text "general:label.better_with_account" }} +
+ + +
+ {% endif %}
diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index 5f5ad0d..bc8d0ed 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -757,7 +757,14 @@ media_theme_pref(); // link filter self.define("link_filter", (_) => { for (const anchor of Array.from(document.querySelectorAll("a"))) { - if (anchor.href.length === 0) { + if ( + anchor.href.length === 0 || + anchor.href.startsWith("https://github.com") || + anchor.href.startsWith("https://trisua.com") || + anchor.href.startsWith("https://buy.stripe.com") || + anchor.href.startsWith("https://billing.stripe.com") || + anchor.href.startsWith("https://last.fm") + ) { continue; } diff --git a/crates/app/src/routes/api/v1/auth/connections/mod.rs b/crates/app/src/routes/api/v1/auth/connections/mod.rs index a3bd829..5cd9813 100644 --- a/crates/app/src/routes/api/v1/auth/connections/mod.rs +++ b/crates/app/src/routes/api/v1/auth/connections/mod.rs @@ -1,5 +1,6 @@ pub mod last_fm; pub mod spotify; +pub mod stripe; use std::collections::HashMap; diff --git a/crates/app/src/routes/api/v1/auth/connections/stripe.rs b/crates/app/src/routes/api/v1/auth/connections/stripe.rs new file mode 100644 index 0000000..551c653 --- /dev/null +++ b/crates/app/src/routes/api/v1/auth/connections/stripe.rs @@ -0,0 +1,148 @@ +use std::time::Duration; + +use axum::{http::HeaderMap, response::IntoResponse, Extension, Json}; +use tetratto_core::model::{auth::Notification, permissions::FinePermission, ApiReturn, Error}; +use stripe::{EventObject, EventType}; +use crate::State; + +pub async fn stripe_webhook( + Extension(data): Extension, + headers: HeaderMap, + body: String, +) -> impl IntoResponse { + let data = &(data.read().await).0; + + if data.0.stripe.is_none() { + return Json(Error::MiscError("Disabled".to_string()).into()); + } + + let sig = match headers.get("Stripe-Signature") { + Some(s) => s, + None => return Json(Error::NotAllowed.into()), + }; + + let req = match stripe::Webhook::construct_event( + &body, + &sig.to_str().unwrap(), + &data.0.stripe.as_ref().unwrap().webhook_signing_secret, + ) { + Ok(e) => e, + Err(e) => return Json(Error::MiscError(e.to_string()).into()), + }; + + match req.type_ { + EventType::CheckoutSessionCompleted => { + // checkout session ended, store user customer id (from stripe) in their user data + let session = match req.data.object { + EventObject::CheckoutSession(c) => c, + _ => unreachable!("cannot be this"), + }; + + let user_id = session + .client_reference_id + .unwrap() + .parse::() + .unwrap(); + + let customer_id = session.customer.unwrap().id(); + + let user = match data.get_user_by_id(user_id).await { + Ok(ua) => ua, + Err(e) => return Json(e.into()), + }; + + tracing::info!("subscribe {} (stripe: {})", user.id, customer_id); + if let Err(e) = data + .update_user_stripe_id(user.id, customer_id.as_str()) + .await + { + return Json(e.into()); + } + } + EventType::InvoicePaymentSucceeded => { + // payment finished and subscription created + // we're doing this *instead* of CustomerSubscriptionDeleted because + // the invoice happens *after* the checkout session ends... which is what we need + let invoice = match req.data.object { + EventObject::Invoice(c) => c, + _ => unreachable!("cannot be this"), + }; + + let customer_id = invoice.customer.unwrap().id(); + + // allow 10s for everything to finalize + tokio::time::sleep(Duration::from_secs(10)).await; + + // pull user and update role + let user = match data.get_user_by_stripe_id(customer_id.as_str()).await { + Ok(ua) => ua, + Err(e) => return Json(e.into()), + }; + + tracing::info!("invoice {} (stripe: {})", user.id, customer_id); + let new_user_permissions = user.permissions | FinePermission::SUPPORTER; + + if let Err(e) = data + .update_user_role(user.id, new_user_permissions, user.clone(), true) + .await + { + return Json(e.into()); + } + + if let Err(e) = data + .create_notification(Notification::new( + "Welcome new supporter!".to_string(), + "Thank you for your support! Your account has been updated with your new role." + .to_string(), + user.id, + )) + .await + { + return Json(e.into()); + } + } + EventType::CustomerSubscriptionDeleted => { + // payment failed and subscription deleted + let subscription = match req.data.object { + EventObject::Subscription(c) => c, + _ => unreachable!("cannot be this"), + }; + + let customer_id = subscription.customer.id(); + + let user = match data.get_user_by_stripe_id(customer_id.as_str()).await { + Ok(ua) => ua, + Err(e) => return Json(e.into()), + }; + + tracing::info!("unsubscribe {} (stripe: {})", user.id, customer_id); + let new_user_permissions = user.permissions - FinePermission::SUPPORTER; + + if let Err(e) = data + .update_user_role(user.id, new_user_permissions, user.clone(), true) + .await + { + return Json(e.into()); + } + + if let Err(e) = data + .create_notification(Notification::new( + "Sorry to see you go... :(".to_string(), + "Thank you for your past support! Please feel free to leave us feedback on why you decided to cancel." + .to_string(), + user.id, + )) + .await + { + return Json(e.into()); + } + } + _ => return Json(Error::Unknown.into()), + } + + Json(ApiReturn { + ok: true, + message: "Acceptable".to_string(), + payload: (), + }) +} diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index 40efd9a..28133a0 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -247,7 +247,7 @@ pub async fn update_user_role_request( None => return Json(Error::NotAllowed.into()), }; - match data.update_user_role(id, req.role, user).await { + match data.update_user_role(id, req.role, user, false).await { Ok(_) => Json(ApiReturn { ok: true, message: "User updated".to_string(), diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 8f70147..4f54dcc 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -281,6 +281,10 @@ pub fn routes() -> Router { "/auth/user/connections/last_fm/api_proxy", post(auth::connections::last_fm::proxy_request), ) + .route( + "/service_hooks/stripe", + post(auth::connections::stripe::stripe_webhook), + ) // channels .route("/channels", post(channels::channels::create_request)) .route( diff --git a/crates/app/src/routes/pages/misc.rs b/crates/app/src/routes/pages/misc.rs index 0ddef29..ff77796 100644 --- a/crates/app/src/routes/pages/misc.rs +++ b/crates/app/src/routes/pages/misc.rs @@ -35,9 +35,22 @@ pub async fn index_request( Some(ua) => ua, None => { return { + // all timeline for unauthenticated users + // i'm only changing this for stripe + let list = match data.0.get_latest_posts(12, req.page).await { + Ok(l) => match data.0.fill_posts_with_community(l, 0, &Vec::new()).await { + Ok(l) => l, + Err(e) => return Html(render_error(e, &jar, &data, &None).await), + }, + Err(e) => return Html(render_error(e, &jar, &data, &None).await), + }; + let lang = get_lang!(jar, data.0); - let context = initial_context(&data.0.0, lang, &None).await; - Html(data.1.render("misc/index.html", &context).unwrap()) + let mut context = initial_context(&data.0.0, lang, &None).await; + + context.insert("list", &list); + context.insert("page", &req.page); + Html(data.1.render("timelines/all.html", &context).unwrap()) }; } }; diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index a9f353e..d154334 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -163,6 +163,39 @@ pub struct ConnectionsConfig { pub last_fm_secret: Option, } +/// Configuration for Stripe integration. +/// +/// User IDs are sent to Stripe through the payment link. +/// +/// +/// # Testing +/// +/// - Run `stripe login` using the Stripe CLI +/// - Run `stripe listen --forward-to localhost:4118/api/v1/service_hooks/stripe` +/// - Use testing card numbers: +#[derive(Clone, Serialize, Deserialize, Debug, Default)] +pub struct StripeConfig { + /// Payment link from the Stripe dashboard. + /// + /// 1. Create a product and set the price for your membership + /// 2. Set the product price to a recurring subscription + /// 3. Create a payment link for the new product + /// 4. The payment link pasted into this config field should NOT include a query string + pub payment_link: String, + /// To apply benefits to user accounts, you should then go into the Stripe developer + /// "workbench" and create a new webhook. The webhook needs the scopes: + /// `invoice.payment_succeeded`, `customer.subscription.deleted`, `checkout.session.completed`. + /// + /// The webhook's destination address should be `{your server origin}/api/v1/service_hooks/stripe`. + /// + /// The signing secret can be found on the right after you have created the webhook. + pub webhook_signing_secret: String, + /// The URL of your customer billing portal. + /// + /// + pub billing_portal_url: String, +} + /// Configuration file #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Config { @@ -224,6 +257,8 @@ pub struct Config { /// into every HTML template. They support access to template fields like `{{ user }}`. #[serde(default)] pub html_footer_path: String, + #[serde(default)] + pub stripe: Option, } fn default_name() -> String { @@ -311,6 +346,7 @@ impl Default for Config { town_square: 0, connections: default_connections(), html_footer_path: String::new(), + stripe: None, } } } diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 6de3e02..5d72940 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -45,6 +45,7 @@ impl DataManager { post_count: get!(x->15(i32)) as usize, request_count: get!(x->16(i32)) as usize, connections: serde_json::from_str(&get!(x->17(String)).to_string()).unwrap(), + stripe_id: get!(x->18(String)), } } @@ -139,7 +140,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)", params![ &(data.id as i64), &(data.created as i64), @@ -159,6 +160,7 @@ impl DataManager { &0_i32, &0_i32, &serde_json::to_string(&data.connections).unwrap(), + &"" ] ); @@ -433,24 +435,27 @@ impl DataManager { id: usize, role: FinePermission, user: User, + force: bool, ) -> Result<()> { - // check permission - if !user.permissions.check(FinePermission::MANAGE_USERS) { - return Err(Error::NotAllowed); - } - let other_user = self.get_user_by_id(id).await?; - if other_user.permissions.check_manager() && !user.permissions.check_admin() { - return Err(Error::MiscError( - "Cannot manage the role of other managers".to_string(), - )); - } + if !force { + // check permission + if !user.permissions.check(FinePermission::MANAGE_USERS) { + return Err(Error::NotAllowed); + } - if other_user.permissions == user.permissions { - return Err(Error::MiscError( - "Cannot manage users of equal level to you".to_string(), - )); + if other_user.permissions.check_manager() && !user.permissions.check_admin() { + return Err(Error::MiscError( + "Cannot manage the role of other managers".to_string(), + )); + } + + if other_user.permissions == user.permissions { + return Err(Error::MiscError( + "Cannot manage users of equal level to you".to_string(), + )); + } } // ... @@ -645,6 +650,9 @@ impl DataManager { auto_method!(update_user_connections(UserConnections)@get_user_by_id -> "UPDATE users SET connections = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_subscriptions(HashMap)@get_user_by_id -> "UPDATE users SET subscriptions = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); + auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User); + auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); + auto_method!(incr_user_notifications()@get_user_by_id -> "UPDATE users SET notification_count = notification_count + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --incr); auto_method!(decr_user_notifications()@get_user_by_id -> "UPDATE users SET notification_count = notification_count - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_user --decr); diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 8f5e14c..0272ed9 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -30,6 +30,8 @@ impl DataManager { execute!(&conn, common::CREATE_TABLE_IPBLOCKS).unwrap(); execute!(&conn, common::CREATE_TABLE_CHANNELS).unwrap(); execute!(&conn, common::CREATE_TABLE_MESSAGES).unwrap(); + execute!(&conn, common::CREATE_TABLE_UPLOADS).unwrap(); + execute!(&conn, common::CREATE_TABLE_EMOJIS).unwrap(); self.2 .set("atto.active_connections:users".to_string(), "0".to_string()) diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index 67d5bb0..fc030e2 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -15,3 +15,5 @@ pub const CREATE_TABLE_QUESTIONS: &str = include_str!("./sql/create_questions.sq pub const CREATE_TABLE_IPBLOCKS: &str = include_str!("./sql/create_ipblocks.sql"); pub const CREATE_TABLE_CHANNELS: &str = include_str!("./sql/create_channels.sql"); pub const CREATE_TABLE_MESSAGES: &str = include_str!("./sql/create_messages.sql"); +pub const CREATE_TABLE_UPLOADS: &str = include_str!("./sql/create_uploads.sql"); +pub const CREATE_TABLE_EMOJIS: &str = include_str!("./sql/create_emojis.sql"); diff --git a/crates/core/src/database/drivers/sql/create_emojis.sql b/crates/core/src/database/drivers/sql/create_emojis.sql new file mode 100644 index 0000000..57d8890 --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_emojis.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS emojis ( + id BIGINT NOT NULL PRIMARY KEY, + created BIGINT NOT NULL, + owner BIGINT NOT NULL, + community BIGINT NOT NULL, + upload_id BIGINT NOT NULL, + name TEXT NOT NULL +) diff --git a/crates/core/src/database/drivers/sql/create_uploads.sql b/crates/core/src/database/drivers/sql/create_uploads.sql new file mode 100644 index 0000000..a563080 --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_uploads.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS uploads ( + id BIGINT NOT NULL PRIMARY KEY, + created BIGINT NOT NULL, + owner BIGINT NOT NULL, + what TEXT NOT NULL +) diff --git a/crates/core/src/database/emojis.rs b/crates/core/src/database/emojis.rs new file mode 100644 index 0000000..9c47ff7 --- /dev/null +++ b/crates/core/src/database/emojis.rs @@ -0,0 +1,191 @@ +use super::*; +use crate::cache::Cache; +use crate::model::{ + Error, Result, auth::User, permissions::FinePermission, + communities_permissions::CommunityPermission, uploads::CustomEmoji, +}; +use crate::{auto_method, execute, get, query_row, query_rows, params}; + +#[cfg(feature = "sqlite")] +use rusqlite::Row; + +#[cfg(feature = "postgres")] +use tokio_postgres::Row; + +impl DataManager { + /// Get a [`CustomEmoji`] from an SQL row. + pub(crate) fn get_emoji_from_row( + #[cfg(feature = "sqlite")] x: &Row<'_>, + #[cfg(feature = "postgres")] x: &Row, + ) -> CustomEmoji { + CustomEmoji { + id: get!(x->0(i64)) as usize, + owner: get!(x->1(i64)) as usize, + created: get!(x->2(i64)) as usize, + community: get!(x->3(i64)) as usize, + upload_id: get!(x->4(i64)) as usize, + name: get!(x->5(String)), + } + } + + auto_method!(get_emoji_by_id(usize as i64)@get_emoji_from_row -> "SELECT * FROM emojis WHERE id = $1" --name="emoji" --returns=CustomEmoji --cache-key-tmpl="atto.emoji:{}"); + + /// Get all emojis by community. + /// + /// # Arguments + /// * `community` - the ID of the community to fetch emojis for + pub async fn get_emojis_by_community(&self, community: usize) -> Result> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM emojis WHERE community = $1 ORDER BY name ASC", + &[&(community as i64)], + |x| { Self::get_emoji_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("emoji".to_string())); + } + + Ok(res.unwrap()) + } + + /// Get all emojis by user. + /// + /// # Arguments + /// * `user` - the ID of the user to fetch emojis for + pub async fn get_emojis_by_user(&self, user: usize) -> Result> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM emojis WHERE (owner = $1 OR members LIKE $2) AND community = 0 ORDER BY created DESC", + params![&(user as i64), &format!("%{user}%")], + |x| { Self::get_emoji_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("emoji".to_string())); + } + + Ok(res.unwrap()) + } + + /// Get a emoji given its `owner` and a member. + /// + /// # Arguments + /// * `owner` - the ID of the owner + /// * `member` - the ID of the member + pub async fn get_emoji_by_owner_member( + &self, + owner: usize, + member: usize, + ) -> Result { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_row!( + &conn, + "SELECT * FROM emojis WHERE owner = $1 AND members = $2 AND community = 0 ORDER BY created DESC", + params![&(owner as i64), &format!("[{member}]")], + |x| { Ok(Self::get_emoji_from_row(x)) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("emoji".to_string())); + } + + Ok(res.unwrap()) + } + + /// Create a new emoji in the database. + /// + /// # Arguments + /// * `data` - a mock [`CustomEmoji`] object to insert + pub async fn create_emoji(&self, data: CustomEmoji) -> Result<()> { + let user = self.get_user_by_id(data.owner).await?; + + // check user permission in community + if data.community != 0 { + let membership = self + .get_membership_by_owner_community(user.id, data.community) + .await?; + + if !membership.role.check(CommunityPermission::MANAGE_EMOJIS) + && !user.permissions.check(FinePermission::MANAGE_EMOJIS) + { + return Err(Error::NotAllowed); + } + } + + // ... + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "INSERT INTO emojis VALUES ($1, $2, $3, $4, $5, $6)", + params![ + &(data.id as i64), + &(data.owner as i64), + &(data.created as i64), + &(data.community as i64), + &(data.upload_id as i64), + &data.name + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(()) + } + + pub async fn delete_emoji(&self, id: usize, user: &User) -> Result<()> { + let emoji = self.get_emoji_by_id(id).await?; + + // check user permission in community + if user.id != emoji.owner { + let membership = self + .get_membership_by_owner_community(user.id, emoji.community) + .await?; + + if !membership.role.check(CommunityPermission::MANAGE_EMOJIS) { + return Err(Error::NotAllowed); + } + } + + // ... + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!(&conn, "DELETE FROM emojis WHERE id = $1", &[&(id as i64)]); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // delete uploads + self.delete_upload(emoji.upload_id).await?; + + // ... + self.2.remove(format!("atto.emoji:{}", id)).await; + Ok(()) + } + + auto_method!(update_emoji_name(&str)@get_emoji_by_id:MANAGE_EMOJIS -> "UPDATE emojis SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.emoji:{}"); +} diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index a094500..62dac40 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -4,6 +4,7 @@ mod common; mod communities; pub mod connections; mod drivers; +mod emojis; mod ipbans; mod ipblocks; mod memberships; @@ -13,6 +14,7 @@ mod questions; mod reactions; mod reports; mod requests; +mod uploads; mod user_warnings; mod userblocks; mod userfollows; diff --git a/crates/core/src/database/uploads.rs b/crates/core/src/database/uploads.rs new file mode 100644 index 0000000..8c79599 --- /dev/null +++ b/crates/core/src/database/uploads.rs @@ -0,0 +1,122 @@ +use std::fs::{exists, remove_file}; +use super::*; +use crate::cache::Cache; +use crate::model::{Error, Result, uploads::MediaUpload}; +use crate::{auto_method, execute, get, query_row, query_rows, params}; + +use pathbufd::PathBufD; +#[cfg(feature = "sqlite")] +use rusqlite::Row; + +#[cfg(feature = "postgres")] +use tokio_postgres::Row; + +impl DataManager { + /// Get a [`MediaUpload`] from an SQL row. + pub(crate) fn get_upload_from_row( + #[cfg(feature = "sqlite")] x: &Row<'_>, + #[cfg(feature = "postgres")] x: &Row, + ) -> MediaUpload { + MediaUpload { + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + owner: get!(x->2(i64)) as usize, + what: serde_json::from_str(&get!(x->3(String))).unwrap(), + } + } + + auto_method!(get_upload_by_id(usize as i64)@get_upload_from_row -> "SELECT * FROM uploads WHERE id = $1" --name="upload" --returns=MediaUpload --cache-key-tmpl="atto.uploads:{}"); + + /// Get all uploads (paginated). + /// + /// # Arguments + /// * `batch` - the limit of items in each page + /// * `page` - the page number + pub async fn get_uploads(&self, batch: usize, page: usize) -> Result> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = query_rows!( + &conn, + "SELECT * FROM uploads ORDER BY created DESC LIMIT $1 OFFSET $2", + &[&(batch as i64), &((page * batch) as i64)], + |x| { Self::get_upload_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("upload".to_string())); + } + + Ok(res.unwrap()) + } + + /// Create a new upload in the database. + /// + /// # Arguments + /// * `data` - a mock [`MediaUpload`] object to insert + pub async fn create_upload(&self, data: MediaUpload) -> Result<()> { + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "INSERT INTO uploads VALUES ($1, $2, $3, $4)", + params![ + &(data.id as i64), + &(data.created as i64), + &(data.owner as i64), + &serde_json::to_string(&data.what).unwrap().as_str(), + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // return + Ok(()) + } + + pub async fn delete_upload(&self, id: usize) -> Result<()> { + // if !user.permissions.check(FinePermission::MANAGE_UPLOADS) { + // return Err(Error::NotAllowed); + // } + + // delete file + // it's most important that the file gets off the file system first, even + // if there's an issue in the database + // + // the actual file takes up much more space than the database entry. + let path = PathBufD::current().extend(&[self.0.dirs.media.as_str(), "uploads"]); + if let Ok(exists) = exists(&path) { + if exists { + if let Err(e) = remove_file(&path) { + return Err(Error::MiscError(e.to_string())); + } + } + } else { + return Err(Error::GeneralNotFound("file".to_string())); + } + + // delete from database + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!(&conn, "DELETE FROM uploads WHERE id = $1", &[&(id as i64)]); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!("atto.upload:{}", id)).await; + + // return + Ok(()) + } +} diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index a7f147d..f565fa6 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -40,6 +40,9 @@ pub struct User { /// External service connection details. #[serde(default)] pub connections: UserConnections, + /// The user's Stripe customer ID. + #[serde(default)] + pub stripe_id: String, } pub type UserConnections = @@ -245,6 +248,7 @@ impl User { post_count: 0, request_count: 0, connections: HashMap::new(), + stripe_id: String::new(), } } diff --git a/crates/core/src/model/communities_permissions.rs b/crates/core/src/model/communities_permissions.rs index 731d4bf..d8f415e 100644 --- a/crates/core/src/model/communities_permissions.rs +++ b/crates/core/src/model/communities_permissions.rs @@ -20,6 +20,7 @@ bitflags! { const MANAGE_QUESTIONS = 1 << 9; const MANAGE_CHANNELS = 1 << 10; const MANAGE_MESSAGES = 1 << 11; + const MANAGE_EMOJIS = 1 << 12; const _ = !0; } diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index 8c5e6f9..77ebb88 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -6,6 +6,7 @@ pub mod oauth; pub mod permissions; pub mod reactions; pub mod requests; +pub mod uploads; #[cfg(feature = "redis")] pub mod channels; diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index 0073e60..f90e2d6 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -32,6 +32,8 @@ bitflags! { const MANAGE_QUESTIONS = 1 << 21; const MANAGE_CHANNELS = 1 << 22; const MANAGE_MESSAGES = 1 << 23; + const MANAGE_UPLOADS = 1 << 24; + const MANAGE_EMOJIS = 1 << 25; const _ = !0; } diff --git a/crates/core/src/model/uploads.rs b/crates/core/src/model/uploads.rs new file mode 100644 index 0000000..0eeae71 --- /dev/null +++ b/crates/core/src/model/uploads.rs @@ -0,0 +1,66 @@ +use serde::{Serialize, Deserialize}; +use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp}; + +#[derive(Serialize, Deserialize)] +pub enum MediaType { + #[serde(alias = "image/webp")] + Webp, + #[serde(alias = "image/avif")] + Avif, + #[serde(alias = "image/png")] + Png, + #[serde(alias = "image/jpg")] + Jpg, + #[serde(alias = "image/gif")] + Gif, +} + +#[derive(Serialize, Deserialize)] +pub struct MediaUpload { + pub id: usize, + pub created: usize, + pub owner: usize, + pub what: MediaType, +} + +impl MediaUpload { + /// Create a new [`MediaUpload`]. + pub fn new(what: MediaType, owner: usize) -> Self { + Self { + id: AlmostSnowflake::new(1234567890) + .to_string() + .parse::() + .unwrap(), + created: unix_epoch_timestamp() as usize, + owner, + what, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct CustomEmoji { + pub id: usize, + pub created: usize, + pub owner: usize, + pub community: usize, + pub upload_id: usize, + pub name: String, +} + +impl CustomEmoji { + /// Create a new [`CustomEmoji`]. + pub fn new(owner: usize, community: usize, upload_id: usize, name: String) -> Self { + Self { + id: AlmostSnowflake::new(1234567890) + .to_string() + .parse::() + .unwrap(), + created: unix_epoch_timestamp() as usize, + owner, + community, + upload_id, + name, + } + } +} diff --git a/sql_changes/users_stripe_id.sql b/sql_changes/users_stripe_id.sql new file mode 100644 index 0000000..c800482 --- /dev/null +++ b/sql_changes/users_stripe_id.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN stripe_id TEXT DEFAULT '';