diff --git a/Cargo.lock b/Cargo.lock
index b2b3f59..38e681e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "adler2"
-version = "2.0.0"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
@@ -28,9 +28,12 @@ dependencies = [
[[package]]
name = "aligned-vec"
-version = "0.5.0"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
[[package]]
name = "ammonia"
@@ -62,9 +65,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.97"
+version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arbitrary"
@@ -80,7 +83,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -108,7 +111,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -144,7 +147,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -155,15 +158,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
-version = "1.4.0"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "av1-grain"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf"
+checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8"
dependencies = [
"anyhow",
"arrayvec",
@@ -175,9 +178,9 @@ dependencies = [
[[package]]
name = "avif-serialize"
-version = "0.8.3"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e"
+checksum = "2ea8ef51aced2b9191c08197f55450d830876d9933f8f48a429b354f1d496b42"
dependencies = [
"arrayvec",
]
@@ -273,7 +276,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -372,9 +375,9 @@ dependencies = [
[[package]]
name = "bstr"
-version = "1.11.3"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
+checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [
"memchr",
"serde",
@@ -388,15 +391,15 @@ checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
[[package]]
name = "bumpalo"
-version = "3.17.0"
+version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "bytemuck"
-version = "1.22.0"
+version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540"
+checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
[[package]]
name = "byteorder"
@@ -418,9 +421,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cc"
-version = "1.2.24"
+version = "1.2.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
+checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
dependencies = [
"jobserver",
"libc",
@@ -455,9 +458,9 @@ dependencies = [
[[package]]
name = "cfg-if"
-version = "1.0.0"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "chrono"
@@ -560,9 +563,9 @@ dependencies = [
[[package]]
name = "core-foundation"
-version = "0.10.0"
+version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
@@ -585,9 +588,9 @@ dependencies = [
[[package]]
name = "crc32fast"
-version = "1.4.2"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@@ -619,9 +622,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
@@ -653,14 +656,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
name = "data-encoding"
-version = "2.7.0"
+version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
+checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "deranged"
@@ -673,9 +676,9 @@ dependencies = [
[[package]]
name = "deunicode"
-version = "1.6.1"
+version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc55fe0d1f6c107595572ec8b107c0999bb1a2e0b75e37429a4fb0d6474a0e7d"
+checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"
[[package]]
name = "digest"
@@ -696,7 +699,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -738,6 +741,26 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "equator"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
+dependencies = [
+ "equator-macro",
+]
+
+[[package]]
+name = "equator-macro"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -746,12 +769,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
-version = "0.3.12"
+version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -807,9 +830,9 @@ dependencies = [
[[package]]
name = "flate2"
-version = "1.1.0"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -900,7 +923,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -983,7 +1006,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
@@ -1000,9 +1023,9 @@ dependencies = [
[[package]]
name = "gif"
-version = "0.13.1"
+version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2"
+checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
dependencies = [
"color_quant",
"weezl",
@@ -1046,9 +1069,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.3.26"
+version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
dependencies = [
"bytes",
"fnv",
@@ -1065,9 +1088,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.4.10"
+version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
+checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785"
dependencies = [
"atomic-waker",
"bytes",
@@ -1084,9 +1107,9 @@ dependencies = [
[[package]]
name = "half"
-version = "2.5.0"
+version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1"
+checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
dependencies = [
"cfg-if",
"crunchy",
@@ -1250,7 +1273,7 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
- "h2 0.3.26",
+ "h2 0.3.27",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
@@ -1273,7 +1296,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
- "h2 0.4.10",
+ "h2 0.4.11",
"http 1.3.1",
"http-body 1.0.1",
"httparse",
@@ -1287,9 +1310,9 @@ dependencies = [
[[package]]
name = "hyper-rustls"
-version = "0.27.6"
+version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http 1.3.1",
"hyper 1.6.0",
@@ -1333,9 +1356,9 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.13"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
+checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -1349,7 +1372,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2 0.5.10",
+ "socket2 0.6.0",
"system-configuration",
"tokio",
"tower-service",
@@ -1359,14 +1382,15 @@ dependencies = [
[[package]]
name = "iana-time-zone"
-version = "0.1.61"
+version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
+ "log",
"wasm-bindgen",
"windows-core",
]
@@ -1528,9 +1552,9 @@ dependencies = [
[[package]]
name = "image-webp"
-version = "0.2.1"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f"
+checksum = "f6970fe7a5300b4b42e62c52efa0187540a5bef546c60edaf554ef595d2e6f0b"
dependencies = [
"byteorder-lite",
"quick-error",
@@ -1544,9 +1568,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408"
[[package]]
name = "indexmap"
-version = "2.9.0"
+version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown",
@@ -1575,14 +1599,14 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
name = "io-uring"
-version = "0.7.8"
+version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
+checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
@@ -1632,9 +1656,9 @@ dependencies = [
[[package]]
name = "jpeg-decoder"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0"
+checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
[[package]]
name = "js-sys"
@@ -1660,15 +1684,15 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
[[package]]
name = "libc"
-version = "0.2.172"
+version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libfuzzer-sys"
-version = "0.4.9"
+version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
+checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
dependencies = [
"arbitrary",
"cc",
@@ -1676,9 +1700,9 @@ dependencies = [
[[package]]
name = "libm"
-version = "0.2.11"
+version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libwebp-sys"
@@ -1771,7 +1795,7 @@ checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -1811,9 +1835,9 @@ dependencies = [
[[package]]
name = "memchr"
-version = "2.7.4"
+version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "mime"
@@ -1839,9 +1863,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
-version = "0.8.8"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
@@ -1854,7 +1878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@@ -1954,7 +1978,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -2036,7 +2060,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -2124,9 +2148,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
-version = "2.7.15"
+version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
+checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
dependencies = [
"memchr",
"thiserror 2.0.12",
@@ -2135,9 +2159,9 @@ dependencies = [
[[package]]
name = "pest_derive"
-version = "2.7.15"
+version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e"
+checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
dependencies = [
"pest",
"pest_generator",
@@ -2145,24 +2169,23 @@ dependencies = [
[[package]]
name = "pest_generator"
-version = "2.7.15"
+version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"
+checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
name = "pest_meta"
-version = "2.7.15"
+version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea"
+checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
dependencies = [
- "once_cell",
"pest",
"sha2",
]
@@ -2216,7 +2239,7 @@ dependencies = [
"phf_shared 0.11.3",
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -2338,21 +2361,21 @@ dependencies = [
[[package]]
name = "profiling"
-version = "1.0.16"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
+checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
dependencies = [
"profiling-procmacros",
]
[[package]]
name = "profiling-procmacros"
-version = "1.0.16"
+version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
+checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
dependencies = [
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -2417,9 +2440,9 @@ dependencies = [
[[package]]
name = "r-efi"
-version = "5.2.0"
+version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
@@ -2558,9 +2581,9 @@ dependencies = [
[[package]]
name = "ravif"
-version = "0.11.11"
+version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6"
+checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b"
dependencies = [
"avif-serialize",
"imgref",
@@ -2615,9 +2638,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
-version = "0.5.12"
+version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
+checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.9.1",
]
@@ -2677,7 +2700,7 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
- "h2 0.4.10",
+ "h2 0.4.11",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
@@ -2712,9 +2735,9 @@ dependencies = [
[[package]]
name = "rgb"
-version = "0.8.50"
+version = "0.8.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
+checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce"
[[package]]
name = "ring"
@@ -2732,28 +2755,28 @@ dependencies = [
[[package]]
name = "rustc-demangle"
-version = "0.1.24"
+version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustix"
-version = "1.0.7"
+version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
name = "rustls"
-version = "0.23.27"
+version = "0.23.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321"
+checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc"
dependencies = [
"once_cell",
"ring",
@@ -2786,9 +2809,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
-version = "0.103.3"
+version = "0.103.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
+checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
dependencies = [
"ring",
"rustls-pki-types",
@@ -2866,7 +2889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
dependencies = [
"bitflags 2.9.1",
- "core-foundation 0.10.0",
+ "core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -2899,7 +2922,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -3042,12 +3065,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
-version = "0.4.9"
+version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
-dependencies = [
- "autocfg",
-]
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
[[package]]
name = "slug"
@@ -3181,9 +3201,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.101"
+version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",
@@ -3207,7 +3227,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -3298,7 +3318,7 @@ dependencies = [
[[package]]
name = "tetratto"
-version = "12.0.0"
+version = "13.0.0"
dependencies = [
"ammonia",
"async-stripe",
@@ -3330,7 +3350,7 @@ dependencies = [
[[package]]
name = "tetratto-core"
-version = "12.0.2"
+version = "13.0.0"
dependencies = [
"async-recursion",
"base16ct",
@@ -3403,7 +3423,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -3414,17 +3434,16 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
name = "thread_local"
-version = "1.1.8"
+version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
- "once_cell",
]
[[package]]
@@ -3440,9 +3459,9 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.40"
+version = "0.3.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618"
+checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
@@ -3461,9 +3480,9 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
[[package]]
name = "time-macros"
-version = "0.2.21"
+version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04"
+checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
dependencies = [
"num-conv",
"time-core",
@@ -3521,7 +3540,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -3572,9 +3591,9 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
-version = "0.26.1"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a"
+checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
@@ -3755,20 +3774,20 @@ dependencies = [
[[package]]
name = "tracing-attributes"
-version = "0.1.28"
+version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
name = "tracing-core"
-version = "0.1.33"
+version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
"valuable",
@@ -3811,17 +3830,16 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
-version = "0.26.1"
+version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24"
+checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
- "byteorder",
"bytes",
"data-encoding",
"http 1.3.1",
"httparse",
"log",
- "rand 0.8.5",
+ "rand 0.9.2",
"sha1",
"thiserror 2.0.12",
"utf-8",
@@ -3986,9 +4004,9 @@ dependencies = [
[[package]]
name = "v_frame"
-version = "0.3.8"
+version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
dependencies = [
"aligned-vec",
"num-traits",
@@ -4052,9 +4070,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
@@ -4093,7 +4111,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
"wasm-bindgen-shared",
]
@@ -4128,7 +4146,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4167,9 +4185,9 @@ dependencies = [
[[package]]
name = "web_atoms"
-version = "0.1.0"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "954c5a41f2bcb7314344079d0891505458cc2f4b422bdea1d5bfbe6d1a04903b"
+checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414"
dependencies = [
"phf 0.11.3",
"phf_codegen",
@@ -4189,9 +4207,9 @@ dependencies = [
[[package]]
name = "weezl"
-version = "0.1.8"
+version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
+checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
[[package]]
name = "whoami"
@@ -4246,28 +4264,54 @@ dependencies = [
[[package]]
name = "windows-core"
-version = "0.52.0"
+version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
- "windows-targets 0.52.6",
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
]
[[package]]
name = "windows-link"
-version = "0.1.1"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-registry"
-version = "0.4.0"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
+ "windows-link",
"windows-result",
"windows-strings",
- "windows-targets 0.53.0",
]
[[package]]
@@ -4281,9 +4325,9 @@ dependencies = [
[[package]]
name = "windows-strings"
-version = "0.3.1"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
@@ -4306,6 +4350,15 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.3",
+]
+
[[package]]
name = "windows-targets"
version = "0.48.5"
@@ -4339,10 +4392,11 @@ dependencies = [
[[package]]
name = "windows-targets"
-version = "0.53.0"
+version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
+checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
+ "windows-link",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
@@ -4493,9 +4547,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
-version = "0.7.10"
+version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
+checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
@@ -4535,28 +4589,28 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
"synstructure",
]
[[package]]
name = "zerocopy"
-version = "0.8.25"
+version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
+checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.25"
+version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
+checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -4576,7 +4630,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
"synstructure",
]
@@ -4616,7 +4670,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.101",
+ "syn 2.0.104",
]
[[package]]
@@ -4636,9 +4690,9 @@ dependencies = [
[[package]]
name = "zune-jpeg"
-version = "0.4.14"
+version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028"
+checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089"
dependencies = [
"zune-core",
]
diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml
index af0d66b..1d402a4 100644
--- a/crates/app/Cargo.toml
+++ b/crates/app/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "tetratto"
-version = "12.0.0"
+version = "13.0.0"
edition = "2024"
authors.workspace = true
repository.workspace = true
diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs
index 82cf197..5280133 100644
--- a/crates/app/src/assets.rs
+++ b/crates/app/src/assets.rs
@@ -139,6 +139,11 @@ pub const LITTLEWEB_BROWSER: &str = include_str!("./public/html/littleweb/browse
pub const MARKETPLACE_SELLER: &str = include_str!("./public/html/marketplace/seller.lisp");
+pub const MAIL_RECEIVED: &str = include_str!("./public/html/mail/received.lisp");
+pub const MAIL_SENT: &str = include_str!("./public/html/mail/sent.lisp");
+pub const MAIL_COMPOSE: &str = include_str!("./public/html/mail/compose.lisp");
+pub const MAIL_LETTER: &str = include_str!("./public/html/mail/letter.lisp");
+
// langs
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
@@ -365,6 +370,11 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD {
write_template!(html_path->"marketplace/seller.html"(crate::assets::MARKETPLACE_SELLER) -d "marketplace" --config=config --lisp plugins);
+ write_template!(html_path->"mail/received.html"(crate::assets::MAIL_RECEIVED) -d "mail" --config=config --lisp plugins);
+ write_template!(html_path->"mail/sent.html"(crate::assets::MAIL_SENT) --config=config --lisp plugins);
+ write_template!(html_path->"mail/compose.html"(crate::assets::MAIL_COMPOSE) --config=config --lisp plugins);
+ write_template!(html_path->"mail/letter.html"(crate::assets::MAIL_LETTER) --config=config --lisp plugins);
+
html_path
}
diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml
index bd692f3..c04243d 100644
--- a/crates/app/src/langs/en-US.toml
+++ b/crates/app/src/langs/en-US.toml
@@ -19,6 +19,7 @@ version = "1.0.0"
"general:link.journals" = "Journals"
"general:link.achievements" = "Achievements"
"general:link.little_web" = "Little web"
+"general:link.mail" = "Mail"
"general:action.save" = "Save"
"general:action.delete" = "Delete"
"general:action.purge" = "Purge"
@@ -203,7 +204,6 @@ version = "1.0.0"
"mod_panel:label.invited_by" = "Invited by"
"mod_panel:label.send_debug_payload" = "Send debug payload"
"mod_panel:label.ban_reason" = "Ban reason"
-"mod_panel:action.send" = "Send"
"requests:label.requests" = "Requests"
"requests:label.community_join_request" = "Community join request"
@@ -308,3 +308,12 @@ version = "1.0.0"
"marketplace:action.get_started" = "Get started"
"marketplace:action.finsh_setting_up_account" = "Finish setting up my account"
"marketplace:action.open_seller_dashboard" = "Open seller dashboard"
+
+"mail:label.received" = "Received"
+"mail:label.sent" = "Sent"
+"mail:label.compose" = "Compose"
+"mail:label.receivers" = "Receivers"
+"mail:label.subject" = "Subject"
+"mail:label.content" = "Content"
+"mail:action.send" = "Send"
+"mail:action.send_mail" = "Send mail"
diff --git a/crates/app/src/macros.rs b/crates/app/src/macros.rs
index 69730e0..9fbda85 100644
--- a/crates/app/src/macros.rs
+++ b/crates/app/src/macros.rs
@@ -189,6 +189,23 @@ macro_rules! user_banned {
};
}
+#[macro_export]
+macro_rules! check_user_is_blocked {
+ ($data:expr, $user:ident, $other_user:ident) => {
+ ($data
+ .get_userblock_by_initiator_receiver($other_user.id, $user.id)
+ .await
+ .is_ok()
+ | $data
+ .get_user_stack_blocked_users($other_user.id)
+ .await
+ .contains(&$user.id))
+ && !$user
+ .permissions
+ .check(tetratto_core::model::permissions::FinePermission::MANAGE_USERS)
+ };
+}
+
#[macro_export]
macro_rules! check_user_blocked_or_private {
($user:expr, $other_user:ident, $data:ident, $jar:ident) => {
@@ -244,20 +261,7 @@ macro_rules! check_user_blocked_or_private {
// check if we're blocked
if let Some(ref ua) = $user {
- if ($data
- .0
- .get_userblock_by_initiator_receiver($other_user.id, ua.id)
- .await
- .is_ok()
- | $data
- .0
- .get_user_stack_blocked_users($other_user.id)
- .await
- .contains(&ua.id))
- && !ua
- .permissions
- .check(tetratto_core::model::permissions::FinePermission::MANAGE_USERS)
- {
+ if crate::check_user_is_blocked!($data.0, ua, $other_user) {
let lang = get_lang!($jar, $data.0);
let mut context = initial_context(&$data.0.0.0, lang, &$user).await;
@@ -341,18 +345,7 @@ macro_rules! check_user_blocked_or_private {
($user:expr, $other_user:ident, $data:ident, @api) => {
// check if we're blocked
if let Some(ref ua) = $user {
- if ($data
- .get_userblock_by_initiator_receiver($other_user.id, ua.id)
- .await
- .is_ok()
- | $data
- .get_user_stack_blocked_users($other_user.id)
- .await
- .contains(&ua.id))
- && !ua
- .permissions
- .check(tetratto_core::model::permissions::FinePermission::MANAGE_USERS)
- {
+ if crate::check_user_is_blocked!($data, ua, $other_user) {
return Json(
tetratto_core::model::Error::MiscError("You're blocked".to_string()).into(),
);
diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css
index ab6a09d..a706c7d 100644
--- a/crates/app/src/public/css/style.css
+++ b/crates/app/src/public/css/style.css
@@ -293,6 +293,12 @@ button.small.square,
height: 32px;
}
+button.tiny.square,
+.button.tiny.square {
+ width: 24px;
+ height: 24px;
+}
+
button.big_icon svg,
.button.big_icon svg {
height: 16px;
diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp
index 8475223..08d98a2 100644
--- a/crates/app/src/public/html/components.lisp
+++ b/crates/app/src/public/html/components.lisp
@@ -113,6 +113,12 @@
("style" "color: var(--color-primary)")
("class" "flex items-center")
(text "{{ icon \"badge-check\" }}"))
+ (text "{%- endif %} {% if user.permissions|has_supporter -%}")
+ (span
+ ("title" "Supporter")
+ ("style" "color: var(--color-primary);")
+ ("class" "flex items-center")
+ (text "{{ icon \"star\" }}"))
(text "{%- endif %} {% if user.permissions|has_staff_badge -%}")
(span
("title" "Staff")
@@ -456,21 +462,25 @@
(div
("class" "w-full card-nest")
(div
- ("class" "card small notif_title flex items-center")
- (text "{% if not notification.read -%}")
- (svg
- ("width" "24")
- ("height" "24")
- ("viewBox" "0 0 24 24")
- ("style" "fill: var(--color-link)")
- (circle
- ("cx" "12")
- ("cy" "12")
- ("r" "6")))
- (text "{%- endif %}")
- (b
- ("class" "no_p_margin")
- (text "{{ notification.title|markdown|safe }}")))
+ ("class" "card small notif_title flex gap-2 justify-between items-center")
+ (div
+ ("class" "flex items-center")
+ (text "{% if not notification.read -%}")
+ (svg
+ ("width" "24")
+ ("height" "24")
+ ("viewBox" "0 0 24 24")
+ ("style" "fill: var(--color-link)")
+ (circle
+ ("cx" "12")
+ ("cy" "12")
+ ("r" "6")))
+ (text "{%- endif %}")
+ (b
+ ("class" "no_p_margin")
+ (text "{{ notification.title|markdown|safe }}")))
+
+ (span ("class" "date") (text "{{ notification.created }}")))
(div
("class" "card notif_content flex flex-col gap-2")
(span
@@ -2451,3 +2461,85 @@
(span
(str (text "dialog:action.continue"))))))
(text "{%- endif %} {%- endmacro %}")
+
+(text "{% macro letter_listing(letter, owner) -%}")
+(div
+ ("class" "card lowered flex gap-2 flex-row")
+ (a
+ ("href" "/@{{ owner.username }}")
+ (text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
+ (div
+ ("class" "flex flex-col")
+ (text "{{ self::full_username(user=owner) }}")
+ (div
+ ("class" "flex items-center gap-2")
+ ; read status
+ (text "{% if user.id in letter.read_by -%}")
+ (div ("class" "flex items-center green") (icon (text "mail-check")))
+ (text "{% else %}")
+ (div ("class" "flex items-center") (icon (text "mail")))
+ (text "{%- endif %}")
+
+ ; subject
+ (a ("class" "flush") ("href" "/mail/letter/{{ letter.id }}") (b (text "{{ letter.subject }}"))))))
+(text "{%- endmacro %}")
+
+(text "{% macro letter(letter, owner, show_subject=true) -%}")
+(div
+ ("class" "card-nest")
+ (text "{% if show_subject -%}")
+ (div
+ ("class" "card flex gap-2 flex-row")
+ (a
+ ("href" "/@{{ owner.username }}")
+ (text "{{ self::avatar(username=owner.username, size=\"32px\") }}"))
+ (div
+ ("class" "flex flex-col")
+ (text "{{ self::full_username(user=owner) }}")
+ (span
+ (b (text "{{ letter.subject }}"))
+ (text "{% if letter.replying_to -%}")
+ (a
+ ("href" "/mail/letter/{{ letter.replying_to }}")
+ (text " (up)"))
+ (text "{%- endif %}"))
+ (div
+ ("class" "flex flex-wrap gap-2")
+ (text "{% for receiver in letter.receivers %}")
+ (a
+ ("href" "/api/v1/auth/user/find/{{ receiver }}")
+ (text "{{ components::avatar(username=receiver, selector_type=\"id\", size=\"18px\") }}"))
+ (text "{%- endfor %}"))))
+ (text "{% else %}")
+ (div
+ ("class" "card small flex gap-2 flex-row")
+ (a
+ ("href" "/@{{ owner.username }}")
+ (text "{{ self::avatar(username=owner.username, size=\"24px\") }}"))
+ (text "{{ self::full_username(user=owner) }}"))
+ (text "{%- endif %}")
+
+ (div
+ ("class" "card flex flex-col gap-2")
+ (text "{{ letter.content|markdown|safe }}")
+ (hr)
+ (div
+ ("class" "flex gap-2 items-center")
+ (a
+ ("class" "button small lowered")
+ ("href" "/mail/compose?receivers={{ owner.username }}&subject=Re%3A%20{{ letter.subject }}&replying_to={{ letter.id }}")
+ ("title" "Reply")
+ (icon (text "reply")))
+ (a
+ ("class" "button small lowered")
+ ("href" "/mail/compose?receivers={% for receiver in letter.receivers %},id%3A{{ receiver }}{% endfor %}&subject=Re%3A%20{{ letter.subject }}&replying_to={{ letter.id }}")
+ ("title" "Reply all")
+ (icon (text "reply-all")))
+ (text "{% if user and letter.owner == user.id -%}")
+ (button
+ ("class" "small lowered red")
+ ("onclick" "delete_letter('{{ letter.id }}')")
+ ("title" "Delete")
+ (icon (text "trash")))
+ (text "{%- endif %}"))))
+(text "{%- endmacro %}")
diff --git a/crates/app/src/public/html/macros.lisp b/crates/app/src/public/html/macros.lisp
index fff3188..56495d5 100644
--- a/crates/app/src/public/html/macros.lisp
+++ b/crates/app/src/public/html/macros.lisp
@@ -76,6 +76,11 @@
("title" "Chats")
(icon (text "message-circle"))
(str (text "communities:label.chats")))
+ (a
+ ("href" "/mail")
+ ("title" "Mail")
+ (icon (text "mail"))
+ (str (text "general:link.mail")))
(a
("href" "/journals/0/0")
(icon (text "notebook"))
diff --git a/crates/app/src/public/html/mail/compose.lisp b/crates/app/src/public/html/mail/compose.lisp
new file mode 100644
index 0000000..1fc7c97
--- /dev/null
+++ b/crates/app/src/public/html/mail/compose.lisp
@@ -0,0 +1,139 @@
+(text "{% extends \"root.html\" %} {% block head %}")
+(title
+ (text "Compose letter - {{ config.name }}"))
+
+(text "{% endblock %} {% block body %} {{ macros::nav() }}")
+(main
+ (div
+ ("class" "card-nest")
+ (div
+ ("class" "card small items-center gap-2 flex justify-between")
+ (div
+ ("class" "flex gap-2 items-center")
+ (icon (text "mail-plus"))
+ (str (text "mail:label.compose")))
+
+ (button
+ ("onclick" "window.history.back()")
+ ("class" "lowered small")
+ (icon (text "arrow-left"))
+ (str (text "general:action.back"))))
+
+ (form
+ ("class" "card flex flex-col gap-2")
+ ("onsubmit" "create_letter_from_form(event)")
+ (div
+ ("class" "flex flex-col gap-1")
+ (span
+ (b (str (text "mail:label.receivers"))))
+ (div
+ ("class" "flex flex-wrap gap-2 small card lowered")
+ (div ("id" "receivers") ("class" "flex flex-wrap gap-2"))
+ (button
+ ("class" "small tiny big_icon square raised")
+ ("onclick" "add_receiver()")
+ ("type" "button")
+ (icon (text "plus")))))
+
+ (div
+ ("class" "flex flex-col gap-1")
+ (label
+ ("for" "subject")
+ (b (str (text "mail:label.subject"))))
+ (input
+ ("type" "text")
+ ("placeholder" "subject")
+ ("required" "")
+ ("name" "subject")
+ ("id" "subject")))
+
+ (div
+ ("class" "flex flex-col gap-1")
+ (label
+ ("for" "content")
+ (b (str (text "mail:label.content"))))
+ (textarea
+ ("placeholder" "content")
+ ("required" "")
+ ("name" "content")
+ ("id" "content")))
+
+ (button
+ (icon (text "send-horizontal"))
+ (str (text "mail:action.send"))))))
+
+(script
+ (text "globalThis.RECEIVERS = [];
+ globalThis.SEARCH_PARAMS = new URLSearchParams(window.location.search);
+
+ globalThis.add_receiver = async () => {
+ const username = await trigger(\"atto::prompt\", [\"Username:\"]);
+ if (!username) {
+ return;
+ }
+
+ RECEIVERS.push(username);
+ render_receivers();
+ }
+
+ globalThis.remove_receiver = (username) => {
+ RECEIVERS.splice(RECEIVERS.indexOf(username), 1);
+ render_receivers();
+ }
+
+ globalThis.render_receivers = () => {
+ const element = document.getElementById(\"receivers\");
+ element.innerHTML = \"\";
+
+ for (let receiver of RECEIVERS) {
+ const is_id = receiver.startsWith(\"id:\");
+ receiver = receiver.replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replace(\"id:\", \"\");
+ element.innerHTML += ``;
+ }
+ }
+
+ globalThis.create_letter_from_form = async (e) => {
+ e.preventDefault();
+ await trigger(\"atto::debounce\", [\"letters::create\"]);
+
+ fetch(\"/api/v1/letters\", {
+ method: \"POST\",
+ headers: {
+ \"Content-Type\": \"application/json\",
+ },
+ body: JSON.stringify({
+ content: e.target.content.value.trim(),
+ subject: e.target.subject.value.trim(),
+ receivers: RECEIVERS,
+ replying_to: SEARCH_PARAMS.get(\"replying_to\") || \"0\",
+ }),
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ if (!res.ok) {
+ trigger(\"atto::toast\", [\"error\", res.message]);
+ } else {
+ e.target.reset();
+ window.location.href = `/mail/letter/${res.payload}`;
+ }
+ });
+ };
+
+ if (SEARCH_PARAMS.get(\"receivers\")) {
+ let r = SEARCH_PARAMS.get(\"receivers\");
+
+ if (r.startsWith(\",\")) {
+ r = r.replace(\",\", \"\");
+ }
+
+ RECEIVERS = r.split(\",\");
+ render_receivers();
+ }
+
+ if (SEARCH_PARAMS.get(\"subject\")) {
+ document.getElementById(\"subject\").value = SEARCH_PARAMS.get(\"subject\");
+ }"))
+(text "{% endblock %}")
diff --git a/crates/app/src/public/html/mail/letter.lisp b/crates/app/src/public/html/mail/letter.lisp
new file mode 100644
index 0000000..4eb5d2d
--- /dev/null
+++ b/crates/app/src/public/html/mail/letter.lisp
@@ -0,0 +1,49 @@
+(text "{% extends \"root.html\" %} {% block head %}")
+(title
+ (text "Letter - {{ config.name }}"))
+(text "{% endblock %} {% block body %} {{ macros::nav() }}")
+(main
+ ("class" "flex flex-col gap-2")
+ (text "{{ components::letter(letter=letter, owner=owner) }}")
+
+ (text "{% for letter in replies %}")
+ (text "{{ components::letter(letter=letter[1], owner=letter[0], show_subject=false) }}")
+ (text "{%- endfor %}")
+
+ (text "{{ components::pagination(page=page, items=replies|length) }}"))
+
+(script
+ (text "globalThis.delete_letter = async (id) => {
+ if (
+ !(await trigger(\"atto::confirm\", [
+ \"Are you sure you would like to do this?\",
+ ]))
+ ) {
+ return;
+ }
+
+ fetch(`/api/v1/letters/${id}`, {
+ method: \"DELETE\",
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ trigger(\"atto::toast\", [
+ res.ok ? \"success\" : \"error\",
+ res.message,
+ ]);
+ });
+ }
+
+ fetch(\"/api/v1/letters/{{ letter.id }}/read\", {
+ method: \"POST\",
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ if (!res.ok) {
+ trigger(\"atto::toast\", [
+ res.ok ? \"success\" : \"error\",
+ res.message,
+ ]);
+ }
+ });"))
+(text "{% endblock %}")
diff --git a/crates/app/src/public/html/mail/received.lisp b/crates/app/src/public/html/mail/received.lisp
new file mode 100644
index 0000000..054c405
--- /dev/null
+++ b/crates/app/src/public/html/mail/received.lisp
@@ -0,0 +1,43 @@
+(text "{% extends \"root.html\" %} {% block head %}")
+(title
+ (text "Received mail - {{ config.name }}"))
+(text "{% endblock %} {% block body %} {{ macros::nav() }}")
+(main
+ ("class" "flex flex-col gap-2")
+ (div
+ ("class" "pillmenu")
+ (a
+ ("href" "/mail")
+ ("class" "active")
+ (str (text "mail:label.received")))
+ (a
+ ("href" "/mail/sent")
+ (str (text "mail:label.sent"))))
+
+ ; letters
+ (div
+ ("class" "card-nest")
+ (div
+ ("class" "card small flex items-center justify-between gap-2")
+ (div
+ ("class" "flex items-center gap-2")
+ (icon (text "mailbox"))
+ (str (text "mail:label.received")))
+ (a
+ ("href" "/mail/compose")
+ ("class" "button small lowered")
+ (icon (text "plus"))
+ (str (text "mail:label.compose"))))
+ (div
+ ("class" "card flex flex-col gap-2")
+ (text "{% for letter in list %}")
+ (text "{{ components::letter_listing(letter=letter[1], owner=letter[0]) }}")
+ (text "{% endfor %}")
+
+ ; pagination
+ (text "{% if list|length == 0 -%}")
+ (i ("class" "fade") (text "Nothing yet!"))
+ (text "{% else %}")
+ (text "{{ components::pagination(page=page, items=list|length) }}")
+ (text "{%- endif %}"))))
+(text "{% endblock %}")
diff --git a/crates/app/src/public/html/mail/sent.lisp b/crates/app/src/public/html/mail/sent.lisp
new file mode 100644
index 0000000..0e5d2b3
--- /dev/null
+++ b/crates/app/src/public/html/mail/sent.lisp
@@ -0,0 +1,43 @@
+(text "{% extends \"root.html\" %} {% block head %}")
+(title
+ (text "Sent mail - {{ config.name }}"))
+(text "{% endblock %} {% block body %} {{ macros::nav() }}")
+(main
+ ("class" "flex flex-col gap-2")
+ (div
+ ("class" "pillmenu")
+ (a
+ ("href" "/mail")
+ (str (text "mail:label.received")))
+ (a
+ ("href" "/mail/sent")
+ ("class" "active")
+ (str (text "mail:label.sent"))))
+
+ ; letters
+ (div
+ ("class" "card-nest")
+ (div
+ ("class" "card small flex items-center justify-between gap-2")
+ (div
+ ("class" "flex items-center gap-2")
+ (icon (text "mailbox"))
+ (str (text "mail:label.sent")))
+ (a
+ ("href" "/mail/compose")
+ ("class" "button small lowered")
+ (icon (text "plus"))
+ (str (text "mail:label.compose"))))
+ (div
+ ("class" "card flex flex-col gap-2")
+ (text "{% for letter in list %}")
+ (text "{{ components::letter_listing(letter=letter[1], owner=letter[0]) }}")
+ (text "{% endfor %}")
+
+ ; pagination
+ (text "{% if list|length == 0 -%}")
+ (i ("class" "fade") (text "Nothing yet!"))
+ (text "{% else %}")
+ (text "{{ components::pagination(page=page, items=list|length) }}")
+ (text "{%- endif %}"))))
+(text "{% endblock %}")
diff --git a/crates/app/src/public/html/misc/error.lisp b/crates/app/src/public/html/misc/error.lisp
index e7b4f16..758be6f 100644
--- a/crates/app/src/public/html/misc/error.lisp
+++ b/crates/app/src/public/html/misc/error.lisp
@@ -7,7 +7,7 @@
(div
("class" "card-nest")
(div
- ("class" "card")
+ ("class" "card small")
(b (text "Error 😦")))
(div
diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp
index e10dec9..efe7605 100644
--- a/crates/app/src/public/html/profile/base.lisp
+++ b/crates/app/src/public/html/profile/base.lisp
@@ -252,13 +252,20 @@
(text "{{ icon \"shield-off\" }}")
(span
(text "{{ text \"auth:action.unblock\" }}")))
- (text "{%- endif %} {% if not user.settings.private_chats or is_following_you %}")
+ (text "{%- endif %} {% if not profile.settings.private_chats or is_following_you %}")
(button
("onclick" "create_group_chat()")
("class" "lowered")
(text "{{ icon \"message-circle\" }}")
(span
(text "{{ text \"auth:action.message\" }}")))
+ (text "{%- endif %} {% if not profile.settings.private_mails or is_following_you %}")
+ (a
+ ("href" "/mail/compose?receivers={{ profile.username }}")
+ ("class" "button lowered")
+ (icon (text "mail-plus"))
+ (span
+ (str (text "mail:action.send_mail"))))
(text "{%- endif %} {% if is_helper -%}")
(a
("href" "/mod_panel/profile/{{ profile.id }}")
diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp
index 64d3a30..3c68666 100644
--- a/crates/app/src/public/html/profile/settings.lisp
+++ b/crates/app/src/public/html/profile/settings.lisp
@@ -1700,6 +1700,7 @@
[\"private_last_seen\", true],
[\"private_communities\", true],
[\"private_chats\", true],
+ [\"private_mails\", true],
[\"require_account\", true],
];
@@ -1830,6 +1831,14 @@
\"{{ profile.settings.private_chats }}\",
\"checkbox\",
],
+ [
+ [
+ \"private_mails\",
+ \"Only allow users I'm following to add send me mail\",
+ ],
+ \"{{ profile.settings.private_mails }}\",
+ \"checkbox\",
+ ],
[
[
\"private_communities\",
diff --git a/crates/app/src/routes/api/v1/letters.rs b/crates/app/src/routes/api/v1/letters.rs
index 35a1aa0..9ab3d36 100644
--- a/crates/app/src/routes/api/v1/letters.rs
+++ b/crates/app/src/routes/api/v1/letters.rs
@@ -1,16 +1,27 @@
-use axum::{response::IntoResponse, Extension, Json, extract::Path};
+use axum::{
+ extract::{Path, Query},
+ response::IntoResponse,
+ Extension, Json,
+};
use tetratto_core::model::{auth::Notification, mail::Letter, oauth, ApiReturn, Error};
-use crate::{get_user_from_token, State, cookie::CookieJar};
+use crate::{cookie::CookieJar, get_user_from_token, routes::pages::PaginatedQuery, State};
use super::CreateLetter;
-pub async fn list_received_request(jar: CookieJar, data: Extension) -> impl IntoResponse {
+pub async fn list_received_request(
+ jar: CookieJar,
+ data: Extension,
+ Query(props): Query,
+) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadLetters) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
- let letters = match data.get_received_letters_by_user(user.id).await {
+ let letters = match data
+ .get_received_letters_by_user(user.id, 12, props.page)
+ .await
+ {
Ok(l) => l,
Err(e) => return Json(e.into()),
};
@@ -22,14 +33,18 @@ pub async fn list_received_request(jar: CookieJar, data: Extension) -> im
})
}
-pub async fn list_sent_request(jar: CookieJar, data: Extension) -> impl IntoResponse {
+pub async fn list_sent_request(
+ jar: CookieJar,
+ data: Extension,
+ Query(props): Query,
+) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadLetters) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
- let letters = match data.get_letters_by_user(user.id).await {
+ let letters = match data.get_letters_by_user(user.id, 12, props.page).await {
Ok(l) => l,
Err(e) => return Json(e.into()),
};
@@ -92,7 +107,7 @@ pub async fn delete_request(
pub async fn create_request(
jar: CookieJar,
data: Extension,
- Json(props): Json,
+ Json(mut props): Json,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateLetters) {
@@ -100,10 +115,51 @@ pub async fn create_request(
None => return Json(Error::NotAllowed.into()),
};
+ // check receivers
+ props.receivers.dedup();
+ let mut receivers = Vec::new();
+
+ if props.receivers.len() < 1 {
+ return Json(Error::DataTooShort("receivers".to_string()).into());
+ } else if props.receivers.len() > 10 {
+ return Json(Error::DataTooLong("receivers".to_string()).into());
+ }
+
+ for receiver in &props.receivers {
+ let other_user = match if receiver.starts_with("id:") {
+ data.get_user_by_id(match receiver.replace("id:", "").parse() {
+ Ok(x) => x,
+ Err(_) => continue,
+ })
+ .await
+ } else {
+ data.get_user_by_username(receiver).await
+ } {
+ Ok(ua) => ua,
+ Err(e) => return Json(e.into()),
+ };
+
+ if crate::check_user_is_blocked!(data, user, other_user) {
+ continue;
+ }
+
+ if other_user.settings.private_mails
+ && data
+ .get_userfollow_by_initiator_receiver(other_user.id, user.id)
+ .await
+ .is_err()
+ {
+ continue;
+ }
+
+ receivers.push(other_user.id);
+ }
+
+ // ...
match data
.create_letter(Letter::new(
user.id,
- props.receivers,
+ receivers,
props.subject,
props.content,
match props.replying_to.parse() {
@@ -120,7 +176,7 @@ pub async fn create_request(
.create_notification(Notification::new(
"You've got mail!".to_string(),
format!(
- "[@{}](/api/v1/auth/user/find/{}) has sent you a [letter](/mail/{}).",
+ "[@{}](/api/v1/auth/user/find/{}) has sent you a [letter](/mail/letter/{}).",
user.username, user.id, l.id
),
*x,
@@ -135,7 +191,7 @@ pub async fn create_request(
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
- payload: Some(l),
+ payload: Some(l.id.to_string()),
})
}
Err(e) => return Json(e.into()),
@@ -163,7 +219,11 @@ pub async fn add_read_request(
}
if letter.read_by.contains(&user.id) {
- return Json(Error::MiscError("Already marked as read".to_string()).into());
+ return Json(ApiReturn {
+ ok: true,
+ message: "Already marked as read".to_string(),
+ payload: (),
+ });
}
letter.read_by.push(user.id);
diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs
index 0d25f09..8d1f655 100644
--- a/crates/app/src/routes/api/v1/mod.rs
+++ b/crates/app/src/routes/api/v1/mod.rs
@@ -1219,7 +1219,7 @@ pub struct QueryAppData {
#[derive(Deserialize)]
pub struct CreateLetter {
- pub receivers: Vec,
+ pub receivers: Vec,
pub subject: String,
pub content: String,
pub replying_to: String,
diff --git a/crates/app/src/routes/pages/mail.rs b/crates/app/src/routes/pages/mail.rs
new file mode 100644
index 0000000..b080116
--- /dev/null
+++ b/crates/app/src/routes/pages/mail.rs
@@ -0,0 +1,156 @@
+use axum::{
+ extract::{Query, Path},
+ response::{Html, IntoResponse},
+ Extension,
+};
+use crate::cookie::CookieJar;
+use tetratto_core::model::Error;
+use crate::{assets::initial_context, get_lang, get_user_from_token, State};
+use super::{render_error, PaginatedQuery};
+
+/// `/mail`
+pub async fn received_request(
+ jar: CookieJar,
+ Extension(data): Extension,
+ Query(props): Query,
+) -> impl IntoResponse {
+ let data = data.read().await;
+ let user = match get_user_from_token!(jar, data.0) {
+ Some(ua) => ua,
+ None => {
+ return Err(Html(
+ render_error(Error::NotAllowed, &jar, &data, &None).await,
+ ));
+ }
+ };
+
+ let list = match data
+ .0
+ .get_received_letters_by_user(user.id, 12, props.page)
+ .await
+ {
+ Ok(x) => match data.0.fill_letters(x).await {
+ Ok(x) => x,
+ Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ },
+ Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ };
+
+ let lang = get_lang!(jar, data.0);
+ let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
+
+ context.insert("list", &list);
+ context.insert("page", &props.page);
+
+ // return
+ Ok(Html(data.1.render("mail/received.html", &context).unwrap()))
+}
+
+/// `/mail/sent`
+pub async fn sent_request(
+ jar: CookieJar,
+ Extension(data): Extension,
+ Query(props): Query,
+) -> impl IntoResponse {
+ let data = data.read().await;
+ let user = match get_user_from_token!(jar, data.0) {
+ Some(ua) => ua,
+ None => {
+ return Err(Html(
+ render_error(Error::NotAllowed, &jar, &data, &None).await,
+ ));
+ }
+ };
+
+ let list = match data.0.get_letters_by_user(user.id, 12, props.page).await {
+ Ok(x) => match data.0.fill_letters(x).await {
+ Ok(x) => x,
+ Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ },
+ Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ };
+
+ let lang = get_lang!(jar, data.0);
+ let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
+
+ context.insert("list", &list);
+ context.insert("page", &props.page);
+
+ // return
+ Ok(Html(data.1.render("mail/sent.html", &context).unwrap()))
+}
+
+/// `/mail/compose`
+pub async fn compose_request(
+ jar: CookieJar,
+ Extension(data): Extension,
+) -> impl IntoResponse {
+ let data = data.read().await;
+ let user = match get_user_from_token!(jar, data.0) {
+ Some(ua) => ua,
+ None => {
+ return Err(Html(
+ render_error(Error::NotAllowed, &jar, &data, &None).await,
+ ));
+ }
+ };
+
+ let lang = get_lang!(jar, data.0);
+ let context = initial_context(&data.0.0.0, lang, &Some(user)).await;
+
+ // return
+ Ok(Html(data.1.render("mail/compose.html", &context).unwrap()))
+}
+
+/// `/mail/letter`
+pub async fn letter_request(
+ jar: CookieJar,
+ Extension(data): Extension,
+ Path(id): Path,
+ Query(props): Query,
+) -> impl IntoResponse {
+ let data = data.read().await;
+ let user = match get_user_from_token!(jar, data.0) {
+ Some(ua) => ua,
+ None => {
+ return Err(Html(
+ render_error(Error::NotAllowed, &jar, &data, &None).await,
+ ));
+ }
+ };
+
+ let letter = match data.0.get_letter_by_id(id).await {
+ Ok(l) => l,
+ Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ };
+
+ if !letter.can_read(&user) {
+ return Err(Html(
+ render_error(Error::NotAllowed, &jar, &data, &None).await,
+ ));
+ }
+
+ let owner = match data.0.get_user_by_id(letter.owner).await {
+ Ok(l) => l,
+ Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ };
+
+ let replies = match data.0.get_letters_by_replying_to(id, 12, props.page).await {
+ Ok(x) => match data.0.fill_letters(x).await {
+ Ok(x) => x,
+ Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ },
+ Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
+ };
+
+ let lang = get_lang!(jar, data.0);
+ let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
+
+ context.insert("letter", &letter);
+ context.insert("owner", &owner);
+ context.insert("replies", &replies);
+ context.insert("page", &props.page);
+
+ // return
+ Ok(Html(data.1.render("mail/letter.html", &context).unwrap()))
+}
diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs
index 2f3c9d5..ebd6b0d 100644
--- a/crates/app/src/routes/pages/mod.rs
+++ b/crates/app/src/routes/pages/mod.rs
@@ -5,6 +5,7 @@ pub mod developer;
pub mod forge;
pub mod journals;
pub mod littleweb;
+pub mod mail;
pub mod marketplace;
pub mod misc;
pub mod mod_panel;
@@ -17,10 +18,7 @@ use axum::{
};
use crate::cookie::CookieJar;
use serde::Deserialize;
-use tetratto_core::{
- model::{Error, auth::User},
-};
-
+use tetratto_core::model::{Error, auth::User};
use crate::{assets::initial_context, get_lang, InnerState};
pub fn routes() -> Router {
@@ -160,6 +158,11 @@ pub fn routes() -> Router {
"/settings/seller",
get(marketplace::seller_settings_request),
)
+ // mail
+ .route("/mail", get(mail::received_request))
+ .route("/mail/sent", get(mail::sent_request))
+ .route("/mail/compose", get(mail::compose_request))
+ .route("/mail/letter/{id}", get(mail::letter_request))
}
pub fn lw_routes() -> Router {
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 6ebd227..1fc9ee9 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "tetratto-core"
description = "The core behind Tetratto"
-version = "12.0.2"
+version = "13.0.0"
edition = "2024"
authors.workspace = true
repository.workspace = true
diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs
index e1637b1..aa845f6 100644
--- a/crates/core/src/config.rs
+++ b/crates/core/src/config.rs
@@ -388,6 +388,7 @@ fn default_banned_usernames() -> Vec {
"app".to_string(),
"services".to_string(),
"domains".to_string(),
+ "mail".to_string(),
]
}
diff --git a/crates/core/src/database/drivers/sql/create_letters.sql b/crates/core/src/database/drivers/sql/create_letters.sql
index f3100eb..4668b64 100644
--- a/crates/core/src/database/drivers/sql/create_letters.sql
+++ b/crates/core/src/database/drivers/sql/create_letters.sql
@@ -5,5 +5,6 @@ CREATE TABLE IF NOT EXISTS letters (
receivers TEXT NOT NULL,
subject TEXT NOT NULL,
content TEXT NOT NULL,
- read_by TEXT NOT NULL
+ read_by TEXT NOT NULL,
+ replying_to BIGINT NOT NULL
)
diff --git a/crates/core/src/database/letters.rs b/crates/core/src/database/letters.rs
index fe9bbac..80f973e 100644
--- a/crates/core/src/database/letters.rs
+++ b/crates/core/src/database/letters.rs
@@ -1,3 +1,5 @@
+use std::collections::HashMap;
+
use crate::model::{auth::User, mail::Letter, permissions::SecondaryPermission, Error, Result};
use crate::{auto_method, DataManager};
use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow};
@@ -13,7 +15,7 @@ impl DataManager {
subject: get!(x->4(String)),
content: get!(x->5(String)),
read_by: serde_json::from_str(&get!(x->6(String))).unwrap(),
- replying_to: get!(x->7(i32)) as usize,
+ replying_to: get!(x->7(i64)) as usize,
}
}
@@ -23,7 +25,14 @@ impl DataManager {
///
/// # Arguments
/// * `id` - the ID of the user to fetch letters for
- pub async fn get_letters_by_user(&self, id: usize) -> Result> {
+ /// * `batch` - the limit of items in each page
+ /// * `page` - the page number
+ pub async fn get_letters_by_user(
+ &self,
+ id: usize,
+ batch: usize,
+ page: usize,
+ ) -> Result> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@@ -31,8 +40,8 @@ impl DataManager {
let res = query_rows!(
&conn,
- "SELECT * FROM letters WHERE owner = $1 ORDER BY created DESC",
- &[&(id as i64)],
+ "SELECT * FROM letters WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
+ &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
|x| { Self::get_letter_from_row(x) }
);
@@ -47,7 +56,14 @@ impl DataManager {
///
/// # Arguments
/// * `id` - the ID of the user to fetch letters for
- pub async fn get_received_letters_by_user(&self, id: usize) -> Result> {
+ /// * `batch` - the limit of items in each page
+ /// * `page` - the page number
+ pub async fn get_received_letters_by_user(
+ &self,
+ id: usize,
+ batch: usize,
+ page: usize,
+ ) -> Result> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@@ -55,8 +71,12 @@ impl DataManager {
let res = query_rows!(
&conn,
- "SELECT * FROM letters WHERE receivers LIKE $1 ORDER BY created DESC",
- &[&format!("%\"{id}\"%")],
+ "SELECT * FROM letters WHERE receivers LIKE $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
+ &[
+ &format!("%{id}%"),
+ &(batch as i64),
+ &((page * batch) as i64)
+ ],
|x| { Self::get_letter_from_row(x) }
);
@@ -71,7 +91,14 @@ impl DataManager {
///
/// # Arguments
/// * `id` - the ID of the letter to fetch letters for
- pub async fn get_letters_by_replying_to(&self, id: usize) -> Result> {
+ /// * `batch` - the limit of items in each page
+ /// * `page` - the page number
+ pub async fn get_letters_by_replying_to(
+ &self,
+ id: usize,
+ batch: usize,
+ page: usize,
+ ) -> Result> {
let conn = match self.0.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@@ -79,8 +106,8 @@ impl DataManager {
let res = query_rows!(
&conn,
- "SELECT * FROM letters WHERE replying_to = $1 ORDER BY created DESC",
- &[&(id as i64)],
+ "SELECT * FROM letters WHERE replying_to = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
+ &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
|x| { Self::get_letter_from_row(x) }
);
@@ -91,6 +118,24 @@ impl DataManager {
Ok(res.unwrap())
}
+ /// Fill a list of letters with their owner.
+ pub async fn fill_letters(&self, letters: Vec) -> Result> {
+ let mut seen_users: HashMap = HashMap::new();
+ let mut out = Vec::new();
+
+ for letter in letters {
+ out.push(if let Some(ua) = seen_users.get(&letter.owner) {
+ (ua.to_owned(), letter)
+ } else {
+ let user = self.get_user_by_id(letter.owner).await?;
+ seen_users.insert(letter.owner, user.clone());
+ (user, letter)
+ })
+ }
+
+ Ok(out)
+ }
+
/// Create a new letter in the database.
///
/// # Arguments
@@ -109,6 +154,12 @@ impl DataManager {
return Err(Error::DataTooLong("content".to_string()));
}
+ if data.receivers.len() < 1 {
+ return Err(Error::DataTooShort("receivers".to_string()));
+ } else if data.receivers.len() > 10 {
+ return Err(Error::DataTooLong("receivers".to_string()));
+ }
+
// ...
let conn = match self.0.connect().await {
Ok(c) => c,
diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs
index 2e45b73..001bd45 100644
--- a/crates/core/src/model/auth.rs
+++ b/crates/core/src/model/auth.rs
@@ -273,6 +273,9 @@ pub struct UserSettings {
/// If other users that you aren't following can add you to chats.
#[serde(default)]
pub private_chats: bool,
+ /// If other users that you aren't following can send you letters.
+ #[serde(default)]
+ pub private_mails: bool,
/// The user's status. Shows over connection info.
#[serde(default)]
pub status: String,