Overview
| Comment: | release 0.5.3 |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | release | v0.5.3 |
| Files: | files | file ages | folders |
| SHA3-256: |
7393d62235d7cc609f348435a928ab75 |
| User & Date: | arcade on 2026-01-07 07:40:58.691 |
| Other Links: | branch diff | manifest | tags |
Context
|
2026-01-09
| ||
| 09:57 | bump year, add data to Cargo.toml check-in: a2c4ae4717 user: arcade tags: trunk | |
|
2026-01-07
| ||
| 07:40 | release 0.5.3 Leaf check-in: 7393d62235 user: arcade tags: release, v0.5.3 | |
| 07:30 | number of small tweaks, use url crate to parse links, simplify and comment Closed-Leaf check-in: dc2089ff6a user: arcade tags: trunk | |
|
2026-01-06
| ||
| 12:04 | Create new branch named "release" check-in: b73681cf26 user: arcade tags: release | |
Changes
Added .github/dependabot.yml version [4aa461a712].
Added .github/workflows/rust-clippy.yml version [589584ce6a].
Modified Cargo.lock
from [e99dfc370d]
to [beed9b6e19].
| ︙ | ︙ | |||
275 276 277 278 279 280 281 | name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" | | | | 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 | name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ |
| ︙ | ︙ | |||
995 996 997 998 999 1000 1001 | "r-efi", "wasip2", "wasm-bindgen", ] [[package]] name = "h2" | | | | 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 | "r-efi", "wasip2", "wasm-bindgen", ] [[package]] name = "h2" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", "http", |
| ︙ | ︙ | |||
1158 1159 1160 1161 1162 1163 1164 | "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", | < | 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 | "hyper", "hyper-util", "rustls", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", ] [[package]] name = "hyper-util" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" |
| ︙ | ︙ | |||
1404 1405 1406 1407 1408 1409 1410 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ "spin", ] [[package]] name = "libc" | | | | 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ "spin", ] [[package]] name = "libc" version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "libm" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" |
| ︙ | ︙ | |||
1725 1726 1727 1728 1729 1730 1731 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" | | | | 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" version = "0.37.5" |
| ︙ | ︙ | |||
1800 1801 1802 1803 1804 1805 1806 | "socket2", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" | | | | 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 | "socket2", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" |
| ︙ | ︙ | |||
1921 1922 1923 1924 1925 1926 1927 | name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" | | | > > > > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | | | 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 | name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" dependencies = [ "base64", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls", "hyper-util", "js-sys", "log", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_json", "sync_wrapper", "tokio", "tokio-rustls", "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", ] [[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.16", "libc", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rsa" version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", |
| ︙ | ︙ | |||
2047 2048 2049 2050 2051 2052 2053 | "derive_builder", "never", "quick-xml", ] [[package]] name = "rsstg" | | | > | 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 | "derive_builder", "never", "quick-xml", ] [[package]] name = "rsstg" version = "0.5.3" dependencies = [ "async-compat", "atom_syndication", "chrono", "config", "futures", "futures-util", "lazy_static", "regex", "reqwest", "rss", "sedregex", "smol", "sqlx", "stacked_errors", "tgbot", "url", ] [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" |
| ︙ | ︙ | |||
2096 2097 2098 2099 2100 2101 2102 | "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" | | | | 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 | "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", "rustls-webpki", "subtle", |
| ︙ | ︙ | |||
2276 2277 2278 2279 2280 2281 2282 | "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" | | | | 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 | "proc-macro2", "quote", "syn", ] [[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", ] |
| ︙ | ︙ | |||
2698 2699 2700 2701 2702 2703 2704 | name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" | | | | 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 | name = "subtle" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[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]] |
| ︙ | ︙ | |||
2750 2751 2752 2753 2754 2755 2756 | dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "tgbot" | | | | | 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 | dependencies = [ "core-foundation-sys", "libc", ] [[package]] name = "tgbot" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0212214ba5db8a369e2853900248792d2b19737dda88ecabbb2e31ef85a1d752" dependencies = [ "async-stream", "bytes", "derive_more", "futures-util", "log", "mime", "mime_guess", "reqwest", "serde", "serde_json", "serde_with", "shellwords", "tokio", "tokio-util", ] |
| ︙ | ︙ | |||
2843 2844 2845 2846 2847 2848 2849 | name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" | | | | 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 | name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[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", "socket2", "windows-sys 0.61.2", |
| ︙ | ︙ | |||
2867 2868 2869 2870 2871 2872 2873 | dependencies = [ "rustls", "tokio", ] [[package]] name = "tokio-stream" | | | | | | 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 | dependencies = [ "rustls", "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", ] |
| ︙ | ︙ | |||
3016 3017 3018 3019 3020 3021 3022 | name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" | | | | 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 | name = "typenum" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" |
| ︙ | ︙ | |||
3055 3056 3057 3058 3059 3060 3061 | name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" | | | | 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 | name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[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", ] |
| ︙ | ︙ | |||
3216 3217 3218 3219 3220 3221 3222 | dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-root-certs" | | | | | | | 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 | dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "webpki-root-certs" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ "webpki-roots 1.0.5", ] [[package]] name = "webpki-roots" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" dependencies = [ "rustls-pki-types", ] [[package]] name = "whoami" version = "1.6.1" |
| ︙ | ︙ | |||
3664 3665 3666 3667 3668 3669 3670 | "quote", "syn", "synstructure", ] [[package]] name = "zerocopy" | | | | | | 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 | "quote", "syn", "synstructure", ] [[package]] name = "zerocopy" version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fabae64378cb18147bb18bca364e63bdbe72a0ffe4adf0addfec8aa166b2c56" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9c2d862265a8bb4471d87e033e730f536e2a285cc7cb05dbce09a2a97075f90" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] |
| ︙ | ︙ | |||
3744 3745 3746 3747 3748 3749 3750 | "proc-macro2", "quote", "syn", ] [[package]] name = "zmij" | | | | 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 | "proc-macro2", "quote", "syn", ] [[package]] name = "zmij" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" |
Modified Cargo.toml
from [deb55a55df]
to [df6a97a5f1].
1 2 | [package] name = "rsstg" | | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
[package]
name = "rsstg"
version = "0.5.3"
authors = ["arcade"]
edition = "2021"
[dependencies]
async-compat = "0.2.5"
atom_syndication = { version = "0.12.4", features = [ "with-serde" ] }
chrono = "0.4.38"
config = { version = "0.15", default-features = false, features = [ "toml" ] }
tgbot = "0.41"
futures = "0.3.30"
futures-util = "0.3.30"
lazy_static = "1.5.0"
regex = "1.10.6"
reqwest = { version = "0.13.1", features = [ "brotli", "socks", "deflate" ]}
rss = "2.0.9"
sedregex = "0.2.5"
smol = "2.0.2"
stacked_errors = "0.7.1"
sqlx = { version = "0.8", features = [ "postgres", "runtime-tokio-rustls", "chrono", "macros" ], default-features = false }
url = "2.5.8"
[profile.release]
lto = true
codegen-units = 1
|
Added README.md version [2ed5e5c1fa].
Modified rsstg.sql
from [6bfa596be6]
to [5ade9090d5].
| ︙ | ︙ | |||
18 19 20 21 22 23 24 |
create table rsstg_post (
source_id integer not null,
posted timestamptz not null,
url text not null,
hour smallint not null generated always as (extract('hour' from posted at time zone 'utc')) stored,
hxm smallint not null generated always as (hxm(posted)) stored,
| | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
create table rsstg_post (
source_id integer not null,
posted timestamptz not null,
url text not null,
hour smallint not null generated always as (extract('hour' from posted at time zone 'utc')) stored,
hxm smallint not null generated always as (hxm(posted)) stored,
FOREIGN KEY (source_id) REFERENCES rsstg_source(source_id) on delete cascade
);
create unique index rsstg_post__url on rsstg_post(url);
create index rsstg_post__hour on rsstg_post(hour);
create index rsstg_post__posted_hour on rsstg_post(posted,hour);
create index rsstg_post__hxm on rsstg_post(hxm);
create index rsstg_post__posted_hxm on rsstg_post(posted,hxm);
|
| ︙ | ︙ |
Modified src/command.rs
from [80a8df9d63]
to [f2ddddf16c].
| ︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 |
ChatMember,
ChatUsername,
GetChat,
GetChatAdministrators,
Message,
ParseMode::MarkdownV2,
};
lazy_static! {
static ref RE_USERNAME: Regex = Regex::new(r"^@([a-zA-Z][a-zA-Z0-9_]+)$").unwrap();
| > < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
ChatMember,
ChatUsername,
GetChat,
GetChatAdministrators,
Message,
ParseMode::MarkdownV2,
};
use url::Url;
lazy_static! {
static ref RE_USERNAME: Regex = Regex::new(r"^@([a-zA-Z][a-zA-Z0-9_]+)$").unwrap();
static ref RE_IV_HASH: Regex = Regex::new(r"^[a-f0-9]{14}$").unwrap();
}
pub async fn start (core: &Core, msg: &Message) -> Result<()> {
core.send("We are open\\. Probably\\. Visit [channel](https://t.me/rsstg_bot_help/3) for details\\.",
Some(msg.chat.get_id()), Some(MarkdownV2)).await.stack()?;
Ok(())
|
| ︙ | ︙ | |||
91 92 93 94 95 96 97 |
bail!("Usernames should be something like \"@\\[a\\-zA\\-Z]\\[a\\-zA\\-Z0\\-9\\_]+\", aren't they?\nNot {channel:?}");
},
};
*/
if ! RE_USERNAME.is_match(channel) {
bail!("Usernames should be something like \"@\\[a\\-zA\\-Z]\\[a\\-zA\\-Z0\\-9\\_]+\", aren't they?\nNot {channel:?}");
};
| > > > | > > | > > | 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
bail!("Usernames should be something like \"@\\[a\\-zA\\-Z]\\[a\\-zA\\-Z0\\-9\\_]+\", aren't they?\nNot {channel:?}");
},
};
*/
if ! RE_USERNAME.is_match(channel) {
bail!("Usernames should be something like \"@\\[a\\-zA\\-Z]\\[a\\-zA\\-Z0\\-9\\_]+\", aren't they?\nNot {channel:?}");
};
{
let parsed_url = Url::parse(url)
.stack_err("Expecting a valid link to ATOM/RSS feed.")?;
match parsed_url.scheme() {
"http" | "https" => {},
scheme => {
bail!("Unsupported URL scheme: {scheme}");
},
};
}
let iv_hash = match iv_hash {
Some(hash) => {
match hash.as_ref() {
"-" => None,
thing => {
if ! RE_IV_HASH.is_match(thing) {
|
| ︙ | ︙ |
Modified src/core.rs
from [dbaaa2430e]
to [170e5288e9].
| ︙ | ︙ | |||
51 52 53 54 55 56 57 |
bail,
};
lazy_static!{
pub static ref RE_SPECIAL: Regex = Regex::new(r"([\-_*\[\]()~`>#+|{}\.!])").unwrap();
}
| > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > > > > > > > | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
bail,
};
lazy_static!{
pub static ref RE_SPECIAL: Regex = Regex::new(r"([\-_*\[\]()~`>#+|{}\.!])").unwrap();
}
/// Escape characters that are special in Telegram MarkdownV2 by prefixing them with a backslash.
///
/// This ensures the returned string can be used as MarkdownV2-formatted Telegram message content
/// without special characters being interpreted as MarkdownV2 markup.
pub fn encode (text: &str) -> Cow<'_, str> {
RE_SPECIAL.replace_all(text, "\\$1")
}
// This one does nothing except making sure only one token exists for each id
pub struct Token {
running: Arc<Mutex<HashSet<i32>>>,
my_id: i32,
}
impl Token {
/// Attempts to acquire a per-id token by inserting `my_id` into the shared `running` set.
///
/// If the id was not already present, the function inserts it and returns `Some(Token)`.
/// When the returned `Token` is dropped, the id will be removed from the `running` set,
/// allowing subsequent acquisitions for the same id.
///
/// # Parameters
///
/// - `running`: Shared set tracking active ids.
/// - `my_id`: Identifier to acquire a token for.
///
/// # Returns
///
/// `Ok(Token)` if the id was successfully acquired, `Error` if a token for the id is already active.
async fn new (running: &Arc<Mutex<HashSet<i32>>>, my_id: i32) -> Result<Token> {
let running = running.clone();
let mut set = running.lock_arc().await;
if set.contains(&my_id) {
bail!("Token already taken");
} else {
set.insert(my_id);
Ok(Token {
running,
my_id,
})
}
}
}
impl Drop for Token {
/// Releases this token's claim on the shared running-set when the token is dropped.
///
/// The token's identifier is removed from the shared `running` set so that future
/// operations for the same id may proceed.
///
/// TODO: is using block_on inside block_on safe? Currently tested and working fine.
fn drop (&mut self) {
smol::block_on(async {
let mut set = self.running.lock_arc().await;
set.remove(&self.my_id);
})
}
}
#[derive(Clone)]
pub struct Core {
owner_chat: ChatPeerId,
// max_delay: u16,
pub tg: Client,
pub me: Bot,
pub db: Db,
running: Arc<Mutex<HashSet<i32>>>,
http_client: reqwest::Client,
}
pub struct Post {
uri: String,
title: String,
authors: String,
summary: String,
}
impl Core {
/// Create a Core instance from configuration and start its background autofetch loop.
///
/// The provided `settings` must include:
/// - `owner` (integer): chat id to use as the default destination,
/// - `api_key` (string): Telegram bot API key,
/// - `api_gateway` (string): Telegram API gateway host,
/// - `pg` (string): PostgreSQL connection string,
/// - optional `proxy` (string): proxy URL for the HTTP client.
///
/// On success returns an initialized `Core` with Telegram and HTTP clients, database connection,
/// an empty running set for per-id tokens, and a spawned background task that periodically runs
/// `autofetch`. If any required setting is missing or initialization fails, an error is returned.
pub async fn new(settings: config::Config) -> Result<Core> {
let owner_chat = ChatPeerId::from(settings.get_int("owner").stack()?);
let api_key = settings.get_string("api_key").stack()?;
let tg = Client::new(&api_key).stack()?
.with_host(settings.get_string("api_gateway").stack()?);
let mut client = reqwest::Client::builder();
if let Ok(proxy) = settings.get_string("proxy") {
let proxy = reqwest::Proxy::all(proxy).stack()?;
client = client.proxy(proxy);
}
let http_client = client.build().stack()?;
let me = tg.execute(GetBot).await.stack()?;
let core = Core {
tg,
me,
owner_chat,
db: Db::new(&settings.get_string("pg").stack()?)?,
running: Arc::new(Mutex::new(HashSet::new())),
http_client,
// max_delay: 60,
};
let clone = core.clone();
smol::spawn(Compat::new(async move {
loop {
let delay = match &clone.autofetch().await {
|
| ︙ | ︙ | |||
127 128 129 130 131 132 133 134 135 136 137 |
let target = target.unwrap_or(self.owner_chat);
self.tg.execute(
SendMessage::new(target, msg)
.with_parse_mode(mode)
).await.stack()
}
pub async fn check (&self, id: i32, real: bool, last_scrape: Option<DateTime<Local>>) -> Result<String> {
let mut posted: i32 = 0;
let mut conn = self.db.begin().await.stack()?;
| > > > > > > > > > > > > > > > > < < < < < | < < < < < < < | | | | | | | | | | | | | > | | | | | | | | | | | | | | | | | > > > > > > > > > > | | | | | | | | | | | | | | | | | | | | > > > > > > | > > | | | | | | | | | | | | | | | | | | | | | | | | | | < | | | | | | | | < < < > | 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
let target = target.unwrap_or(self.owner_chat);
self.tg.execute(
SendMessage::new(target, msg)
.with_parse_mode(mode)
).await.stack()
}
/// Fetches the feed for a source, sends any newly discovered posts to the appropriate chat, and records them in the database.
///
/// This acquires a per-source guard to prevent concurrent checks for the same `id`. If a check is already running for
/// the given `id`, the function returns an error. If `last_scrape` is provided, it is sent as the `If-Modified-Since`
/// header to the feed request. The function parses RSS or Atom feeds, sends unseen post URLs to either the source's
/// channel (when `real` is true) or the source owner (when `real` is false), and persists posted entries so they are
/// not reposted later.
///
/// Parameters:
/// - `id`: Identifier of the source to check.
/// - `real`: When `true`, send posts to the source's channel; when `false`, send to the source owner.
/// - `last_scrape`: Optional timestamp used to set the `If-Modified-Since` header for the HTTP request.
///
/// # Returns
///
/// `Posted: N` where `N` is the number of posts processed and sent.
pub async fn check (&self, id: i32, real: bool, last_scrape: Option<DateTime<Local>>) -> Result<String> {
let mut posted: i32 = 0;
let mut conn = self.db.begin().await.stack()?;
let _token = Token::new(&self.running, id).await.stack()?;
let source = conn.get_source(id, self.owner_chat).await.stack()?;
conn.set_scrape(id).await.stack()?;
let destination = ChatPeerId::from(match real {
true => source.channel_id,
false => source.owner,
});
let mut this_fetch: Option<DateTime<chrono::FixedOffset>> = None;
let mut posts: BTreeMap<DateTime<chrono::FixedOffset>, Post> = BTreeMap::new();
let mut builder = self.http_client.get(&source.url);
if let Some(last_scrape) = last_scrape {
builder = builder.header(LAST_MODIFIED, last_scrape.to_rfc2822());
};
let response = builder.send().await.stack()?;
#[cfg(debug_assertions)]
{
let headers = response.headers();
let expires = headers.get(EXPIRES);
let cache = headers.get(CACHE_CONTROL);
if expires.is_some() || cache.is_some() {
println!("{} {} {:?} {:?} {:?}", Local::now().to_rfc2822(), &source.url, last_scrape, expires, cache);
}
}
let status = response.status();
let content = response.bytes().await.stack()?;
match rss::Channel::read_from(&content[..]) {
Ok(feed) => {
for item in feed.items() {
if let Some(link) = item.link() {
let date = match item.pub_date() {
Some(feed_date) => DateTime::parse_from_rfc2822(feed_date),
None => DateTime::parse_from_rfc3339(match item.dublin_core_ext() {
Some(ext) => {
let dates = ext.dates();
if dates.is_empty() {
bail!("Feed item has Dublin Core extension but no dates.")
} else {
&dates[0]
}
},
None => bail!("Feed item misses posting date."),
}),
}.stack()?;
let uri = link.to_string();
let title = item.title().unwrap_or("").to_string();
let authors = item.author().unwrap_or("").to_string();
let summary = item.content().unwrap_or("").to_string();
posts.insert(date, Post{
uri,
title,
authors,
summary,
});
}
};
},
Err(err) => match err {
rss::Error::InvalidStartTag => {
match atom_syndication::Feed::read_from(&content[..]) {
Ok(feed) => {
for item in feed.entries() {
let date = item.published()
.stack_err("Feed item missing publishing date.")?;
let uri = {
let links = item.links();
if links.is_empty() {
bail!("Feed item missing post links.");
} else {
links[0].href().to_string()
}
};
let title = item.title().to_string();
let authors = item.authors().iter().map(|x| format!("{} <{:?}>", x.name(), x.email())).collect::<Vec<String>>().join(", ");
let summary = if let Some(sum) = item.summary() { sum.value.clone() } else { String::new() };
posts.insert(*date, Post{
uri,
title,
authors,
summary,
});
};
},
Err(err) => {
bail!("Unsupported or mangled content:\n{:?}\n{err}\n{status:#?}\n", &source.url)
},
}
},
rss::Error::Eof => (),
_ => bail!("Unsupported or mangled content:\n{:?}\n{err}\n{status:#?}\n", &source.url)
}
};
for (date, post) in posts.iter() {
let post_url: Cow<str> = match source.url_re {
Some(ref x) => sedregex::ReplaceCommand::new(x).stack()?.execute(&post.uri),
None => post.uri.clone().into(),
};
if ! conn.exists(&post_url, id).await.stack()? {
if this_fetch.is_none() || *date > this_fetch.unwrap() {
this_fetch = Some(*date);
};
self.send( match &source.iv_hash {
Some(hash) => format!("<a href=\"https://t.me/iv?url={post_url}&rhash={hash}\"> </a>{post_url}"),
None => format!("{post_url}"),
}, Some(destination), Some(ParseMode::Html)).await.stack()?;
conn.add_post(id, date, &post_url).await.stack()?;
posted += 1;
};
};
posts.clear();
Ok(format!("Posted: {posted}"))
}
async fn autofetch(&self) -> Result<std::time::Duration> {
let mut delay = chrono::Duration::minutes(1);
let now = chrono::Local::now();
let queue = {
|
| ︙ | ︙ | |||
259 260 261 262 263 264 265 |
owner_chat: ChatPeerId::from(owner),
..self.clone()
};
let source = {
let mut conn = self.db.begin().await.stack()?;
match conn.get_one(owner, source_id).await {
Ok(Some(source)) => source.to_string(),
| | | | 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 |
owner_chat: ChatPeerId::from(owner),
..self.clone()
};
let source = {
let mut conn = self.db.begin().await.stack()?;
match conn.get_one(owner, source_id).await {
Ok(Some(source)) => source.to_string(),
Ok(None) => "Source not found in database?".to_string(),
Err(err) => format!("Failed to fetch source data:\n{err}"),
}
};
smol::spawn(Compat::new(async move {
if let Err(err) = clone.check(source_id, true, Some(last_scrape)).await {
if let Err(err) = clone.send(&format!("🛑 {source}\n{}", encode(&err.to_string())), None, Some(ParseMode::MarkdownV2)).await {
eprintln!("Check error: {err}");
// clone.disable(&source_id, owner).await.unwrap();
};
};
})).detach();
}
} else if next_fetch - now < delay {
|
| ︙ | ︙ |
Modified src/main.rs
from [3cb558ff1b]
to [527f3dd3d4].
| ︙ | ︙ | |||
11 12 13 14 15 16 17 |
use stacked_errors::{
Result,
StackableErr,
};
use tgbot::handler::LongPoll;
fn main () -> Result<()> {
| | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
use stacked_errors::{
Result,
StackableErr,
};
use tgbot::handler::LongPoll;
fn main () -> Result<()> {
smol::block_on(Compat::new(async {
async_main().await.unwrap();
}));
Ok(())
}
async fn async_main () -> Result<()> {
|
| ︙ | ︙ |
Modified src/sql.rs
from [6bfecdc0d2]
to [ccfc867118].
| ︙ | ︙ | |||
146 147 148 149 150 151 152 |
.execute(&mut *self.0).await.stack()?.rows_affected() {
1 => { Ok("Source enabled.") },
0 => { Ok("Source not found.") },
_ => { bail!("Database error.") },
}
}
| | | | > > | | > > | 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
.execute(&mut *self.0).await.stack()?.rows_affected() {
1 => { Ok("Source enabled.") },
0 => { Ok("Source not found.") },
_ => { bail!("Database error.") },
}
}
pub async fn exists <I> (&mut self, post_url: &str, id: I) -> Result<bool>
where I: Into<i64> {
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.into())
.fetch_one(&mut *self.0).await.stack()?;
if let Some(exists) = row.try_get("exists").stack()? {
Ok(exists)
} else {
bail!("Database error: can't check whether source exists.");
}
}
/// Get all pending events for (now + 1 minute)
pub async fn get_queue (&mut self) -> Result<Vec<Queue>> {
let block: Vec<Queue> = sqlx::query_as("select source_id, next_fetch, owner, last_scrape from rsstg_order natural left join rsstg_source where next_fetch < now() + interval '1 minute';")
.fetch_all(&mut *self.0).await.stack()?;
Ok(block)
}
pub async fn get_list <I> (&mut self, owner: I) -> Result<Vec<List>>
|
| ︙ | ︙ |