From b2a73d286bf432373fef94328a58f5841623c0e4 Mon Sep 17 00:00:00 2001 From: trisua Date: Sat, 2 Aug 2025 16:04:50 -0400 Subject: [PATCH] add: mail ui --- Cargo.lock | 426 ++++++++++-------- crates/app/Cargo.toml | 2 +- crates/app/src/assets.rs | 10 + crates/app/src/langs/en-US.toml | 11 +- crates/app/src/macros.rs | 45 +- crates/app/src/public/css/style.css | 6 + crates/app/src/public/html/components.lisp | 122 ++++- crates/app/src/public/html/macros.lisp | 5 + crates/app/src/public/html/mail/compose.lisp | 139 ++++++ crates/app/src/public/html/mail/letter.lisp | 49 ++ crates/app/src/public/html/mail/received.lisp | 43 ++ crates/app/src/public/html/mail/sent.lisp | 43 ++ crates/app/src/public/html/misc/error.lisp | 2 +- crates/app/src/public/html/profile/base.lisp | 9 +- .../app/src/public/html/profile/settings.lisp | 9 + crates/app/src/routes/api/v1/letters.rs | 82 +++- crates/app/src/routes/api/v1/mod.rs | 2 +- crates/app/src/routes/pages/mail.rs | 156 +++++++ crates/app/src/routes/pages/mod.rs | 11 +- crates/core/Cargo.toml | 2 +- crates/core/src/config.rs | 1 + .../database/drivers/sql/create_letters.sql | 3 +- crates/core/src/database/letters.rs | 71 ++- crates/core/src/model/auth.rs | 3 + 24 files changed, 993 insertions(+), 259 deletions(-) create mode 100644 crates/app/src/public/html/mail/compose.lisp create mode 100644 crates/app/src/public/html/mail/letter.lisp create mode 100644 crates/app/src/public/html/mail/received.lisp create mode 100644 crates/app/src/public/html/mail/sent.lisp create mode 100644 crates/app/src/routes/pages/mail.rs 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,