Index: Cargo.lock ================================================================== --- Cargo.lock +++ Cargo.lock @@ -66,24 +66,10 @@ name = "anyhow" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" -[[package]] -name = "aquamarine" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" -dependencies = [ - "include_dir", - "itertools", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "async-attributes" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" @@ -115,13 +101,13 @@ "pin-project-lite", ] [[package]] name = "async-compression" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "brotli", "flate2", "futures-core", "memchr", @@ -247,10 +233,21 @@ [[package]] name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] [[package]] name = "atoi" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -346,27 +343,52 @@ "async-task", "futures-io", "futures-lite 2.6.0", "piper", ] + +[[package]] +name = "bon" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced38439e7a86a4761f7f7d5ded5ff009135939ecb464a24452eaa4c1696af7d" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce61d2d3844c6b8d31b2353d9f66cf5e632b3e9549583fe3cac2f4f6136725e" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.100", +] [[package]] name = "brotli" -version = "7.0.0" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "cf19e729cdbd51af9a397fb9ef8ac8378007b797f8273cfbfdf45dcaa316167b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", "brotli-decompressor", ] [[package]] name = "brotli-decompressor" -version = "4.0.3" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] @@ -374,16 +396,10 @@ name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" -[[package]] -name = "bytemuck" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" - [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" @@ -394,13 +410,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.19" +version = "1.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362" +checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" dependencies = [ "shlex", ] [[package]] @@ -575,20 +591,10 @@ "const-oid", "pem-rfc7468", "zeroize", ] -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", - "serde", -] - [[package]] name = "derive_builder" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" @@ -616,31 +622,10 @@ dependencies = [ "derive_builder_core", "syn 2.0.100", ] -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.100", - "unicode-xid", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" @@ -675,19 +660,10 @@ name = "dotenvy" version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[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" @@ -708,20 +684,10 @@ 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.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" @@ -837,10 +803,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] + +[[package]] +name = "frankenstein" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb8f97b9b2fefaa34071c0ccb9df53d7d15d5c4a375c07cd701e133dac048af" +dependencies = [ + "async-trait", + "bon", + "macro_rules_attribute", + "paste", + "reqwest", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.12", + "tokio", +] [[package]] name = "futures" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -978,13 +962,13 @@ "version_check", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -1033,23 +1017,17 @@ "bytes", "fnv", "futures-core", "futures-sink", "http", - "indexmap 2.9.0", + "indexmap", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" @@ -1063,11 +1041,11 @@ name = "hashlink" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" dependencies = [ - "hashbrown 0.15.2", + "hashbrown", ] [[package]] name = "heck" version = "0.5.0" @@ -1400,49 +1378,18 @@ dependencies = [ "icu_normalizer", "icu_properties", ] -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - [[package]] name = "indexmap" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", - "serde", + "hashbrown", ] [[package]] name = "instant" version = "0.1.13" @@ -1467,19 +1414,10 @@ name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" @@ -1518,13 +1456,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libm" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72" [[package]] name = "libsqlite3-sys" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1575,10 +1513,26 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" dependencies = [ "value-bag", ] +[[package]] +name = "macro_rules_attribute" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a82271f7bc033d84bbca59a3ce3e4159938cb08a9c3aebbe54d215131518a13" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dd856d451cc0da70e2ef2ce95a18e39a93b7558bedf10201ad28503f918568" + [[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" @@ -1667,16 +1621,10 @@ "rand 0.8.5", "smallvec", "zeroize", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" @@ -1791,10 +1739,16 @@ "redox_syscall", "smallvec", "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" @@ -1812,30 +1766,10 @@ name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[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.100", -] - [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" @@ -1913,16 +1847,10 @@ "rustix 0.38.44", "tracing", "windows-sys 0.59.0", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" @@ -1929,28 +1857,17 @@ dependencies = [ "zerocopy", ] [[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", +name = "prettyplease" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +dependencies = [ + "proc-macro2", + "syn 2.0.100", ] [[package]] name = "proc-macro2" version = "1.0.95" @@ -1958,19 +1875,10 @@ checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] -[[package]] -name = "psm" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58e5423e24c18cc840e1c98370b3993c6649cd1678b4d24318bcf0a083cbe88" -dependencies = [ - "cc", -] - [[package]] name = "quick-xml" version = "0.37.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" @@ -1999,13 +1907,13 @@ "web-time", ] [[package]] name = "quinn-proto" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" dependencies = [ "bytes", "getrandom 0.3.2", "rand 0.9.1", "ring", @@ -2093,11 +2001,11 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.16", ] [[package]] name = "rand_core" version = "0.9.3" @@ -2105,19 +2013,10 @@ checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.2", ] -[[package]] -name = "rc-box" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897fecc9fac6febd4408f9e935e86df739b0023b625e610e0357535b9c8adad0" -dependencies = [ - "erasable", -] - [[package]] name = "redox_syscall" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" @@ -2206,28 +2105,19 @@ "web-sys", "webpki-roots", "windows-registry", ] -[[package]] -name = "rgb" -version = "0.8.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" -dependencies = [ - "bytemuck", -] - [[package]] name = "ring" version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", ] @@ -2263,26 +2153,26 @@ "quick-xml", ] [[package]] name = "rsstg" -version = "0.3.0" +version = "0.3.2" dependencies = [ "anyhow", "async-std", "atom_syndication", "chrono", "config", + "frankenstein", "futures", "futures-util", "lazy_static", "regex", "reqwest", "rss", "sedregex", "sqlx", - "teloxide", "thiserror 2.0.12", ] [[package]] name = "rustc-demangle" @@ -2495,20 +2385,13 @@ name = "serde_with" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ - "base64", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.9.0", "serde", "serde_derive", - "serde_json", "serde_with_macros", - "time", ] [[package]] name = "serde_with_macros" version = "3.12.0" @@ -2547,19 +2430,10 @@ 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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" @@ -2655,13 +2529,13 @@ "event-listener 5.4.0", "futures-core", "futures-intrusive", "futures-io", "futures-util", - "hashbrown 0.15.2", + "hashbrown", "hashlink", - "indexmap 2.9.0", + "indexmap", "log", "memchr", "once_cell", "percent-encoding", "rustls", @@ -2824,23 +2698,10 @@ name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "stacker" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601f9201feb9b09c00266478bf459952b9ef9a6b94edb2f21eba14ab681a60a9" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - [[package]] name = "stringprep" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" @@ -2923,92 +2784,10 @@ 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.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cb7c03a9217286fe11021dc72d5a674acbb0d3b24ba38d04f7efe7920e4948" -dependencies = [ - "aquamarine", - "bytes", - "derive_more", - "dptree", - "either", - "futures", - "log", - "mime", - "pin-project", - "serde", - "serde_json", - "teloxide-core", - "teloxide-macros", - "thiserror 2.0.12", - "tokio", - "tokio-stream", - "tokio-util", - "url", -] - -[[package]] -name = "teloxide-core" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2f70a3cd58c2b31ca899691b99573a40c6da713ab230bb78bbb4fb0b5c751a" -dependencies = [ - "bitflags 2.9.0", - "bytes", - "chrono", - "derive_more", - "either", - "futures", - "log", - "mime", - "once_cell", - "pin-project", - "rc-box", - "reqwest", - "rgb", - "serde", - "serde_json", - "serde_with", - "stacker", - "take_mut", - "takecell", - "thiserror 2.0.12", - "tokio", - "tokio-util", - "url", - "uuid", -] - -[[package]] -name = "teloxide-macros" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3118a980ed2ec11f73d9495a6606905bd74726e3ffe95a42fbeb187ded8fdbf4" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "tempfile" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" @@ -3058,41 +2837,10 @@ "proc-macro2", "quote", "syn 2.0.100", ] -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" @@ -3125,11 +2873,10 @@ "backtrace", "bytes", "libc", "mio", "pin-project-lite", - "signal-hook-registry", "socket2 0.5.9", "windows-sys 0.52.0", ] [[package]] @@ -3162,26 +2909,15 @@ "futures-util", "thiserror 1.0.69", "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", @@ -3188,36 +2924,36 @@ "tokio", ] [[package]] name = "toml" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +checksum = "900f6c86a685850b1bc9f6223b20125115ee3f31e01207d81655bbcc0aea9231" dependencies = [ "serde", "serde_spanned", "toml_datetime", "toml_edit", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.24" +version = "0.22.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +checksum = "10558ed0bd2a1562e630926a2d1f0b98c827da99fabd3fe20920a59642504485" dependencies = [ - "indexmap 2.9.0", + "indexmap", "serde", "serde_spanned", "toml_datetime", "winnow", ] @@ -3324,16 +3060,10 @@ name = "unicode-properties" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" @@ -3345,11 +3075,10 @@ checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde", ] [[package]] name = "utf16_iter" version = "1.0.5" @@ -3360,19 +3089,10 @@ name = "utf8_iter" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" -[[package]] -name = "uuid" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" -dependencies = [ - "getrandom 0.3.2", -] - [[package]] name = "value-bag" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" @@ -3861,13 +3581,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" dependencies = [ "memchr", ] [[package]] @@ -3915,22 +3635,22 @@ "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.24" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", "syn 2.0.100", ] Index: Cargo.toml ================================================================== --- Cargo.toml +++ Cargo.toml @@ -1,27 +1,26 @@ [package] name = "rsstg" -version = "0.3.0" +version = "0.3.2" authors = ["arcade"] edition = "2021" [dependencies] anyhow = "1.0.86" async-std = { version = "1.12.0", features = [ "attributes", "tokio1" ] } atom_syndication = { version = "0.12.4", features = [ "with-serde" ] } chrono = "0.4.38" config = { version = "0.15", default-features = false, features = [ "toml" ] } +frankenstein = { version = "0.40.0", features = [ "client-reqwest" ] } futures = "0.3.30" futures-util = "0.3.30" lazy_static = "1.5.0" regex = "1.10.6" reqwest = { version = "0.12.7", features = [ "brotli", "socks", "deflate" ]} rss = "2.0.9" sedregex = "0.2.5" sqlx = { version = "0.8", features = [ "postgres", "runtime-async-std-rustls", "chrono", "macros" ], default-features = false } -#telegram-bot = { git = "https://github.com/kworr/telegram-bot", features = [ "rustls" ], default-features = false } -teloxide = { version = "0.15.0", default-features = false, features = [ "ctrlc_handler", "macros", "rustls" ] } thiserror = "2.0.0" [profile.release] lto = true codegen-units = 1 Index: rsstg.sql ================================================================== --- rsstg.sql +++ rsstg.sql @@ -8,11 +8,12 @@ channel_id integer not null, url text not null, last_scrape not null timestamptz default now(), enabled boolean not null default true, iv_hash text, - owner bigint not null); + owner bigint not null, + url_re text); create unique index rsstg_source__source_id on rsstg_source(source_id); create unique index rsstg_source__channel_id__owner on rsstg_source(channel_id, owner); create index rsstg_source__owner on rsstg_source(owner); create table rsstg_post ( Index: src/command.rs ================================================================== --- src/command.rs +++ src/command.rs @@ -1,74 +1,60 @@ use crate::core::Core; +use std::borrow::Cow; + use anyhow::{ bail, Context, Result +}; +use frankenstein::{ + methods::{ + GetChatAdministratorsParams, + GetChatParams, + }, + types::{ + ChatId, + ChatMember, + }, + AsyncTelegramApi, + ParseMode, }; use lazy_static::lazy_static; use regex::Regex; use sedregex::ReplaceCommand; -use std::borrow::Cow; -use teloxide::{ - Bot, - dispatching::dialogue::GetChatId, - payloads::GetChatAdministrators, - requests::{ - Requester, - ResponseResult - }, - types::{ - Message, - UserId, - }, - utils::command::BotCommands, -}; lazy_static! { static ref RE_USERNAME: Regex = Regex::new(r"^@[a-zA-Z][a-zA-Z0-9_]+$").unwrap(); static ref RE_LINK: Regex = Regex::new(r"^https?://[a-zA-Z.0-9-]+/[-_a-zA-Z.:0-9/?=]+$").unwrap(); static ref RE_IV_HASH: Regex = Regex::new(r"^[a-f0-9]{14}$").unwrap(); } -#[derive(BotCommands, Clone)] -#[command(rename_rule = "lowercase", description = "Supported commands:")] -enum Command { - #[command(description = "display this help.")] - Help, - #[command(description = "Does nothing.")] - Start, - #[commant(description = "List active subscriptions.")] - List, -} - -pub async fn cmd_handler(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> { - match cmd { - Command::Help => bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?, - Command::Start => bot.send_message(msg.chat.id, - "We are open. Probably. Visit [channel](https://t.me/rsstg_bot_help/3) for details.").await?, - Command::List => bot.send_message(msg.chat.id, core.list(msg.from).await?).await?, - }; - Ok(()) -} - -pub async fn list(core: &Core, sender: UserId) -> Result<()> { - core.send(core.list(sender).await?, Some(sender), Some(telegram_bot::types::ParseMode::MarkdownV2)).await?; - Ok(()) -} - -pub async fn command(core: &Core, sender: telegram_bot::UserId, command: Vec<&str>) -> Result<()> { +pub async fn start(core: &Core, chat_id: i64) -> Result<()> { + core.send("We are open\\. Probably\\. Visit [channel](https://t.me/rsstg_bot_help/3) for details\\.", + Some(chat_id), Some(ParseMode::MarkdownV2)).await?; + Ok(()) +} + +pub async fn list(core: &mut Core, sender: i64) -> Result<()> { + let msg = core.list(sender).await?; + core.send(msg, Some(sender), Some(ParseMode::MarkdownV2)).await?; + Ok(()) +} + +pub async fn command(core: &mut Core, sender: i64, command: Vec<&str>) -> Result<()> { + let mut conn = core.db.begin().await?; if command.len() >= 2 { let msg: Cow = match &command[1].parse::() { Err(err) => format!("I need a number.\n{}", &err).into(), Ok(number) => match command[0] { "/check" => core.check(number, sender, false).await - .context("Channel check failed.")?, - "/clean" => core.clean(number, sender).await?, - "/enable" => core.enable(number, sender).await?.into(), - "/delete" => core.delete(number, sender).await?, - "/disable" => core.disable(number, sender).await?.into(), + .context("Channel check failed.")?.into(), + "/clean" => conn.clean(*number, sender).await?, + "/enable" => conn.enable(*number, sender).await?.into(), + "/delete" => conn.delete(*number, sender).await?, + "/disable" => conn.disable(*number, sender).await?.into(), _ => bail!("Command {} not handled.", &command[0]), }, }; core.send(msg, Some(sender), None).await?; } else { @@ -75,42 +61,42 @@ core.send("This command needs a number.", Some(sender), None).await?; } Ok(()) } -pub async fn update(core: &Core, sender: telegram_bot::UserId, command: Vec<&str>) -> Result<()> { +pub async fn update(core: &mut Core, sender: i64, command: Vec<&str>) -> Result<()> { let mut source_id: Option = None; let at_least = "Requires at least 3 parameters."; let mut i_command = command.iter(); let first_word = i_command.next().context(at_least)?; match *first_word { "/update" => { let next_word = i_command.next().context(at_least)?; source_id = Some(next_word.parse::() - .context(format!("I need a number, but got {}.", next_word))?); + .context(format!("I need a number, but got {next_word}."))?); }, "/add" => {}, - _ => bail!("Passing {} is not possible here.", first_word), + _ => bail!("Passing {first_word} is not possible here."), }; let (channel, url, iv_hash, url_re) = ( i_command.next().context(at_least)?, i_command.next().context(at_least)?, i_command.next(), i_command.next()); if ! RE_USERNAME.is_match(channel) { - bail!("Usernames should be something like \"@\\[a\\-zA\\-Z]\\[a\\-zA\\-Z0\\-9\\_]+\", aren't they?\nNot {:?}", &channel); + bail!("Usernames should be something like \"@\\[a\\-zA\\-Z]\\[a\\-zA\\-Z0\\-9\\_]+\", aren't they?\nNot {channel:?}"); }; if ! RE_LINK.is_match(url) { - bail!("Link should be a link to atom/rss feed, something like \"https://domain/path\".\nNot {:?}", &url); + bail!("Link should be a link to atom/rss feed, something like \"https://domain/path\".\nNot {url:?}"); } let iv_hash = match iv_hash { Some(hash) => { match *hash { "-" => None, thing => { if ! RE_IV_HASH.is_match(thing) { - bail!("IV hash should be 14 hex digits.\nNot {:?}", thing); + bail!("IV hash should be 14 hex digits.\nNot {thing:?}"); }; Some(thing) }, } }, @@ -126,22 +112,32 @@ } } }, None => None, }; - let channel_id = i64::from(core.request(telegram_bot::GetChat::new(telegram_bot::ChatRef::ChannelUsername(channel.to_string()))).await?.id()); - let chan_adm = core.request(telegram_bot::GetChatAdministrators::new(telegram_bot::ChatRef::ChannelUsername(channel.to_string()))).await - .context("Sorry, I have no access to that chat.")?; + let chat_id = ChatId::String((*channel).into()); + let channel_id = core.tg.get_chat(&GetChatParams { chat_id: chat_id.clone() }).await?.result.id; + let chan_adm = core.tg.get_chat_administrators(&GetChatAdministratorsParams { chat_id }).await + .context("Sorry, I have no access to that chat.")?.result; let (mut me, mut user) = (false, false); for admin in chan_adm { - if admin.user.id == core.my.id { + let member_id = match admin { + ChatMember::Creator(member) => member.user.id, + ChatMember::Administrator(member) => member.user.id, + ChatMember::Left(_) + | ChatMember::Kicked(_) + | ChatMember::Member(_) + | ChatMember::Restricted(_) => continue, + } as i64; + if member_id == core.me.id as i64 { me = true; }; - if admin.user.id == sender { + if member_id == sender { user = true; }; }; if ! me { bail!("I need to be admin on that channel."); }; if ! user { bail!("You should be admin on that channel."); }; - core.send(core.update(source_id, channel, channel_id, url, iv_hash, url_re, sender).await?, Some(sender), None).await?; + let mut conn = core.db.begin().await?; + core.send(conn.update(source_id, channel, channel_id, url, iv_hash, url_re, sender).await?, Some(sender), None).await?; Ok(()) } Index: src/core.rs ================================================================== --- src/core.rs +++ src/core.rs @@ -1,19 +1,9 @@ -use anyhow::{anyhow, bail, Context, Result}; -use async_std::task; -use chrono::DateTime; -use sqlx::postgres::PgPoolOptions; -use teloxide::{ - Bot, - payloads::SendMessage, - requests::Requester, - types::{ - Me, - UserId, - }, +use crate::{ + command, + sql::Db, }; -use thiserror::Error; use std::{ borrow::Cow, collections::{ BTreeMap, @@ -24,10 +14,33 @@ Arc, Mutex }, }; +use anyhow::{ + bail, + Result, +}; +use async_std::task; +use chrono::DateTime; +use frankenstein::{ + client_reqwest::Bot, + methods::{ + GetUpdatesParams, + SendMessageParams + }, + types::{ + AllowedUpdate, + MessageEntityType, + User, + }, + updates::UpdateContent, + AsyncTelegramApi, + ParseMode, +}; +use thiserror::Error; + #[derive(Error, Debug)] pub enum RssError { // #[error(transparent)] // Tg(#[from] TgError), #[error(transparent)] @@ -34,107 +47,126 @@ Int(#[from] TryFromIntError), } #[derive(Clone)] pub struct Core { - owner_chat: UserId, + owner_chat: i64, pub tg: Bot, - pub my: Me, - pool: sqlx::Pool, + pub me: User, + pub db: Db, sources: Arc>>>, http_client: reqwest::Client, } impl Core { - pub fn new(settings: config::Config) -> Result> { - let owner: u64 = settings.get_int("owner")?.try_into()?; + pub async fn new(settings: config::Config) -> Result { + let owner_chat = settings.get_int("owner")?; let api_key = settings.get_string("api_key")?; - let tg = Bot::new(api_key); - let tg_cloned = tg.clone(); + let tg = Bot::new(&api_key); let mut client = reqwest::Client::builder(); if let Ok(proxy) = settings.get_string("proxy") { let proxy = reqwest::Proxy::all(proxy)?; client = client.proxy(proxy); } let http_client = client.build()?; - let core = Arc::new(Core { + let me = tg.get_me().await?; + let me = me.result; + let core = Core { tg, - my: task::block_on(async { - tg_cloned.get_me().await - })?, - owner_chat: UserId(owner), - pool: PgPoolOptions::new() - .max_connections(5) - .acquire_timeout(std::time::Duration::new(300, 0)) - .idle_timeout(std::time::Duration::new(60, 0)) - .connect_lazy(&settings.get_string("pg")?)?, + me, + owner_chat, + db: Db::new(&settings.get_string("pg")?)?, sources: Arc::new(Mutex::new(HashSet::new())), http_client, - }); - /* let clone = core.clone(); + }; + let mut clone = core.clone(); task::spawn(async move { loop { let delay = match &clone.autofetch().await { Err(err) => { - if let Err(err) = clone.send(format!("šŸ›‘ {:?}", err), None, None).await { - eprintln!("Autofetch error: {}", err); + if let Err(err) = clone.send(format!("šŸ›‘ {err:?}"), None, None).await { + eprintln!("Autofetch error: {err:?}"); }; std::time::Duration::from_secs(60) }, Ok(time) => *time, }; task::sleep(delay).await; } - }); */ + }); Ok(core) } - pub fn stream(&self) -> Result<()> { - let mut last_update: Option = None; - loop { - let updates = self.tg.get_updates(last_update, None, 300, Some(vec!["message"])); - } - Ok(()) - } - - /* - pub async fn send<'a, S>(&self, msg: S, target: Option, mode: Option) -> Result<()> - where S: Into> { - let mode = mode.unwrap_or(telegram_bot::types::ParseMode::Html); - let target = target.unwrap_or(self.owner_chat); - self.request(telegram_bot::SendMessage::new(target, msg).parse_mode(mode)).await?; - Ok(()) - } */ - - /* pub async fn request (&self, req: Req) -> Result<::Type, RssError> { - loop { - let res = self.tg.send(&req).await; - match res { - Ok(_) => return Ok(res?), - Err(err) => { - match &err { - TgError::Raw(TgrError::TelegramError { description: _, parameters: Some(params) }) => { - if let Some(delay) = params.retry_after { - println!("Throttled, waiting {} senconds.", delay); - task::sleep(std::time::Duration::from_secs(delay.try_into()?)).await; - } else { - return Err(err.into()); - } - }, - _ => return Err(err.into()), - } - }, - }; - } - } */ - - /* pub async fn check(&self, id: &i32, owner: S, real: bool) -> Result> - where S: Into { - let owner = owner.into(); - let mut posted: i32 = 0; - let mut conn = self.pool.acquire().await?; + pub async fn stream(&mut self) -> Result<()> { + let mut offset: i64 = 0; + let mut params = GetUpdatesParams { + offset: None, + limit: Some(100), + timeout: Some(300), + allowed_updates: Some(vec![AllowedUpdate::Message]), + }; + loop { + let updates = self.tg.get_updates(¶ms).await?.result; + if updates.is_empty() { + offset = 0; + params.offset = None; + continue; + } + for update in updates { + if i64::from(update.update_id) >= offset { + offset = i64::from(update.update_id) + 1; + params.offset = Some(offset); + } + if let UpdateContent::Message(msg) = update.content { + if let Some(text) = msg.text { + if let Some(entities) = msg.entities { + let chars: Vec = text.encode_utf16().collect(); + for entity in entities { + if entity.type_field == MessageEntityType::BotCommand && entity.offset != 0 { + bail!("commands should be at message start"); + }; + let cmd = String::from_utf16_lossy(&chars[entity.offset as usize..entity.length as usize]); + let words: Vec<&str> = text.split_whitespace().collect(); + match cmd.as_ref() { + "/check" | "/clean" | "/enable" | "/delete" | "/disable" => { command::command(self, msg.chat.id, words).await? }, + "/start" => { command::start(self, msg.chat.id).await?; }, + "/list" => { command::list(self, msg.chat.id).await?; }, + "/add" | "/update" => { command::update(self, msg.chat.id, words).await?; }, + any => { + self.send(format!("\\#error\n```\nUnknown command: {any}\n```"), + Some(msg.chat.id), + Some(ParseMode::MarkdownV2) + ).await?; + }, + }; + }; + }; + }; + }; + } + } + } + + pub async fn send (&self, msg: S, target: Option, mode: Option) -> Result<()> + where S: Into { + let msg = msg.into(); + + let mode = mode.unwrap_or(ParseMode::Html); + let target = target.unwrap_or(self.owner_chat); + let send = SendMessageParams::builder() + .chat_id(target) + .text(msg) + .parse_mode(mode) + .build(); + self.tg.send_message(&send).await?; + Ok(()) + } + + pub async fn check (&mut self, id: &i32, owner: i64, real: bool) -> Result { + let mut posted: i32 = 0; + let mut conn = self.db.begin().await?; let id = { let mut set = self.sources.lock().unwrap(); match set.get(id) { Some(id) => id.clone(), @@ -145,15 +177,15 @@ }, } }; let count = Arc::strong_count(&id); if count == 2 { - let source = sqlx::query!("select source_id, channel_id, url, iv_hash, owner, url_re from rsstg_source where source_id = $1 and owner = $2", - *id, owner).fetch_one(&mut *conn).await?; + let source = conn.get_source(*id, owner).await?; + conn.set_scrape(*id).await?; let destination = match real { - true => telegram_bot::UserId::new(source.channel_id), - false => telegram_bot::UserId::new(source.owner), + true => source.channel_id, + false => source.owner, }; let mut this_fetch: Option> = None; let mut posts: BTreeMap, String> = BTreeMap::new(); let response = self.http_client.get(&source.url).send().await?; @@ -181,148 +213,58 @@ let url = item.links()[0].href(); posts.insert(*date, url.to_string()); }; }, Err(err) => { - bail!("Unsupported or mangled content:\n{:?}\n{:#?}\n{:#?}\n", &source.url, err, status) + bail!("Unsupported or mangled content:\n{:?}\n{err:#?}\n{status:#?}\n", &source.url) }, } }, rss::Error::Eof => (), - _ => bail!("Unsupported or mangled content:\n{:?}\n{:#?}\n{:#?}\n", &source.url, err, status) + _ => bail!("Unsupported or mangled content:\n{:?}\n{err:#?}\n{status:#?}\n", &source.url) } }; for (date, url) in posts.iter() { let post_url: Cow = match source.url_re { Some(ref x) => sedregex::ReplaceCommand::new(x)?.execute(url), None => url.into(), }; - if let Some(exists) = sqlx::query!("select exists(select true from rsstg_post where url = $1 and source_id = $2) as exists;", - &post_url, *id).fetch_one(&mut *conn).await?.exists { + if let Some(exists) = conn.exists(&post_url, *id).await? { if ! exists { if this_fetch.is_none() || *date > this_fetch.unwrap() { this_fetch = Some(*date); }; - self.request( match &source.iv_hash { - Some(hash) => telegram_bot::SendMessage::new(destination, format!(" {0}", &post_url, hash)), - None => telegram_bot::SendMessage::new(destination, format!("{}", post_url)), - }.parse_mode(telegram_bot::types::ParseMode::Html)).await - .context("Can't post message:")?; - sqlx::query!("insert into rsstg_post (source_id, posted, url) values ($1, $2, $3);", - *id, date, &post_url).execute(&mut *conn).await?; + self.send( match &source.iv_hash { + Some(hash) => format!(" {post_url}"), + None => format!("{post_url}"), + }, Some(destination), Some(ParseMode::Html)).await?; + conn.add_post(*id, date, &post_url).await?; }; }; posted += 1; }; posts.clear(); }; - sqlx::query!("update rsstg_source set last_scrape = now() where source_id = $1;", - *id).execute(&mut *conn).await?; - Ok(format!("Posted: {}", &posted).into()) - } */ - - /* pub async fn delete(&self, source_id: &i32, owner: S) -> Result> - where S: Into { - let owner = owner.into(); - - match sqlx::query!("delete from rsstg_source where source_id = $1 and owner = $2;", - source_id, owner).execute(&mut *self.pool.acquire().await?).await?.rows_affected() { - 0 => { Ok("No data found found.".into()) }, - x => { Ok(format!("{} sources removed.", x).into()) }, - } - } */ - - /* pub async fn clean(&self, source_id: &i32, owner: S) -> Result> - where S: Into { - let owner = owner.into(); - - match sqlx::query!("delete from rsstg_post p using rsstg_source s where p.source_id = $1 and owner = $2 and p.source_id = s.source_id;", - source_id, owner).execute(&mut *self.pool.acquire().await?).await?.rows_affected() { - 0 => { Ok("No data found found.".into()) }, - x => { Ok(format!("{} posts purged.", x).into()) }, - } - } */ - - /* pub async fn enable(&self, source_id: &i32, owner: S) -> Result<&str> - where S: Into { - let owner = owner.into(); - - match sqlx::query!("update rsstg_source set enabled = true where source_id = $1 and owner = $2", - source_id, owner).execute(&mut *self.pool.acquire().await?).await?.rows_affected() { - 1 => { Ok("Source enabled.") }, - 0 => { Ok("Source not found.") }, - _ => { Err(anyhow!("Database error.")) }, - } - } */ - - /* pub async fn disable(&self, source_id: &i32, owner: S) -> Result<&str> - where S: Into { - let owner = owner.into(); - - match sqlx::query!("update rsstg_source set enabled = false where source_id = $1 and owner = $2", - source_id, owner).execute(&mut *self.pool.acquire().await?).await?.rows_affected() { - 1 => { Ok("Source disabled.") }, - 0 => { Ok("Source not found.") }, - _ => { Err(anyhow!("Database error.")) }, - } - } */ - - /* pub async fn update(&self, update: Option, channel: &str, channel_id: i64, url: &str, iv_hash: Option<&str>, url_re: Option<&str>, owner: S) -> Result<&str> - where S: Into { - let owner = owner.into(); - let mut conn = self.pool.acquire().await?; - - match match update { - Some(id) => { - sqlx::query!("update rsstg_source set channel_id = $2, url = $3, iv_hash = $4, owner = $5, channel = $6, url_re = $7 where source_id = $1", - id, channel_id, url, iv_hash, owner, channel, url_re).execute(&mut *conn).await - }, - None => { - sqlx::query!("insert into rsstg_source (channel_id, url, iv_hash, owner, channel, url_re) values ($1, $2, $3, $4, $5, $6)", - channel_id, url, iv_hash, owner, channel, url_re).execute(&mut *conn).await - }, - } { - Ok(_) => Ok(match update { - Some(_) => "Channel updated.", - None => "Channel added.", - }), - Err(sqlx::Error::Database(err)) => { - match err.downcast::().routine() { - Some("_bt_check_unique", ) => { - Ok("Duplicate key.") - }, - Some(_) => { - Ok("Database error.") - }, - None => { - Ok("No database error extracted.") - }, - } - }, - Err(err) => { - bail!("Sorry, unknown error:\n{:#?}\n", err); - }, - } - } - - async fn autofetch(&self) -> Result { - let mut delay = chrono::Duration::minutes(1); - let now = chrono::Local::now(); - let mut queue = sqlx::query!(r#"select source_id, next_fetch as "next_fetch: DateTime", owner from rsstg_order natural left join rsstg_source where next_fetch < now() + interval '1 minute';"#) - .fetch_all(&mut *self.pool.acquire().await?).await?; - for row in queue.iter() { + Ok(format!("Posted: {posted}")) + } + + async fn autofetch(&mut self) -> Result { + let mut delay = chrono::Duration::minutes(1); + let now = chrono::Local::now(); + let mut conn = self.db.begin().await?; + for row in conn.get_queue().await? { if let Some(next_fetch) = row.next_fetch { if next_fetch < now { if let (Some(owner), Some(source_id)) = (row.owner, row.source_id) { - let clone = Core { - owner_chat: telegram_bot::UserId::new(owner), + let mut clone = Core { + owner_chat: owner, ..self.clone() }; task::spawn(async move { if let Err(err) = clone.check(&source_id, owner, true).await { - if let Err(err) = clone.send(&format!("šŸ›‘ {:?}", err), None, None).await { - dbg!("Check error: {}", err); + if let Err(err) = clone.send(&format!("šŸ›‘ {err:?}"), None, None).await { + eprintln!("Check error: {err:?}"); // clone.disable(&source_id, owner).await.unwrap(); }; }; }); } @@ -329,33 +271,28 @@ } else if next_fetch - now < delay { delay = next_fetch - now; } } }; - queue.clear(); Ok(delay.to_std()?) } - pub async fn list(&self, owner: S) -> Result - where S: Into { - let owner = owner.into(); - + pub async fn list (&mut self, owner: i64) -> Result { let mut reply: Vec> = vec![]; reply.push("Channels:".into()); - let rows = sqlx::query!("select source_id, channel, enabled, url, iv_hash, url_re from rsstg_source where owner = $1 order by source_id", - owner).fetch_all(&mut *self.pool.acquire().await?).await?; - for row in rows.iter() { + let mut conn = self.db.begin().await?; + for row in conn.get_list(owner).await? { reply.push(format!("\n\\#ļøāƒ£ {} \\*ļøāƒ£ `{}` {}\nšŸ”— `{}`", row.source_id, row.channel, match row.enabled { true => "šŸ”„ enabled", false => "ā›” disabled", }, row.url).into()); if let Some(hash) = &row.iv_hash { - reply.push(format!("IV: `{}`", hash).into()); + reply.push(format!("IV: `{hash}`").into()); } if let Some(re) = &row.url_re { - reply.push(format!("RE: `{}`", re).into()); + reply.push(format!("RE: `{re}`").into()); } }; Ok(reply.join("\n")) - } */ + } } Index: src/main.rs ================================================================== --- src/main.rs +++ src/main.rs @@ -3,60 +3,21 @@ #![warn(missing_docs)] mod command; mod core; +mod sql; use anyhow::Result; -use async_std::task; -use async_std::stream::StreamExt; #[async_std::main] async fn main() -> Result<()> { let settings = config::Config::builder() .add_source(config::File::with_name("rsstg")) .build()?; - let core = core::Core::new(settings)?; - - let mut stream = core.stream(); - stream.allowed_updates(&[telegram_bot::AllowedUpdate::Message]); - - task::block_on(async { - let mut reply_to: Option; - loop { - reply_to = None; - match stream.next().await { - Some(update) => { - if let Err(err) = handle(update?, &core, &reply_to).await { - core.send(&format!("šŸ›‘ {err:?}"), reply_to, None).await?; - }; - }, - None => { - core.send("šŸ›‘ None error.", None, None).await?; - } - }; - } - }) -} - -async fn handle(update: telegram_bot::Update, core: &core::Core, mut _reply_to: &Option) -> Result<()> { - if let telegram_bot::UpdateKind::Message(message) = update.kind { - if let Some(from) = message.from { - if let telegram_bot::MessageKind::Text{ ref data, .. } = message.kind { - let sender = from.id; - let words: Vec<&str> = data.split_whitespace().collect(); - if let Err(err) = match words[0] { - "/check" | "/clean" | "/enable" | "/delete" | "/disable" => command::command(core, sender, words).await, - "/start" => command::start(core, sender).await, - "/list" => command::list(core, sender).await, - "/add" | "/update" => command::update(core, sender, words).await, - _ => Ok(()), - } { - core.send(format!("šŸ›‘ {:?}", err), Some(sender), None).await?; - }; - }; - }; - }; + let mut core = core::Core::new(settings).await?; + + core.stream().await?; Ok(()) } ADDED src/sql.rs Index: src/sql.rs ================================================================== --- /dev/null +++ src/sql.rs @@ -0,0 +1,207 @@ +use std::borrow::Cow; + +use anyhow::{ + Result, + bail, +}; +use chrono::{ + DateTime, + FixedOffset, + Local, +}; +use sqlx::{ + Pool, + Postgres, + Row, + postgres::PgPoolOptions, + pool::PoolConnection, +}; + +#[derive(sqlx::FromRow, Debug)] +pub struct List { + pub source_id: i32, + pub channel: String, + pub enabled: bool, + pub url: String, + pub iv_hash: Option, + pub url_re: Option, +} + +#[derive(sqlx::FromRow, Debug)] +pub struct Source { + pub channel_id: i64, + pub url: String, + pub iv_hash: Option, + pub owner: i64, + pub url_re: Option, +} + +#[derive(sqlx::FromRow)] +pub struct Queue { + pub source_id: Option, + pub next_fetch: Option>, + pub owner: Option, +} + +#[derive(Clone)] +pub struct Db { + pool: sqlx::Pool, +} + +pub struct Conn{ + conn: PoolConnection, +} + +impl Db { + pub fn new (pguri: &str) -> Result { + Ok(Db{ + pool: PgPoolOptions::new() + .max_connections(5) + .acquire_timeout(std::time::Duration::new(300, 0)) + .idle_timeout(std::time::Duration::new(60, 0)) + .connect_lazy(pguri)?, + }) + } + + pub async fn begin(&mut self) -> Result { + Conn::new(&mut self.pool).await + } +} + +impl Conn { + pub async fn new (pool: &mut Pool) -> Result { + let conn = pool.acquire().await?; + Ok(Conn{ + conn, + }) + } + + pub async fn add_post (&mut self, id: i32, date: &DateTime, post_url: &str) -> Result<()> { + sqlx::query("insert into rsstg_post (source_id, posted, url) values ($1, $2, $3);") + .bind(id) + .bind(date) + .bind(post_url) + .execute(&mut *self.conn).await?; + Ok(()) + } + + pub async fn clean (&mut self, source_id: i32, owner: i64) -> Result> { + match sqlx::query("delete from rsstg_post p using rsstg_source s where p.source_id = $1 and owner = $2 and p.source_id = s.source_id;") + .bind(source_id) + .bind(owner) + .execute(&mut *self.conn).await?.rows_affected() { + 0 => { Ok("No data found found.".into()) }, + x => { Ok(format!("{x} posts purged.").into()) }, + } + } + + pub async fn delete (&mut self, source_id: i32, owner: i64) -> Result> { + match sqlx::query("delete from rsstg_source where source_id = $1 and owner = $2;") + .bind(source_id) + .bind(owner) + .execute(&mut *self.conn).await?.rows_affected() { + 0 => { Ok("No data found found.".into()) }, + x => { Ok(format!("{} sources removed.", x).into()) }, + } + } + + pub async fn disable (&mut self, source_id: i32, owner: i64) -> Result<&str> { + match sqlx::query("update rsstg_source set enabled = false where source_id = $1 and owner = $2") + .bind(source_id) + .bind(owner) + .execute(&mut *self.conn).await?.rows_affected() { + 1 => { Ok("Source disabled.") }, + 0 => { Ok("Source not found.") }, + _ => { bail!("Database error.") }, + } + } + + pub async fn enable (&mut self, source_id: i32, owner: i64) -> Result<&str> { + match sqlx::query("update rsstg_source set enabled = true where source_id = $1 and owner = $2") + .bind(source_id) + .bind(owner) + .execute(&mut *self.conn).await?.rows_affected() { + 1 => { Ok("Source enabled.") }, + 0 => { Ok("Source not found.") }, + _ => { bail!("Database error.") }, + } + } + + pub async fn exists (&mut self, post_url: &str, id: i32) -> Result> { + let row = sqlx::query("select exists(select true from rsstg_post where url = $1 and source_id = $2) as exists;") + .bind(post_url) + .bind(id) + .fetch_one(&mut *self.conn).await?; + let exists: Option = row.try_get("exists")?; + Ok(exists) + } + + pub async fn get_queue (&mut self) -> Result> { + let block: Vec = sqlx::query_as("select source_id, next_fetch, owner from rsstg_order natural left join rsstg_source where next_fetch < now() + interval '1 minute';") + .fetch_all(&mut *self.conn).await?; + Ok(block) + } + + pub async fn get_list (&mut self, owner: i64) -> Result> { + let source: Vec = sqlx::query_as("select source_id, channel, enabled, url, iv_hash, url_re from rsstg_source where owner = $1 order by source_id") + .bind(owner) + .fetch_all(&mut *self.conn).await?; + Ok(source) + } + + pub async fn get_source (&mut self, id: i32, owner: i64) -> Result { + let source: Source = sqlx::query_as("select channel_id, url, iv_hash, owner, url_re from rsstg_source where source_id = $1 and owner = $2") + .bind(id) + .bind(owner) + .fetch_one(&mut *self.conn).await?; + Ok(source) + } + + pub async fn set_scrape (&mut self, id: i32) -> Result<()> { + sqlx::query("update rsstg_source set last_scrape = now() where source_id = $1;") + .bind(id) + .execute(&mut *self.conn).await?; + Ok(()) + } + + pub async fn update (&mut self, update: Option, channel: &str, channel_id: i64, url: &str, iv_hash: Option<&str>, url_re: Option<&str>, owner: i64) -> Result<&str> { + match match update { + Some(id) => { + sqlx::query("update rsstg_source set channel_id = $2, url = $3, iv_hash = $4, owner = $5, channel = $6, url_re = $7 where source_id = $1") + .bind(id) + }, + None => { + sqlx::query("insert into rsstg_source (channel_id, url, iv_hash, owner, channel, url_re) values ($1, $2, $3, $4, $5, $6)") + }, + } + .bind(channel_id) + .bind(url) + .bind(iv_hash) + .bind(owner) + .bind(channel) + .bind(url_re) + .execute(&mut *self.conn).await + { + Ok(_) => Ok(match update { + Some(_) => "Channel updated.", + None => "Channel added.", + }), + Err(sqlx::Error::Database(err)) => { + match err.downcast::().routine() { + Some("_bt_check_unique", ) => { + Ok("Duplicate key.") + }, + Some(_) => { + Ok("Database error.") + }, + None => { + Ok("No database error extracted.") + }, + } + }, + Err(err) => { + bail!("Sorry, unknown error:\n{err:#?}\n"); + }, + } + } +}