commit ff8091fe9814e78f1bb1b12c9d06ec29b17c1eb8 Author: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Mon Feb 2 00:02:51 2026 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3e42a6b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2256 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "aquamarine" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941c39708478e8eea39243b5983f1c42d2717b3620ee91f4a52115fd02ac43f" +dependencies = [ + "itertools", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "num-traits", +] + +[[package]] +name = "clap" +version = "4.5.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.114", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "dptree" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81175dab5ec79c30e0576df2ed2c244e1721720c302000bb321b107e82e265c" +dependencies = [ + "futures", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erasable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437cfb75878119ed8265685c41a115724eae43fb7cc5a0bf0e4ecc3b803af1c4" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "never" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rc-box" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897fecc9fac6febd4408f9e935e86df739b0023b625e610e0357535b9c8adad0" +dependencies = [ + "erasable", +] + +[[package]] +name = "readlater-bot" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger", + "log", + "rand", + "serde", + "serde_json", + "teloxide", + "tempfile", + "tokio", + "toml", + "uuid", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "takecell" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" + +[[package]] +name = "teloxide" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63345cf32a8850ebddcdd769dc2d5193d5e231262d5dada264b79da01a664da" +dependencies = [ + "aquamarine", + "bytes", + "derive_more", + "dptree", + "futures", + "log", + "mime", + "pin-project", + "serde", + "serde_json", + "serde_with_macros", + "teloxide-core", + "teloxide-macros", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "url", +] + +[[package]] +name = "teloxide-core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "303db260110c238e3af77bb9dff18bf7a5b5196f783059b0852aab75f91d5a16" +dependencies = [ + "bitflags 1.3.2", + "bytes", + "chrono", + "derive_more", + "either", + "futures", + "log", + "mime", + "never", + "once_cell", + "pin-project", + "rc-box", + "reqwest", + "serde", + "serde_json", + "serde_with_macros", + "take_mut", + "takecell", + "thiserror", + "tokio", + "tokio-util", + "url", + "uuid", +] + +[[package]] +name = "teloxide-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1d653b093dba5e44cada57a516f572167df37b8a619443e59c8c517bb6d804" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.114", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +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.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..825aa2a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "readlater-bot" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +env_logger = "0.11" +log = "0.4" +rand = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tempfile = "3" +teloxide = { version = "0.12", features = ["macros", "auto-send"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } +toml = "0.8" +uuid = { version = "1", features = ["v4"] } diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..d2f0bf0 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,78 @@ +Scope +- Build a Telegram bot (Rust) packaged via Nix flake with a NixOS module and a package. +- Manage two Obsidian markdown files: /Users/thegeneralist/obsidian/10 Read Later.md and /Users/thegeneralist/obsidian/20 Finished Reading.md. +- Single-user bot (Telegram user ID 5311210922). +- No tags, no URL parsing, treat all inputs as raw text. + +Files and entry boundaries +- Entries are defined by lines beginning with - at column 1. +- An entry starts at a - line and continues until the next - line or EOF. +- Any text before the first - line is a preamble header and must be preserved unchanged. +- New entries must be written with - prefix. +- Multi-line entries are allowed; subsequent lines are written as-is (no indentation). +- Line endings normalized to LF (\n), ensure a trailing newline at EOF. +- UTF-8 encoding. + +Read Later behavior +- New items are prepended (inserted immediately after any preamble). +- Deduping: exact full-block match (entire entry text). If identical block exists, skip add and inform user. +- Add acknowledgment: send Saved. and auto-delete after 5s. + +Finished Reading behavior +- Mark Finished moves an entry: remove from Read Later, prepend to Finished (no separators). +- Acknowledge with Moved and auto-delete after 5s. +- Undo window: 30 minutes, persists across restarts. + +Delete behavior +- Delete requires two sequential confirmations via inline buttons. +- Confirmation buttons expire after 5 minutes. +- After delete, offer Undo for 30 minutes. + +Listing /list UX +- /list command required; also provide /start and /help. +- Initial chooser shows Top/Bottom/Random with counts (Top/Bottom buttons include count). +- Top/Bottom peek shows 3 entries at a time with previews (two-line preview). +- Under peek: buttons 1, 2, 3 to select an item, plus Prev, Next, Back, Random. +- Selecting an item shows full entry text with actions: Mark Finished, Delete, Back. +- Back from selected item returns to the same peek view. +- Random picks from all Read Later entries, avoids repeats per /list session. + +Multi-item messages +- If the incoming message contains the delimiter token ---, treat it as a multi-item input. +- Split by the token wherever it appears (not just on a line by itself). +- Show an interactive picker that lets you choose which items to add; include Add selected and Cancel. + +Errors and retries +- On write failure: retry 3 times immediately. +- If still failing: enqueue the operation on disk and notify user. +- Background retry interval: 30 seconds. +- Error messages are not auto-deleted. + +State storage +- No hidden markers and no sidecar index. +- A configurable data_dir stores queue and undo logs. + +Security +- Ignore messages from unauthorized users silently. +- Auth by Telegram user ID. + +Config +- TOML config file, path provided via NixOS module option. +- Required TOML fields: + - token (Telegram bot token) + - user_id (Telegram user ID) + - read_later_path (absolute path) + - finished_path (absolute path) + - data_dir (absolute path) + - retry_interval_seconds (default 30, configurable) + +Implementation +- Rust bot (teloxide or equivalent). +- Full-file rewrite on changes, with atomic write (write temp + rename). +- Serialize writes (single queue). +- Minimal logging: errors and counters only. + +Nix flake outputs +- packages..default builds the bot binary. +- nixosModules.default provides the NixOS service with configFile option. +- Service runs as a systemd unit; uses the TOML config path. diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..679e59f --- /dev/null +++ b/flake.nix @@ -0,0 +1,71 @@ +{ + description = "Read Later Telegram bot"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + }: + let + packageFor = + system: + let + pkgs = import nixpkgs { inherit system; }; + in + pkgs.rustPlatform.buildRustPackage { + pname = "readlater-bot"; + version = "0.1.0"; + src = self; + cargoLock.lockFile = ./Cargo.lock; + }; + in + flake-utils.lib.eachDefaultSystem (system: { + packages.default = packageFor system; + }) + // { + nixosModules.default = + { + config, + lib, + pkgs, + ... + }: + let + cfg = config.services.readlater-bot; + in + { + options.services.readlater-bot = { + enable = lib.mkEnableOption "Read Later Telegram bot"; + package = lib.mkOption { + type = lib.types.package; + default = self.packages.${pkgs.system}.default; + description = "Package providing the bot binary."; + }; + configFile = lib.mkOption { + type = lib.types.path; + description = "Path to TOML config file."; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.readlater-bot = { + description = "Read Later Telegram bot"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + ExecStart = "${cfg.package}/bin/readlater-bot --config ${cfg.configFile}"; + Restart = "on-failure"; + RestartSec = 5; + }; + }; + }; + }; + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0845d99 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,1471 @@ +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use anyhow::{anyhow, Context, Result}; +use clap::Parser; +use log::error; +use rand::seq::SliceRandom; +use serde::{Deserialize, Serialize}; +use teloxide::prelude::*; +use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup, Message, MessageId}; +use tokio::sync::Mutex; +use uuid::Uuid; + +const ACK_TTL_SECS: u64 = 5; +const UNDO_TTL_SECS: u64 = 30 * 60; +const DELETE_CONFIRM_TTL_SECS: u64 = 5 * 60; + +#[derive(Debug, Deserialize, Clone)] +struct Config { + token: String, + user_id: u64, + read_later_path: PathBuf, + finished_path: PathBuf, + data_dir: PathBuf, + retry_interval_seconds: Option, +} + +#[derive(Parser, Debug)] +struct Args { + #[arg(long)] + config: PathBuf, +} + +#[derive(Clone, Debug)] +struct EntryBlock { + lines: Vec, +} + +impl EntryBlock { + fn from_text(text: &str) -> Self { + let normalized = normalize_line_endings(text); + let mut lines: Vec = normalized.split('\n').map(|s| s.to_string()).collect(); + if lines.is_empty() { + lines.push(String::new()); + } + if let Some(first) = lines.get_mut(0) { + if first.starts_with("- ") { + // Keep as-is. + } else if first.starts_with('-') { + let rest = first[1..].trim_start(); + *first = format!("- {}", rest); + } else { + *first = format!("- {}", first); + } + } + EntryBlock { lines } + } + + fn from_block(block: &str) -> Self { + let normalized = normalize_line_endings(block); + let lines: Vec = normalized.split('\n').map(|s| s.to_string()).collect(); + EntryBlock { lines } + } + + fn block_string(&self) -> String { + self.lines.join("\n") + } + + fn display_lines(&self) -> Vec { + let mut lines = self.lines.clone(); + if let Some(first) = lines.get_mut(0) { + if first.starts_with("- ") { + *first = first[2..].to_string(); + } else if first.starts_with('-') { + let rest = first[1..].trim_start(); + *first = rest.to_string(); + } + } + lines + } + + fn preview_lines(&self) -> Vec { + let display = self.display_lines(); + let mut preview = Vec::new(); + if let Some(first) = display.get(0) { + preview.push(first.clone()); + } + if let Some(second) = display.get(1) { + preview.push(second.clone()); + } + if display.len() > 2 { + if let Some(last) = preview.last_mut() { + last.push_str("..."); + } + } + preview + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct QueuedOp { + kind: QueuedOpKind, + entry: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +enum QueuedOpKind { + Add, + Delete, + MoveToFinished, + MoveToReadLater, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +struct UndoRecord { + id: String, + kind: UndoKind, + entry: String, + expires_at: u64, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +enum UndoKind { + MoveToFinished, + Delete, +} + +#[derive(Clone, Debug)] +struct PickerState { + id: String, + chat_id: i64, + message_id: MessageId, + items: Vec, + selected: Vec, +} + +#[derive(Clone, Debug)] +struct ListSession { + id: String, + entries: Vec, + view: ListView, + seen_random: HashSet, +} + +#[derive(Clone, Debug)] +enum ListView { + Menu, + Peek { mode: ListMode, page: usize }, + Selected { return_to: Box, index: usize }, + DeleteConfirm { + selected: Box, + index: usize, + step: u8, + expires_at: u64, + }, +} + +#[derive(Clone, Debug, Copy)] +enum ListMode { + Top, + Bottom, +} + +struct AppState { + config: Config, + write_lock: Mutex<()>, + sessions: Mutex>, + pickers: Mutex>, + queue: Mutex>, + undo: Mutex>, + queue_path: PathBuf, + undo_path: PathBuf, +} + +#[derive(Debug)] +enum AddOutcome { + Added, + Duplicate, +} + +#[derive(Debug)] +enum ModifyOutcome { + Applied, + NotFound, +} + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init(); + + let args = Args::parse(); + let config = load_config(&args.config)?; + fs::create_dir_all(&config.data_dir).context("create data_dir")?; + + let queue_path = config.data_dir.join("queue.json"); + let undo_path = config.data_dir.join("undo.json"); + + let mut undo = load_undo(&undo_path)?; + prune_undo(&mut undo); + save_undo(&undo_path, &undo)?; + + let state = AppState { + config: config.clone(), + write_lock: Mutex::new(()), + sessions: Mutex::new(HashMap::new()), + pickers: Mutex::new(HashMap::new()), + queue: Mutex::new(load_queue(&queue_path)?), + undo: Mutex::new(undo), + queue_path, + undo_path, + }; + + let state = std::sync::Arc::new(state); + + let retry_secs = config.retry_interval_seconds.unwrap_or(30); + start_retry_loop(state.clone(), retry_secs); + + let bot = Bot::new(config.token.clone()); + + let handler = dptree::entry() + .branch(Update::filter_message().endpoint(handle_message)) + .branch(Update::filter_callback_query().endpoint(handle_callback)); + + Dispatcher::builder(bot, handler) + .dependencies(dptree::deps![state]) + .enable_ctrlc_handler() + .build() + .dispatch() + .await; + + Ok(()) +} + +async fn handle_message( + bot: Bot, + msg: Message, + state: std::sync::Arc, +) -> Result<()> { + let user_id = match msg.from() { + Some(user) => user.id.0, + None => return Ok(()), + }; + + if user_id != state.config.user_id { + return Ok(()); + } + + let text = match msg.text() { + Some(text) => text, + None => return Ok(()), + }; + + if let Some(cmd) = parse_command(text) { + match cmd { + "start" | "help" => { + let help = "Send any text to save it. Use /list to browse. Use --- to split a message into multiple items."; + bot.send_message(msg.chat.id, help).await?; + return Ok(()); + } + "list" => { + handle_list_command(bot, msg, state).await?; + return Ok(()); + } + _ => { + // Unknown command, fall through as text. + } + } + } + + if text.contains("---") { + handle_multi_item(bot, msg.chat.id, state, text).await?; + } else { + handle_single_item(bot, msg.chat.id, state, text).await?; + } + + Ok(()) +} + +async fn handle_callback( + bot: Bot, + q: CallbackQuery, + state: std::sync::Arc, +) -> Result<()> { + let user_id = q.from.id.0; + if user_id != state.config.user_id { + return Ok(()); + } + + if let Some(data) = q.data.as_deref() { + if data.starts_with("ls:") { + handle_list_callback(bot, q, state).await?; + } else if data.starts_with("pick:") { + handle_picker_callback(bot, q, state).await?; + } else if data.starts_with("undo:") { + handle_undo_callback(bot, q, state).await?; + } + } + + Ok(()) +} + +async fn handle_list_command( + bot: Bot, + msg: Message, + state: std::sync::Arc, +) -> Result<()> { + let entries = read_entries(&state.config.read_later_path)?.1; + let count = entries.len(); + let session_id = short_id(); + let session = ListSession { + id: session_id.clone(), + entries, + view: ListView::Menu, + seen_random: HashSet::new(), + }; + state + .sessions + .lock() + .await + .insert(msg.chat.id.0, session); + + let (text, kb) = build_menu_view(&session_id, count); + bot.send_message(msg.chat.id, text) + .reply_markup(kb) + .await?; + Ok(()) +} + +async fn handle_single_item( + bot: Bot, + chat_id: ChatId, + state: std::sync::Arc, + text: &str, +) -> Result<()> { + let entry = EntryBlock::from_text(text); + let op = QueuedOp { + kind: QueuedOpKind::Add, + entry: entry.block_string(), + }; + + match apply_user_op(&state, &op).await? { + UserOpOutcome::Applied(ApplyOutcome::Applied) => { + send_ephemeral(&bot, chat_id, "Saved.", ACK_TTL_SECS).await?; + } + UserOpOutcome::Applied(ApplyOutcome::Duplicate) => { + send_ephemeral(&bot, chat_id, "Already saved.", ACK_TTL_SECS).await?; + } + UserOpOutcome::Applied(ApplyOutcome::NotFound) => { + // Not used for add. + } + UserOpOutcome::Queued => { + send_error(&bot, chat_id, "Write failed; queued for retry.").await?; + } + } + + Ok(()) +} + +async fn handle_multi_item( + bot: Bot, + chat_id: ChatId, + state: std::sync::Arc, + text: &str, +) -> Result<()> { + let items = split_items(text); + if items.is_empty() { + send_error(&bot, chat_id, "No items found.").await?; + return Ok(()); + } + + let picker_id = short_id(); + let selected = vec![false; items.len()]; + let view_text = build_picker_text(&items, &selected); + let kb = build_picker_keyboard(&picker_id, &selected); + let sent = bot.send_message(chat_id, view_text).reply_markup(kb).await?; + + let picker = PickerState { + id: picker_id.clone(), + chat_id: chat_id.0, + message_id: sent.id, + items, + selected, + }; + state.pickers.lock().await.insert(picker_id, picker); + Ok(()) +} + +async fn handle_list_callback( + bot: Bot, + q: CallbackQuery, + state: std::sync::Arc, +) -> Result<()> { + let Some(message) = q.message.clone() else { + return Ok(()); + }; + let Some(data) = q.data.as_deref() else { + return Ok(()); + }; + let mut parts = data.split(':'); + let _ = parts.next(); + let session_id = match parts.next() { + Some(id) => id.to_string(), + None => return Ok(()), + }; + let action = match parts.next() { + Some(action) => action, + None => return Ok(()), + }; + + let chat_id = message.chat.id.0; + let mut session = { + let mut sessions = state.sessions.lock().await; + let session = match sessions.remove(&chat_id) { + Some(session) => session, + None => { + bot.answer_callback_query(q.id).await?; + return Ok(()); + } + }; + if session.id != session_id { + sessions.insert(chat_id, session); + bot.answer_callback_query(q.id).await?; + return Ok(()); + } + session + }; + + match action { + "menu" => { + session.view = ListView::Menu; + } + "top" => { + let page = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0); + session.view = ListView::Peek { + mode: ListMode::Top, + page, + }; + } + "bottom" => { + let page = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0); + session.view = ListView::Peek { + mode: ListMode::Bottom, + page, + }; + } + "next" => { + if let ListView::Peek { mode, page } = session.view.clone() { + session.view = ListView::Peek { + mode, + page: page + 1, + }; + } + } + "prev" => { + if let ListView::Peek { mode, page } = session.view.clone() { + session.view = ListView::Peek { + mode, + page: page.saturating_sub(1), + }; + } + } + "back" => { + session.view = match session.view.clone() { + ListView::Selected { return_to, .. } => *return_to, + ListView::Peek { .. } => ListView::Menu, + other => other, + }; + } + "random" => { + if session.entries.is_empty() { + // Stay in place. + } else if session.seen_random.len() >= session.entries.len() { + // No unseen items left. + send_error(&bot, message.chat.id, "All items have been shown in this session.") + .await?; + } else { + let mut remaining: Vec = (0..session.entries.len()) + .filter(|i| !session.seen_random.contains(i)) + .collect(); + let mut rng = rand::thread_rng(); + remaining.shuffle(&mut rng); + if let Some(index) = remaining.first().copied() { + session.seen_random.insert(index); + let return_to = Box::new(session.view.clone()); + session.view = ListView::Selected { return_to, index }; + } + } + } + "pick" => { + if let ListView::Peek { mode, page } = session.view.clone() { + let pick_index = parts.next().and_then(|p| p.parse::().ok()); + if let Some(pick_index) = pick_index { + if let Some(entry_index) = peek_indices(session.entries.len(), mode, page) + .get(pick_index.saturating_sub(1)) + .copied() + { + let return_to = Box::new(ListView::Peek { mode, page }); + session.view = ListView::Selected { + return_to, + index: entry_index, + }; + } + } + } + } + "finish" => { + if let ListView::Selected { index, return_to } = session.view.clone() { + let entry_block = session.entries.get(index).map(|e| e.block_string()); + if let Some(entry_block) = entry_block { + let op = QueuedOp { + kind: QueuedOpKind::MoveToFinished, + entry: entry_block.clone(), + }; + match apply_user_op(&state, &op).await? { + UserOpOutcome::Applied(ApplyOutcome::Applied) => { + session.entries.remove(index); + session.view = *return_to; + normalize_peek_view(&mut session); + send_ephemeral(&bot, message.chat.id, "Moved.", ACK_TTL_SECS) + .await?; + let undo_id = add_undo(&state, UndoKind::MoveToFinished, entry_block) + .await?; + send_undo_message(&bot, message.chat.id, &undo_id).await?; + } + UserOpOutcome::Applied(ApplyOutcome::NotFound) => { + send_error(&bot, message.chat.id, "Item not found.").await?; + } + UserOpOutcome::Applied(ApplyOutcome::Duplicate) => {} + UserOpOutcome::Queued => { + send_error(&bot, message.chat.id, "Write failed; queued for retry.") + .await?; + } + } + } + } + } + "delete" => { + if let ListView::Selected { index, .. } = session.view.clone() { + let expires_at = now_ts() + DELETE_CONFIRM_TTL_SECS; + session.view = ListView::DeleteConfirm { + selected: Box::new(session.view.clone()), + index, + step: 1, + expires_at, + }; + } + } + "del1" => { + if let ListView::DeleteConfirm { + selected, + index, + step: _, + expires_at, + } = session.view.clone() + { + if now_ts() > expires_at { + session.view = *selected; + send_error(&bot, message.chat.id, "Delete confirmation expired.") + .await?; + } else { + session.view = ListView::DeleteConfirm { + selected, + index, + step: 2, + expires_at, + }; + } + } + } + "del2" => { + if let ListView::DeleteConfirm { + selected, + index, + step: _, + expires_at, + } = session.view.clone() + { + if now_ts() > expires_at { + session.view = *selected; + send_error(&bot, message.chat.id, "Delete confirmation expired.") + .await?; + } else { + let entry_block = session.entries.get(index).map(|e| e.block_string()); + if let Some(entry_block) = entry_block { + let op = QueuedOp { + kind: QueuedOpKind::Delete, + entry: entry_block.clone(), + }; + match apply_user_op(&state, &op).await? { + UserOpOutcome::Applied(ApplyOutcome::Applied) => { + session.entries.remove(index); + if let ListView::Selected { return_to, .. } = *selected { + session.view = *return_to; + } else { + session.view = ListView::Menu; + } + normalize_peek_view(&mut session); + send_ephemeral(&bot, message.chat.id, "Deleted.", ACK_TTL_SECS) + .await?; + let undo_id = add_undo(&state, UndoKind::Delete, entry_block) + .await?; + send_undo_message(&bot, message.chat.id, &undo_id).await?; + } + UserOpOutcome::Applied(ApplyOutcome::NotFound) => { + send_error(&bot, message.chat.id, "Item not found.").await?; + session.view = *selected; + } + UserOpOutcome::Applied(ApplyOutcome::Duplicate) => {} + UserOpOutcome::Queued => { + send_error( + &bot, + message.chat.id, + "Write failed; queued for retry.", + ) + .await?; + session.view = *selected; + } + } + } + } + } + } + "cancel_del" => { + if let ListView::DeleteConfirm { selected, .. } = session.view.clone() { + session.view = *selected; + } + } + _ => {} + } + + let (text, kb) = render_list_view(&session.id, &session); + state.sessions.lock().await.insert(chat_id, session); + bot.edit_message_text(message.chat.id, message.id, text) + .reply_markup(kb) + .await?; + bot.answer_callback_query(q.id).await?; + Ok(()) +} + +async fn handle_picker_callback( + bot: Bot, + q: CallbackQuery, + state: std::sync::Arc, +) -> Result<()> { + let Some(message) = q.message.clone() else { + return Ok(()); + }; + let Some(data) = q.data.as_deref() else { + return Ok(()); + }; + let mut parts = data.split(':'); + let _ = parts.next(); + let picker_id = match parts.next() { + Some(id) => id.to_string(), + None => return Ok(()), + }; + let action = match parts.next() { + Some(action) => action, + None => return Ok(()), + }; + + let mut picker = { + let mut pickers = state.pickers.lock().await; + let picker = match pickers.remove(&picker_id) { + Some(picker) => picker, + None => { + bot.answer_callback_query(q.id).await?; + return Ok(()); + } + }; + if picker.chat_id != message.chat.id.0 || picker.message_id != message.id { + pickers.insert(picker_id.clone(), picker); + bot.answer_callback_query(q.id).await?; + return Ok(()); + } + picker + }; + + let mut reinsert = false; + + match action { + "toggle" => { + if let Some(index) = parts.next().and_then(|p| p.parse::().ok()) { + if index < picker.selected.len() { + picker.selected[index] = !picker.selected[index]; + } + } + let text = build_picker_text(&picker.items, &picker.selected); + let kb = build_picker_keyboard(&picker.id, &picker.selected); + bot.edit_message_text(message.chat.id, message.id, text) + .reply_markup(kb) + .await?; + reinsert = true; + } + "add" => { + let selected_items: Vec = picker + .items + .iter() + .zip(picker.selected.iter()) + .filter_map(|(item, selected)| if *selected { Some(item.clone()) } else { None }) + .collect(); + if selected_items.is_empty() { + bot.answer_callback_query(q.id) + .text("Select at least one item.") + .await?; + return Ok(()); + } + + let mut added = 0usize; + let mut duplicates = 0usize; + let mut queued = false; + for item in selected_items { + let entry = EntryBlock::from_text(&item); + let op = QueuedOp { + kind: QueuedOpKind::Add, + entry: entry.block_string(), + }; + match apply_user_op(&state, &op).await? { + UserOpOutcome::Applied(ApplyOutcome::Applied) => added += 1, + UserOpOutcome::Applied(ApplyOutcome::Duplicate) => duplicates += 1, + UserOpOutcome::Applied(ApplyOutcome::NotFound) => {} + UserOpOutcome::Queued => queued = true, + } + } + + if queued { + send_error(&bot, message.chat.id, "Write failed; queued for retry.") + .await?; + } + + let summary = if duplicates > 0 { + format!("Saved {} item(s); {} duplicate(s) skipped.", added, duplicates) + } else { + format!("Saved {} item(s).", added) + }; + send_ephemeral(&bot, message.chat.id, &summary, ACK_TTL_SECS).await?; + bot.delete_message(message.chat.id, message.id).await?; + } + "cancel" => { + bot.delete_message(message.chat.id, message.id).await?; + } + _ => {} + } + + if reinsert { + state.pickers.lock().await.insert(picker_id, picker); + } + + bot.answer_callback_query(q.id).await?; + Ok(()) +} + +async fn handle_undo_callback( + bot: Bot, + q: CallbackQuery, + state: std::sync::Arc, +) -> Result<()> { + let Some(data) = q.data.as_deref() else { + return Ok(()); + }; + let undo_id = data.trim_start_matches("undo:"); + + let (record, undo_snapshot) = { + let mut undo = state.undo.lock().await; + prune_undo(&mut undo); + let pos = undo.iter().position(|r| r.id == undo_id); + let record = if let Some(pos) = pos { + Some(undo.remove(pos)) + } else { + None + }; + (record, undo.clone()) + }; + save_undo(&state.undo_path, &undo_snapshot)?; + + if let Some(record) = record { + let chat_id = chat_id_from_user_id(q.from.id.0); + if record.expires_at < now_ts() { + send_error(&bot, chat_id, "Undo expired.").await?; + bot.answer_callback_query(q.id).await?; + return Ok(()); + } + + let op = match record.kind { + UndoKind::MoveToFinished => QueuedOp { + kind: QueuedOpKind::MoveToReadLater, + entry: record.entry, + }, + UndoKind::Delete => QueuedOp { + kind: QueuedOpKind::Add, + entry: record.entry, + }, + }; + + match apply_user_op(&state, &op).await? { + UserOpOutcome::Applied(ApplyOutcome::Applied) + | UserOpOutcome::Applied(ApplyOutcome::Duplicate) + | UserOpOutcome::Applied(ApplyOutcome::NotFound) => { + send_ephemeral(&bot, chat_id, "Undone.", ACK_TTL_SECS).await?; + } + UserOpOutcome::Queued => { + send_error(&bot, chat_id, "Write failed; queued for retry.").await?; + } + } + } else { + send_error(&bot, chat_id_from_user_id(q.from.id.0), "Undo not found.").await?; + } + + bot.answer_callback_query(q.id).await?; + Ok(()) +} + +async fn apply_user_op(state: &std::sync::Arc, op: &QueuedOp) -> Result { + match apply_op(state, op).await { + Ok(outcome) => Ok(UserOpOutcome::Applied(outcome)), + Err(err) => { + error!("write failed: {:#}", err); + queue_op(state, op.clone()).await?; + Ok(UserOpOutcome::Queued) + } + } +} + +async fn apply_op(state: &std::sync::Arc, op: &QueuedOp) -> Result { + let _guard = state.write_lock.lock().await; + match op.kind { + QueuedOpKind::Add => { + let entry = EntryBlock::from_block(&op.entry); + let outcome = with_retries(|| add_entry_sync(&state.config.read_later_path, &entry)) + .await?; + Ok(match outcome { + AddOutcome::Added => ApplyOutcome::Applied, + AddOutcome::Duplicate => ApplyOutcome::Duplicate, + }) + } + QueuedOpKind::Delete => { + let outcome = with_retries(|| { + delete_entry_sync(&state.config.read_later_path, &op.entry) + }) + .await?; + Ok(match outcome { + ModifyOutcome::Applied => ApplyOutcome::Applied, + ModifyOutcome::NotFound => ApplyOutcome::NotFound, + }) + } + QueuedOpKind::MoveToFinished => { + let outcome = with_retries(|| { + move_to_finished_sync( + &state.config.read_later_path, + &state.config.finished_path, + &op.entry, + ) + }) + .await?; + Ok(match outcome { + ModifyOutcome::Applied => ApplyOutcome::Applied, + ModifyOutcome::NotFound => ApplyOutcome::NotFound, + }) + } + QueuedOpKind::MoveToReadLater => { + let outcome = with_retries(|| { + move_to_read_later_sync( + &state.config.read_later_path, + &state.config.finished_path, + &op.entry, + ) + }) + .await?; + Ok(match outcome { + ModifyOutcome::Applied => ApplyOutcome::Applied, + ModifyOutcome::NotFound => ApplyOutcome::NotFound, + }) + } + } +} + +#[derive(Debug)] +enum ApplyOutcome { + Applied, + Duplicate, + NotFound, +} + +enum UserOpOutcome { + Applied(ApplyOutcome), + Queued, +} + +async fn queue_op(state: &std::sync::Arc, op: QueuedOp) -> Result<()> { + let mut queue = state.queue.lock().await; + queue.push(op); + save_queue(&state.queue_path, &queue) +} + +fn split_items(text: &str) -> Vec { + text.split("---") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect() +} + +fn build_picker_text(items: &[String], selected: &[bool]) -> String { + let mut text = String::from("Select items to save:\n\n"); + for (idx, item) in items.iter().enumerate() { + let marker = if selected.get(idx).copied().unwrap_or(false) { + "[x]" + } else { + "[ ]" + }; + let preview = preview_text(item); + text.push_str(&format!("{} {}\n", idx + 1, marker)); + if let Some(first) = preview.get(0) { + text.push_str(&format!("{}\n", first)); + } + if let Some(second) = preview.get(1) { + text.push_str(&format!("{}\n", second)); + } + text.push('\n'); + } + text.trim_end().to_string() +} + +fn build_picker_keyboard(picker_id: &str, selected: &[bool]) -> InlineKeyboardMarkup { + let mut rows = Vec::new(); + for (idx, is_selected) in selected.iter().enumerate() { + let label = if *is_selected { + format!("{} [x]", idx + 1) + } else { + format!("{} [ ]", idx + 1) + }; + let data = format!("pick:{}:toggle:{}", picker_id, idx); + rows.push(vec![InlineKeyboardButton::callback(label, data)]); + } + rows.push(vec![ + InlineKeyboardButton::callback( + "Add selected", + format!("pick:{}:add", picker_id), + ), + InlineKeyboardButton::callback("Cancel", format!("pick:{}:cancel", picker_id)), + ]); + InlineKeyboardMarkup::new(rows) +} + +fn render_list_view(session_id: &str, session: &ListSession) -> (String, InlineKeyboardMarkup) { + match &session.view { + ListView::Menu => build_menu_view(session_id, session.entries.len()), + ListView::Peek { mode, page } => build_peek_view(session_id, session, *mode, *page), + ListView::Selected { index, .. } => build_selected_view(session_id, session, *index), + ListView::DeleteConfirm { step, index, .. } => { + build_delete_confirm_view(session_id, session, *index, *step) + } + } +} + +fn build_menu_view(session_id: &str, count: usize) -> (String, InlineKeyboardMarkup) { + let text = if count == 0 { + "Read Later is empty.".to_string() + } else { + "Choose Top, Bottom, or Random.".to_string() + }; + + let mut rows = Vec::new(); + if count > 0 { + rows.push(vec![ + InlineKeyboardButton::callback( + format!("Top ({})", count), + format!("ls:{}:top:0", session_id), + ), + InlineKeyboardButton::callback( + format!("Bottom ({})", count), + format!("ls:{}:bottom:0", session_id), + ), + ]); + rows.push(vec![InlineKeyboardButton::callback( + "Random", + format!("ls:{}:random", session_id), + )]); + } + + (text, InlineKeyboardMarkup::new(rows)) +} + +fn build_peek_view( + session_id: &str, + session: &ListSession, + mode: ListMode, + page: usize, +) -> (String, InlineKeyboardMarkup) { + let indices = peek_indices(session.entries.len(), mode, page); + let title = match mode { + ListMode::Top => "Top view", + ListMode::Bottom => "Bottom view", + }; + let mut text = format!("{} (page {})\n", title, page + 1); + if indices.is_empty() { + text.push_str("No items on this page."); + } else { + for (display_index, entry_index) in indices.iter().enumerate() { + if let Some(entry) = session.entries.get(*entry_index) { + let preview = entry.preview_lines(); + text.push_str(&format!("{}) ", display_index + 1)); + if let Some(first) = preview.get(0) { + text.push_str(first); + } + text.push('\n'); + if let Some(second) = preview.get(1) { + text.push_str(" "); + text.push_str(second); + text.push('\n'); + } + } + } + } + + let mut rows = Vec::new(); + if !indices.is_empty() { + let mut pick_row = Vec::new(); + for i in 0..indices.len() { + pick_row.push(InlineKeyboardButton::callback( + format!("{}", i + 1), + format!("ls:{}:pick:{}", session_id, i + 1), + )); + } + rows.push(pick_row); + } + + rows.push(vec![ + InlineKeyboardButton::callback("Prev", format!("ls:{}:prev", session_id)), + InlineKeyboardButton::callback("Next", format!("ls:{}:next", session_id)), + ]); + rows.push(vec![ + InlineKeyboardButton::callback("Back", format!("ls:{}:back", session_id)), + InlineKeyboardButton::callback("Random", format!("ls:{}:random", session_id)), + ]); + + (text.trim_end().to_string(), InlineKeyboardMarkup::new(rows)) +} + +fn build_selected_view( + session_id: &str, + session: &ListSession, + index: usize, +) -> (String, InlineKeyboardMarkup) { + let entry = session.entries.get(index); + let text = if let Some(entry) = entry { + let lines = entry.display_lines(); + format!("Selected item:\n\n{}", lines.join("\n")) + } else { + "Selected item not found.".to_string() + }; + + let rows = vec![ + vec![InlineKeyboardButton::callback( + "Mark Finished", + format!("ls:{}:finish", session_id), + )], + vec![InlineKeyboardButton::callback( + "Delete", + format!("ls:{}:delete", session_id), + )], + vec![InlineKeyboardButton::callback( + "Back", + format!("ls:{}:back", session_id), + )], + ]; + + (text, InlineKeyboardMarkup::new(rows)) +} + +fn build_delete_confirm_view( + session_id: &str, + session: &ListSession, + index: usize, + step: u8, +) -> (String, InlineKeyboardMarkup) { + let entry = session.entries.get(index); + let preview = entry.map(|e| e.preview_lines()).unwrap_or_default(); + let mut text = format!("Confirm delete ({}/2)?\n\n", step); + if let Some(first) = preview.get(0) { + text.push_str(first); + text.push('\n'); + } + if let Some(second) = preview.get(1) { + text.push_str(second); + text.push('\n'); + } + + let confirm_action = if step == 1 { "del1" } else { "del2" }; + let rows = vec![ + vec![InlineKeyboardButton::callback( + "Confirm", + format!("ls:{}:{}", session_id, confirm_action), + )], + vec![InlineKeyboardButton::callback( + "Cancel", + format!("ls:{}:cancel_del", session_id), + )], + ]; + + (text.trim_end().to_string(), InlineKeyboardMarkup::new(rows)) +} + +fn peek_indices(total: usize, mode: ListMode, page: usize) -> Vec { + if total == 0 { + return Vec::new(); + } + + match mode { + ListMode::Top => { + let start = page * 3; + if start >= total { + return Vec::new(); + } + let end = (start + 3).min(total); + (start..end).collect() + } + ListMode::Bottom => { + let end = total.saturating_sub(page * 3); + let start = end.saturating_sub(3); + if start >= end { + return Vec::new(); + } + (start..end).collect() + } + } +} + +fn normalize_peek_view(session: &mut ListSession) { + if let ListView::Peek { mode, page } = session.view.clone() { + let indices = peek_indices(session.entries.len(), mode, page); + if indices.is_empty() && page > 0 { + session.view = ListView::Peek { + mode, + page: page.saturating_sub(1), + }; + } + } +} + +fn preview_text(text: &str) -> Vec { + let normalized = normalize_line_endings(text); + let lines: Vec<&str> = normalized.lines().collect(); + let mut out = Vec::new(); + if let Some(first) = lines.get(0) { + out.push(first.to_string()); + } + if let Some(second) = lines.get(1) { + out.push(second.to_string()); + } + if lines.len() > 2 { + if let Some(last) = out.last_mut() { + last.push_str("..."); + } + } + out +} + +async fn send_ephemeral( + bot: &Bot, + chat_id: ChatId, + text: &str, + ttl_secs: u64, +) -> Result<()> { + let sent = bot.send_message(chat_id, text).await?; + let bot = bot.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(ttl_secs)).await; + let _ = bot.delete_message(chat_id, sent.id).await; + }); + Ok(()) +} + +async fn send_error(bot: &Bot, chat_id: ChatId, text: &str) -> Result<()> { + bot.send_message(chat_id, text).await?; + Ok(()) +} + +async fn send_undo_message(bot: &Bot, chat_id: ChatId, undo_id: &str) -> Result<()> { + let text = "Undo available for 30m."; + let kb = InlineKeyboardMarkup::new(vec![vec![InlineKeyboardButton::callback( + "Undo", + format!("undo:{}", undo_id), + )]]); + let sent = bot.send_message(chat_id, text).reply_markup(kb).await?; + let bot = bot.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(UNDO_TTL_SECS)).await; + let _ = bot.delete_message(chat_id, sent.id).await; + }); + Ok(()) +} + +async fn add_undo( + state: &std::sync::Arc, + kind: UndoKind, + entry: String, +) -> Result { + let mut undo = state.undo.lock().await; + prune_undo(&mut undo); + let id = short_id(); + undo.push(UndoRecord { + id: id.clone(), + kind, + entry, + expires_at: now_ts() + UNDO_TTL_SECS, + }); + save_undo(&state.undo_path, &undo)?; + Ok(id) +} + +async fn with_retries(mut f: F) -> Result +where + F: FnMut() -> Result, +{ + let mut last_err = None; + for attempt in 0..3 { + match f() { + Ok(value) => return Ok(value), + Err(err) => last_err = Some(err), + } + if attempt < 2 { + tokio::time::sleep(Duration::from_millis(200)).await; + } + } + Err(last_err.unwrap_or_else(|| anyhow!("retry failed"))) +} + +fn load_config(path: &Path) -> Result { + let contents = fs::read_to_string(path).with_context(|| format!("read config {}", path.display()))?; + let config: Config = toml::from_str(&contents).context("parse config")?; + Ok(config) +} + +fn read_entries(path: &Path) -> Result<(Vec, Vec)> { + if !path.exists() { + return Ok((Vec::new(), Vec::new())); + } + let contents = fs::read_to_string(path) + .with_context(|| format!("read file {}", path.display()))?; + let normalized = normalize_line_endings(&contents); + Ok(parse_entries(&normalized)) +} + +fn parse_entries(contents: &str) -> (Vec, Vec) { + let mut preamble = Vec::new(); + let mut entries: Vec = Vec::new(); + let mut current: Vec = Vec::new(); + let mut in_entries = false; + + for line in contents.lines() { + if line.starts_with('-') { + if in_entries && !current.is_empty() { + entries.push(EntryBlock { lines: current }); + current = Vec::new(); + } + in_entries = true; + current.push(line.to_string()); + } else if in_entries { + current.push(line.to_string()); + } else { + preamble.push(line.to_string()); + } + } + + if in_entries && !current.is_empty() { + entries.push(EntryBlock { lines: current }); + } + + (preamble, entries) +} + +fn write_entries(path: &Path, preamble: &[String], entries: &[EntryBlock]) -> Result<()> { + let mut lines: Vec = Vec::new(); + lines.extend_from_slice(preamble); + for entry in entries { + lines.extend(entry.lines.clone()); + } + let mut content = lines.join("\n"); + if !content.is_empty() { + content.push('\n'); + } + atomic_write(path, content.as_bytes()) +} + +fn atomic_write(path: &Path, data: &[u8]) -> Result<()> { + let dir = path + .parent() + .ok_or_else(|| anyhow!("no parent dir for {}", path.display()))?; + fs::create_dir_all(dir).with_context(|| format!("create dir {}", dir.display()))?; + let mut tmp = tempfile::NamedTempFile::new_in(dir) + .with_context(|| format!("create temp file in {}", dir.display()))?; + tmp.write_all(data).context("write temp file")?; + tmp.flush().context("flush temp file")?; + tmp.as_file_mut().sync_all().context("sync temp file")?; + tmp.persist(path) + .map_err(|e| anyhow!("persist temp file: {}", e))?; + Ok(()) +} + +fn add_entry_sync(path: &Path, entry: &EntryBlock) -> Result { + let (preamble, mut entries) = read_entries(path)?; + let block = entry.block_string(); + if entries.iter().any(|e| e.block_string() == block) { + return Ok(AddOutcome::Duplicate); + } + entries.insert(0, entry.clone()); + write_entries(path, &preamble, &entries)?; + Ok(AddOutcome::Added) +} + +fn delete_entry_sync(path: &Path, entry_block: &str) -> Result { + let (preamble, mut entries) = read_entries(path)?; + let pos = entries + .iter() + .position(|e| e.block_string() == entry_block); + let Some(pos) = pos else { + return Ok(ModifyOutcome::NotFound); + }; + entries.remove(pos); + write_entries(path, &preamble, &entries)?; + Ok(ModifyOutcome::Applied) +} + +fn move_to_finished_sync( + read_later: &Path, + finished: &Path, + entry_block: &str, +) -> Result { + let (preamble_rl, mut entries_rl) = read_entries(read_later)?; + let pos = entries_rl + .iter() + .position(|e| e.block_string() == entry_block); + let Some(pos) = pos else { + return Ok(ModifyOutcome::NotFound); + }; + let entry = entries_rl.remove(pos); + + let (preamble_fin, mut entries_fin) = read_entries(finished)?; + entries_fin.insert(0, entry); + write_entries(finished, &preamble_fin, &entries_fin)?; + write_entries(read_later, &preamble_rl, &entries_rl)?; + Ok(ModifyOutcome::Applied) +} + +fn move_to_read_later_sync( + read_later: &Path, + finished: &Path, + entry_block: &str, +) -> Result { + let (preamble_fin, mut entries_fin) = read_entries(finished)?; + let pos = entries_fin + .iter() + .position(|e| e.block_string() == entry_block); + let Some(pos) = pos else { + return Ok(ModifyOutcome::NotFound); + }; + let entry = entries_fin.remove(pos); + + let (preamble_rl, mut entries_rl) = read_entries(read_later)?; + entries_rl.insert(0, entry); + write_entries(read_later, &preamble_rl, &entries_rl)?; + write_entries(finished, &preamble_fin, &entries_fin)?; + Ok(ModifyOutcome::Applied) +} + +fn load_queue(path: &Path) -> Result> { + if !path.exists() { + return Ok(Vec::new()); + } + let data = fs::read_to_string(path).with_context(|| format!("read queue {}", path.display()))?; + let queue = serde_json::from_str(&data).context("parse queue")?; + Ok(queue) +} + +fn save_queue(path: &Path, queue: &[QueuedOp]) -> Result<()> { + let data = serde_json::to_vec_pretty(queue).context("serialize queue")?; + atomic_write(path, &data) +} + +fn load_undo(path: &Path) -> Result> { + if !path.exists() { + return Ok(Vec::new()); + } + let data = fs::read_to_string(path).with_context(|| format!("read undo {}", path.display()))?; + let undo = serde_json::from_str(&data).context("parse undo")?; + Ok(undo) +} + +fn save_undo(path: &Path, undo: &[UndoRecord]) -> Result<()> { + let data = serde_json::to_vec_pretty(undo).context("serialize undo")?; + atomic_write(path, &data) +} + +fn prune_undo(undo: &mut Vec) { + let now = now_ts(); + undo.retain(|r| r.expires_at > now); +} + +fn normalize_line_endings(input: &str) -> String { + input.replace("\r\n", "\n").replace('\r', "\n") +} + +fn parse_command(text: &str) -> Option<&str> { + let first = text.split_whitespace().next()?; + if !first.starts_with('/') { + return None; + } + let cmd = first.trim_start_matches('/'); + Some(cmd.split('@').next().unwrap_or(cmd)) +} + +fn short_id() -> String { + let id = Uuid::new_v4().to_string(); + id.split('-').next().unwrap_or(&id).to_string() +} + +fn now_ts() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)) + .as_secs() +} + +fn chat_id_from_user_id(user_id: u64) -> ChatId { + ChatId(user_id as i64) +} + +fn start_retry_loop(state: std::sync::Arc, interval_secs: u64) { + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(interval_secs)); + loop { + interval.tick().await; + if let Err(err) = process_queue(state.clone()).await { + error!("queue processing failed: {:#}", err); + } + } + }); +} + +async fn process_queue(state: std::sync::Arc) -> Result<()> { + let pending = { + let mut queue = state.queue.lock().await; + std::mem::take(&mut *queue) + }; + + if pending.is_empty() { + return Ok(()); + } + + let mut remaining = Vec::new(); + for op in pending { + match apply_op(&state, &op).await { + Ok(_) => {} + Err(err) => { + error!("queued op failed: {:#}", err); + remaining.push(op); + } + } + } + + let mut queue = state.queue.lock().await; + if !queue.is_empty() { + remaining.extend(queue.drain(..)); + } + *queue = remaining; + save_queue(&state.queue_path, &queue) +}