Check-in [44575a91d3]
Logged in as anonymous
Overview
Comment:bump and switch error handling
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk | v0.4.5
Files: files | file ages | folders
SHA3-256: 44575a91d308920b8ec6701e94661c5098c2349f97379bb741ea52a35beecd39
User & Date: arcade on 2025-07-09 05:35:41.274
Other Links: manifest | tags
Context
2025-08-05
06:26
bump, reformat errors Leaf check-in: 07b34bcad6 user: arcade tags: trunk, v0.4.6
2025-07-09
05:35
bump and switch error handling check-in: 44575a91d3 user: arcade tags: trunk, v0.4.5
2025-07-01
11:13
tweak logging, simplify connection objects, use async_std primitives check-in: c6d3e97290 user: arcade tags: trunk, v0.4.4
Changes
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
58
59
60
61
62
63
64






65
66
67
68
69
70
71







-
-
-
-
-
-







version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
 "libc",
]

[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"

[[package]]
name = "async-attributes"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5"
dependencies = [
 "quote",
87
88
89
90
91
92
93
94

95
96

97
98
99
100
101
102
103
81
82
83
84
85
86
87

88
89

90
91
92
93
94
95
96
97







-
+

-
+







 "concurrent-queue",
 "event-listener 2.5.3",
 "futures-core",
]

[[package]]
name = "async-channel"
version = "2.3.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
 "concurrent-queue",
 "event-listener-strategy",
 "futures-core",
 "pin-project-lite",
]

131
132
133
134
135
136
137
138

139
140
141
142
143
144
145
125
126
127
128
129
130
131

132
133
134
135
136
137
138
139







-
+








[[package]]
name = "async-global-executor"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
dependencies = [
 "async-channel 2.3.1",
 "async-channel 2.5.0",
 "async-executor",
 "async-io 2.4.1",
 "async-lock 3.4.0",
 "blocking",
 "futures-lite 2.6.0",
 "once_cell",
 "tokio",
343
344
345
346
347
348
349
350

351
352

353
354

355
356
357
358
359
360
361
337
338
339
340
341
342
343

344
345

346
347

348
349
350
351
352
353
354
355







-
+

-
+

-
+







checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
 "generic-array",
]

[[package]]
name = "blocking"
version = "1.6.1"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
 "async-channel 2.3.1",
 "async-channel 2.5.0",
 "async-task",
 "futures-io",
 "futures-lite 2.6.0",
 "piper",
]

[[package]]
395
396
397
398
399
400
401
402

403
404

405
406
407
408
409
410
411
389
390
391
392
393
394
395

396
397

398
399
400
401
402
403
404
405







-
+

-
+







name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"

[[package]]
name = "cc"
version = "1.2.27"
version = "1.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
dependencies = [
 "shlex",
]

[[package]]
name = "cfg-if"
version = "1.0.1"
440
441
442
443
444
445
446
447

448
449

450
451
452
453
454
455
456
434
435
436
437
438
439
440

441
442

443
444
445
446
447
448
449
450







-
+

-
+







checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
 "crossbeam-utils",
]

[[package]]
name = "config"
version = "0.15.11"
version = "0.15.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80"
checksum = "9baeea16b4f8fc242a701d2abacd87d3b024af0325fb0b59dd16bc14c214c2af"
dependencies = [
 "pathdiff",
 "serde",
 "toml",
 "winnow",
]

1177
1178
1179
1180
1181
1182
1183
1184

1185
1186

1187
1188
1189
1190
1191
1192
1193
1171
1172
1173
1174
1175
1176
1177

1178
1179

1180
1181
1182
1183
1184
1185
1186
1187







-
+

-
+







 "tokio",
 "tokio-native-tls",
 "tower-service",
]

[[package]]
name = "hyper-util"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df"
dependencies = [
 "base64",
 "bytes",
 "futures-channel",
 "futures-core",
 "futures-util",
 "http",
1367
1368
1369
1370
1371
1372
1373











1374
1375
1376
1377
1378
1379
1380
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385







+
+
+
+
+
+
+
+
+
+
+







source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
 "hermit-abi 0.3.9",
 "libc",
 "windows-sys 0.48.0",
]

[[package]]
name = "io-uring"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
dependencies = [
 "bitflags 2.9.1",
 "cfg-if",
 "libc",
]

[[package]]
name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"

1664
1665
1666
1667
1668
1669
1670






1671
1672
1673
1674
1675
1676
1677
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688







+
+
+
+
+
+







dependencies = [
 "cc",
 "libc",
 "pkg-config",
 "vcpkg",
]

[[package]]
name = "owo-colors"
version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"

[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"

[[package]]
2001
2002
2003
2004
2005
2006
2007
2008

2009
2010

2011
2012
2013
2014
2015
2016
2017
2012
2013
2014
2015
2016
2017
2018

2019
2020

2021
2022
2023
2024
2025
2026
2027
2028







-
+

-
+







name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"

[[package]]
name = "reqwest"
version = "0.12.21"
version = "0.12.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8cea6b35bcceb099f30173754403d2eba0a5dc18cea3630fccd88251909288"
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
dependencies = [
 "async-compression",
 "base64",
 "bytes",
 "encoding_rs",
 "futures-core",
 "futures-util",
2096
2097
2098
2099
2100
2101
2102
2103

2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117

2118
2119
2120
2121
2122
2123
2124
2107
2108
2109
2110
2111
2112
2113

2114
2115

2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135







-
+

-












+







 "derive_builder",
 "never",
 "quick-xml",
]

[[package]]
name = "rsstg"
version = "0.4.4"
version = "0.4.5"
dependencies = [
 "anyhow",
 "async-std",
 "atom_syndication",
 "chrono",
 "config",
 "futures",
 "futures-util",
 "lazy_static",
 "regex",
 "reqwest",
 "rss",
 "sedregex",
 "sqlx",
 "stacked_errors",
 "tgbot",
]

[[package]]
name = "rustc-demangle"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
2281
2282
2283
2284
2285
2286
2287
2288

2289
2290

2291
2292
2293
2294
2295
2296
2297
2292
2293
2294
2295
2296
2297
2298

2299
2300

2301
2302
2303
2304
2305
2306
2307
2308







-
+

-
+







 "memchr",
 "ryu",
 "serde",
]

[[package]]
name = "serde_spanned"
version = "0.6.9"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
dependencies = [
 "serde",
]

[[package]]
name = "serde_urlencoded"
version = "0.7.1"
2377
2378
2379
2380
2381
2382
2383






2384
2385
2386
2387
2388
2389
2390
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407







+
+
+
+
+
+








[[package]]
name = "slab"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"

[[package]]
name = "smallbox"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aca054fd9f8c2ebe8557a2433f307e038c0716124efd045daa0388afa5172189"

[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
 "serde",
2624
2625
2626
2627
2628
2629
2630












2631
2632
2633
2634
2635
2636
2637
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666







+
+
+
+
+
+
+
+
+
+
+
+







]

[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"

[[package]]
name = "stacked_errors"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45ef11d2fabcf9a75b82a9d80966bde3257410b1245b31f1fb6849103ceda0c3"
dependencies = [
 "owo-colors",
 "smallbox",
 "thin-vec",
 "thiserror",
]

[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
2726
2727
2728
2729
2730
2731
2732
2733

2734
2735

2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752






2753
2754
2755
2756
2757
2758
2759
2755
2756
2757
2758
2759
2760
2761

2762
2763

2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794







-
+

-
+

















+
+
+
+
+
+







 "once_cell",
 "rustix 1.0.7",
 "windows-sys 0.59.0",
]

[[package]]
name = "tgbot"
version = "0.38.0"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f93a57a73ceda468ee27ccfa9ee21b35891d72ad0ea9c48ba24ca96621f25ca"
checksum = "210b428621f06584a8a87942861f1bfb248d41b7c38fdcee57c1b6d45d7a0678"
dependencies = [
 "async-stream",
 "bytes",
 "derive_more",
 "futures-util",
 "log",
 "mime",
 "mime_guess",
 "reqwest",
 "serde",
 "serde_json",
 "serde_with",
 "shellwords",
 "tokio",
 "tokio-util",
]

[[package]]
name = "thin-vec"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"

[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
 "thiserror-impl",
2793
2794
2795
2796
2797
2798
2799
2800

2801
2802

2803
2804
2805

2806
2807
2808

2809
2810
2811
2812
2813
2814
2815
2828
2829
2830
2831
2832
2833
2834

2835
2836

2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852







-
+

-
+



+



+







name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"

[[package]]
name = "tokio"
version = "1.45.1"
version = "1.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
dependencies = [
 "backtrace",
 "bytes",
 "io-uring",
 "libc",
 "mio",
 "pin-project-lite",
 "slab",
 "socket2 0.5.10",
 "windows-sys 0.52.0",
]

[[package]]
name = "tokio-native-tls"
version = "0.3.1"
2841
2842
2843
2844
2845
2846
2847
2848

2849
2850

2851
2852
2853
2854
2855


2856
2857
2858
2859
2860

2861
2862

2863
2864
2865
2866
2867
2868
2869


2870
2871

2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2878
2879
2880
2881
2882
2883
2884

2885
2886

2887
2888
2889
2890
2891

2892
2893
2894
2895
2896
2897

2898
2899

2900
2901
2902
2903
2904
2905


2906
2907
2908

2909
2910




2911
2912
2913
2914
2915
2916
2917







-
+

-
+




-
+
+




-
+

-
+





-
-
+
+

-
+

-
-
-
-







 "futures-sink",
 "pin-project-lite",
 "tokio",
]

[[package]]
name = "toml"
version = "0.8.23"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
checksum = "f271e09bde39ab52250160a67e88577e0559ad77e9085de6e9051a2c4353f8f8"
dependencies = [
 "serde",
 "serde_spanned",
 "toml_datetime",
 "toml_edit",
 "toml_parser",
 "winnow",
]

[[package]]
name = "toml_datetime"
version = "0.6.11"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
 "serde",
]

[[package]]
name = "toml_edit"
version = "0.22.27"
name = "toml_parser"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
checksum = "b5c1c469eda89749d2230d8156a5969a69ffe0d6d01200581cdc6110674d293e"
dependencies = [
 "indexmap",
 "serde",
 "serde_spanned",
 "toml_datetime",
 "winnow",
]

[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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
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


-
+




-




-
+







+





[package]
name = "rsstg"
version = "0.4.4"
version = "0.4.5"
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" ] }
tgbot = "0.38"
tgbot = "0.39"
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"
stacked_errors = "0.7.1"
sqlx = { version = "0.8", features = [ "postgres", "runtime-async-std-rustls", "chrono", "macros" ], default-features = false }

[profile.release]
lto = true
codegen-units = 1
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
28
29

30
31
32
33
34
35
36
37



38
39
40
41
42

43
44

45
46
47
48
49
50
51
52
53
54




55
56
57
58
59
60
61

62
63
64
65
66
67

68
69
70
71
72
73
74
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

28
29
30
31
32
33



34
35
36
37
38
39
40

41
42

43
44
45
46
47
48
49




50
51
52
53
54
55
56
57
58
59

60
61
62
63
64
65

66
67
68
69
70
71
72
73


-
-
-
-
-
-



+
+
+
+
+

















-
+





-
-
-
+
+
+




-
+

-
+






-
-
-
-
+
+
+
+






-
+





-
+







use crate::core::Core;

use anyhow::{
	anyhow,
	bail,
	Context,
	Result,
};
use lazy_static::lazy_static;
use regex::Regex;
use sedregex::ReplaceCommand;
use stacked_errors::{
	Result,
	StackableErr,
	bail,
};
use tgbot::types::{
	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();
	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();
}

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?;
		Some(msg.chat.get_id()), Some(MarkdownV2)).await.stack()?;
	Ok(())
}

pub async fn list (core: &Core, msg: &Message) -> Result<()> {
	let sender = msg.sender.get_user_id()
		.ok_or(anyhow!("Ignoring unreal users."))?;
	let reply = core.list(sender).await?;
	core.send(reply, Some(msg.chat.get_id()), Some(MarkdownV2)).await?;
		.stack_err("Ignoring unreal users.")?;
	let reply = core.list(sender).await.stack()?;
	core.send(reply, Some(msg.chat.get_id()), Some(MarkdownV2)).await.stack()?;
	Ok(())
}

pub async fn command (core: &Core, command: &str, msg: &Message, words: &[String]) -> Result<()> {
	let mut conn = core.db.begin().await?;
	let mut conn = core.db.begin().await.stack()?;
	let sender = msg.sender.get_user_id()
		.ok_or(anyhow!("Ignoring unreal users."))?;
		.stack_err("Ignoring unreal users.")?;
	let reply = if words.len() == 1 {
		match words[0].parse::<i32>() {
			Err(err) => format!("I need a number.\n{}", &err).into(),
			Ok(number) => match command {
				"/check" => core.check(number, false).await
					.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(),
				"/clean" => conn.clean(number, sender).await.stack()?,
				"/enable" => conn.enable(number, sender).await.stack()?.into(),
				"/delete" => conn.delete(number, sender).await.stack()?,
				"/disable" => conn.disable(number, sender).await.stack()?.into(),
				_ => bail!("Command {command} {words:?} not handled."),
			},
		}
	} else {
		"This command needs exacly one number.".into()
	};
	core.send(reply, Some(msg.chat.get_id()), None).await?;
	core.send(reply, Some(msg.chat.get_id()), None).await.stack()?;
	Ok(())
}

pub async fn update (core: &Core, command: &str, msg: &Message, words: &[String]) -> Result<()> {
	let sender = msg.sender.get_user_id()
		.ok_or(anyhow!("Ignoring unreal users."))?;
		.stack_err("Ignoring unreal users.")?;
	let mut source_id: Option<i32> = None;
	let at_least = "Requires at least 3 parameters.";
	let mut i_words = words.iter();
	match command {
		"/update" => {
			let next_word = i_words.next().context(at_least)?;
			source_id = Some(next_word.parse::<i32>()
122
123
124
125
126
127
128
129

130
131
132
133
134
135
136
121
122
123
124
125
126
127

128
129
130
131
132
133
134
135







-
+







					Some(thing)
				}
			}
		},
		None => None,
	};
	let chat_id = ChatUsername::from(channel.as_ref());
	let channel_id = core.tg.execute(GetChat::new(chat_id.clone())).await?.id;
	let channel_id = core.tg.execute(GetChat::new(chat_id.clone())).await.stack_err("gettting GetChat")?.id;
	let chan_adm = core.tg.execute(GetChatAdministrators::new(chat_id)).await
		.context("Sorry, I have no access to that chat.")?;
	let (mut me, mut user) = (false, false);
	for admin in chan_adm {
		let member_id = match admin {
			ChatMember::Creator(member) => member.user.id,
			ChatMember::Administrator(member) => member.user.id,
144
145
146
147
148
149
150
151
152


153
154
143
144
145
146
147
148
149


150
151
152
153







-
-
+
+


		}
		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."); };
	let mut conn = core.db.begin().await?;
	core.send(conn.update(source_id, channel, channel_id, url, iv_hash, url_re, sender).await?, Some(msg.chat.get_id()), None).await?;
	let mut conn = core.db.begin().await.stack()?;
	core.send(conn.update(source_id, channel, channel_id, url, iv_hash, url_re, sender).await.stack()?, Some(msg.chat.get_id()), None).await.stack()?;
	Ok(())
}
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
1
2
3
4
5
6
7
8
9
10
11
12
13





14
15
16
17
18
19
20













-
-
-
-
-







use crate::{
	command,
	sql::Db,
};

use std::{
	borrow::Cow,
	collections::{
		BTreeMap,
		HashSet,
	},
};

use anyhow::{
	anyhow,
	bail,
	Result,
};
use async_std::{
	task,
	sync::{
		Arc,
		Mutex
	},
};
38
39
40
41
42
43
44






45
46
47
48
49
50
51
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52







+
+
+
+
+
+







		ParseMode,
		SendMessage,
		Update,
		UpdateType,
		UserPeerId,
	},
};
use stacked_errors::{
	Result,
	StackableErr,
	anyhow,
	bail,
};

lazy_static!{
	pub static ref RE_SPECIAL: Regex = Regex::new(r"([\-_*\[\]()~`>#+|{}\.!])").unwrap();
}

/// Encodes special HTML entities to prevent them interfering with Telegram HTML
pub fn encode (text: &str) -> Cow<'_, str> {
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
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







-
-
-
+
+
+



-
+


-
-
+
+




-
+







	pub db: Db,
	sources: Arc<Mutex<HashSet<Arc<i32>>>>,
	http_client: reqwest::Client,
}

impl Core {
	pub async fn new(settings: config::Config) -> Result<Core> {
		let owner_chat = ChatPeerId::from(settings.get_int("owner")?);
		let api_key = settings.get_string("api_key")?;
		let tg = Client::new(&api_key)?;
		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()?;

		let mut client = reqwest::Client::builder();
		if let Ok(proxy) = settings.get_string("proxy") {
			let proxy = reqwest::Proxy::all(proxy)?;
			let proxy = reqwest::Proxy::all(proxy).stack()?;
			client = client.proxy(proxy);
		}
		let http_client = client.build()?;
		let me = tg.execute(GetBot).await?;
		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")?)?,
			db: Db::new(&settings.get_string("pg").stack()?)?,
			sources: Arc::new(Mutex::new(HashSet::new())),
			http_client,
			// max_delay: 60,
		};
		let clone = core.clone();
		task::spawn(async move {
			loop {
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
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







-
+


-
+




-
+














-
-
+
+







-
+

-
+







-
+








	pub async fn send <S>(&self, msg: S, target: Option<ChatPeerId>, mode: Option<ParseMode>) -> Result<Message>
	where S: Into<String> {
		let msg = msg.into();

		let mode = mode.unwrap_or(ParseMode::Html);
		let target = target.unwrap_or(self.owner_chat);
		Ok(self.tg.execute(
		self.tg.execute(
			SendMessage::new(target, msg)
				.with_parse_mode(mode)
		).await?)
		).await.stack()
	}

	pub async fn check (&self, id: i32, real: bool) -> Result<String> {
		let mut posted: i32 = 0;
		let mut conn = self.db.begin().await?;
		let mut conn = self.db.begin().await.stack()?;

		let id = {
			let mut set = self.sources.lock_arc().await;
			match set.get(&id) {
				Some(id) => id.clone(),
				None => {
					let id = Arc::new(id);
					set.insert(id.clone());
					id.clone()
				},
			}
		};
		let count = Arc::strong_count(&id);
		if count == 2 {
			let source = conn.get_source(*id, self.owner_chat).await?;
			conn.set_scrape(*id).await?;
			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>, String> = BTreeMap::new();

			let response = self.http_client.get(&source.url).send().await?;
			let response = self.http_client.get(&source.url).send().await.stack()?;
			let status = response.status();
			let content = response.bytes().await?;
			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(&item.dublin_core_ext().unwrap().dates()[0]),
							}?;
							}.stack()?;
							let url = link;
							posts.insert(date, url.to_string());
						}
					};
				},
				Err(err) => match err {
					rss::Error::InvalidStartTag => {
174
175
176
177
178
179
180
181

182
183
184

185
186
187
188
189
190
191
192
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
175
176
177
178
179
180
181

182
183
184

185
186
187
188
189
190
191
192


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







-
+


-
+







-
-
+
+













-
-
+
+










-
+


-
+

















-
+





-
-
+
+







					},
					rss::Error::Eof => (),
					_ => bail!("Unsupported or mangled content:\n{:?}\n{err:#?}\n{status:#?}\n", &source.url)
				}
			};
			for (date, url) in posts.iter() {
				let post_url: Cow<str> = match source.url_re {
					Some(ref x) => sedregex::ReplaceCommand::new(x)?.execute(url),
					Some(ref x) => sedregex::ReplaceCommand::new(x).stack()?.execute(url),
					None => url.into(),
				};
				if let Some(exists) = conn.exists(&post_url, *id).await? {
				if let Some(exists) = conn.exists(&post_url, *id).await.stack()? {
					if ! exists {
						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?;
						conn.add_post(*id, date, &post_url).await?;
						}, 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 = {
			let mut conn = self.db.begin().await?;
			conn.get_queue().await?
			let mut conn = self.db.begin().await.stack()?;
			conn.get_queue().await.stack()?
		};
		for row in queue {
			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: ChatPeerId::from(owner),
							..self.clone()
						};
						let source = {
							let mut conn = self.db.begin().await?;
							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(),
								Ok(None) => "Source not found in database.stack()?".to_string(),
								Err(err) => format!("Failed to fetch source data:\n{err}"),
							}
						};
						task::spawn(async move {
							if let Err(err) = clone.check(source_id, true).await {
								if let Err(err) = clone.send(&format!("{source}\n\nšŸ›‘ {}", encode(&err.to_string())), None, Some(ParseMode::MarkdownV2)).await {
									eprintln!("Check error: {err:?}");
									// clone.disable(&source_id, owner).await.unwrap();
								};
							};
						});
					}
				} else if next_fetch - now < delay {
					delay = next_fetch - now;
				}
			}
		};
		Ok(delay.to_std()?)
		delay.to_std().stack()
	}

	pub async fn list (&self, owner: UserPeerId) -> Result<String> {
		let mut reply: Vec<String> = vec![];
		reply.push("Channels:".into());
		let mut conn = self.db.begin().await?;
		for row in conn.get_list(owner).await? {
		let mut conn = self.db.begin().await.stack()?;
		for row in conn.get_list(owner).await.stack()? {
			reply.push(row.to_string());
		};
		Ok(reply.join("\n\n"))
	}
}

impl UpdateHandler for Core {
1
2
3
4
5
6
7
8
9

10



11
12
13
14
15
16
17


18
19

20
21
22
23
24
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
28









+
-
+
+
+






-
+
+

-
+





//! This is telegram bot to fetch RSS/ATOM feeds and post results on public
//! channels

#![warn(missing_docs)]

mod command;
mod core;
mod sql;

use stacked_errors::{
use anyhow::Result;
	Result,
	StackableErr,
};
use tgbot::handler::LongPoll;

#[async_std::main]
async fn main() -> Result<()> {
	let settings = config::Config::builder()
		.add_source(config::File::with_name("rsstg"))
		.build()?;
		.build()
		.stack()?;

	let core = core::Core::new(settings).await?;
	let core = core::Core::new(settings).await.stack()?;

	LongPoll::new(core.tg.clone(), core).run().await;

	Ok(())
}
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
28
29
30
31
32
33
34
35
36
37

38
39
40
41
42
43
44
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
28
29
30
31
32
33
34
35
36
37

38
39
40
41
42
43
44
45





-
-
-
-















+
+
+
+
+












-
+







use std::{
	borrow::Cow,
	fmt,
};

use anyhow::{
	Result,
	bail,
};
use async_std::sync::{
	Arc,
	Mutex,
};
use chrono::{
	DateTime,
	FixedOffset,
	Local,
};
use sqlx::{
	Postgres,
	Row,
	postgres::PgPoolOptions,
	pool::PoolConnection,
};
use stacked_errors::{
	Result,
	StackableErr,
	bail,
};

#[derive(sqlx::FromRow, Debug)]
pub struct List {
	pub source_id: i32,
	pub channel: String,
	pub enabled: bool,
	pub url: String,
	pub iv_hash: Option<String>,
	pub url_re: Option<String>,
}

impl fmt::Display for List {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::result::Result<(), fmt::Error> {
		write!(f, "#{} \\*ļøāƒ£ `{}` {}\nšŸ”— `{}`", self.source_id, self.channel,
			match self.enabled {
				true  => "šŸ”„ enabled",
				false => "ā›” disabled",
			}, self.url)?;
		if let Some(iv_hash) = &self.iv_hash {
			write!(f, "\nIV: `{iv_hash}`")?;
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
174
175
176
177
178
179
180
181

182
183
184
185
186
187
188
189
190

191
192
193
194
195
196
197
198

199
200
201
202
203
204
205
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

174
175
176
177
178
179
180
181
182

183
184
185
186
187
188
189
190
191

192
193
194
195
196
197
198
199

200
201
202
203
204
205
206
207







-
+
+





-
+














-
+








-
+










-
+

-
+








-
+











-
+











-
-
+
+





-
+







-
+








-
+








-
+







-
+







impl Db {
	pub fn new (pguri: &str) -> Result<Db> {
		Ok(Db (
			Arc::new(Mutex::new(PgPoolOptions::new()
				.max_connections(5)
				.acquire_timeout(std::time::Duration::new(300, 0))
				.idle_timeout(std::time::Duration::new(60, 0))
				.connect_lazy(pguri)?)),
				.connect_lazy(pguri)
				.stack()?)),
		))
	}

	pub async fn begin(&self) -> Result<Conn> {
		let pool = self.0.lock_arc().await;
		let conn = Conn ( pool.acquire().await? );
		let conn = Conn ( pool.acquire().await.stack()? );
		Ok(conn)
	}
}

pub struct Conn (
	PoolConnection<Postgres>,
);

impl Conn {
	pub async fn add_post (&mut self, source_id: i32, date: &DateTime<FixedOffset>, post_url: &str) -> Result<()> {
		sqlx::query("insert into rsstg_post (source_id, posted, url) values ($1, $2, $3);")
			.bind(source_id)
			.bind(date)
			.bind(post_url)
			.execute(&mut *self.0).await?;
			.execute(&mut *self.0).await.stack()?;
		Ok(())
	}

	pub async fn clean <I> (&mut self, source_id: i32, owner: I) -> Result<Cow<'_, str>>
	where I: Into<i64> {
		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.into())
			.execute(&mut *self.0).await?.rows_affected() {
			.execute(&mut *self.0).await.stack()?.rows_affected() {
			0 => { Ok("No data found found.".into()) },
			x => { Ok(format!("{x} posts purged.").into()) },
		}
	}

	pub async fn delete <I> (&mut self, source_id: i32, owner: I) -> Result<Cow<'_, str>>
	where I: Into<i64> {
		match sqlx::query("delete from rsstg_source where source_id = $1 and owner = $2;")
			.bind(source_id)
			.bind(owner.into())
			.execute(&mut *self.0).await?.rows_affected() {
			.execute(&mut *self.0).await.stack()?.rows_affected() {
			0 => { Ok("No data found found.".into()) },
			x => { Ok(format!("{} sources removed.", x).into()) },
			x => { Ok(format!("{x} sources removed.").into()) },
		}
	}

	pub async fn disable <I> (&mut self, source_id: i32, owner: I) -> Result<&str>
	where I: Into<i64> {
		match sqlx::query("update rsstg_source set enabled = false where source_id = $1 and owner = $2")
			.bind(source_id)
			.bind(owner.into())
			.execute(&mut *self.0).await?.rows_affected() {
			.execute(&mut *self.0).await.stack()?.rows_affected() {
			1 => { Ok("Source disabled.") },
			0 => { Ok("Source not found.") },
			_ => { bail!("Database error.") },
		}
	}

	pub async fn enable <I> (&mut self, source_id: i32, owner: I) -> Result<&str>
	where I: Into<i64> {
		match sqlx::query("update rsstg_source set enabled = true where source_id = $1 and owner = $2")
			.bind(source_id)
			.bind(owner.into())
			.execute(&mut *self.0).await?.rows_affected() {
			.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<Option<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?;
		let exists: Option<bool> = row.try_get("exists")?;
			.fetch_one(&mut *self.0).await.stack()?;
		let exists: Option<bool> = row.try_get("exists").stack()?;
		Ok(exists)
	}

	pub async fn get_queue (&mut self) -> Result<Vec<Queue>> {
		let block: Vec<Queue> = 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.0).await?;
			.fetch_all(&mut *self.0).await.stack()?;
		Ok(block)
	}

	pub async fn get_list <I> (&mut self, owner: I) -> Result<Vec<List>>
	where I: Into<i64> {
		let source: Vec<List> = 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.into())
			.fetch_all(&mut *self.0).await?;
			.fetch_all(&mut *self.0).await.stack()?;
		Ok(source)
	}

	pub async fn get_one <I> (&mut self, owner: I, id: i32) -> Result<Option<List>>
	where I: Into<i64> {
		let source: Option<List> = sqlx::query_as("select source_id, channel, enabled, url, iv_hash, url_re from rsstg_source where owner = $1 and source_id = $2")
			.bind(owner.into())
			.bind(id)
			.fetch_optional(&mut *self.0).await?;
			.fetch_optional(&mut *self.0).await.stack()?;
		Ok(source)
	}

	pub async fn get_source <I> (&mut self, id: i32, owner: I) -> Result<Source>
	where I: Into<i64> {
		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.into())
			.fetch_one(&mut *self.0).await?;
			.fetch_one(&mut *self.0).await.stack()?;
		Ok(source)
	}

	pub async fn set_scrape <I> (&mut self, id: I) -> Result<()>
	where I: Into<i64> {
		sqlx::query("update rsstg_source set last_scrape = now() where source_id = $1;")
			.bind(id.into())
			.execute(&mut *self.0).await?;
			.execute(&mut *self.0).await.stack()?;
		Ok(())
	}

	pub async fn update <I> (&mut self, update: Option<i32>, channel: &str, channel_id: i64, url: &str, iv_hash: Option<&str>, url_re: Option<&str>, owner: I) -> Result<&str>
	where I: Into<i64> {
		match match update {
				Some(id) => {