Check-in [d8c1d259a2]
Logged in as anonymous
Overview
Comment:some fixes (by CodeRabbit), merge changes from release branch
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | trunk
Files: files | file ages | folders
SHA3-256: d8c1d259a292ba4d06a715abf834bd0f399d9c232103c0ec1ea2f5fa8f44402d
User & Date: arcade on 2026-04-23 18:44:39.858
Other Links: manifest | tags
Context
2026-04-23
18:44
some fixes (by CodeRabbit), merge changes from release branch leaf check-in: d8c1d259a2 user: arcade tags: trunk
10:36
fix workflow, fix and comment clippy warnings bump rustls-webpki to 0.103.13 (fixes 2 lowsec issues) leaf check-in: 01565c7f7e user: arcade tags: release, v0.5.6
2026-04-18
18:31
bump, update workflow, add update reaction code, change paging logic check-in: be0b8602d1 user: arcade tags: trunk
Changes
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
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

+
-
+
+
+










+







-
+







name: rust-ci
on:
on: push
  push:
    branches: [ master, release ]
  pull_request:

# sccache enable for rust/C builds
env:
  SCCACHE_GHA_ENABLED: "true"
  RUSTC_WRAPPER: "sccache"

jobs:
  rust-ci-run:
    name: Run rust-clippy analyzing and tests
    runs-on: ubuntu-latest
    if: github.event_name == 'push' || (github.head_ref != 'master' && github.head_ref != 'release')
    permissions:
      contents: read
    steps:
      # SETUP
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - uses: mozilla-actions/sccache-action@v0.0.9
      - uses: mozilla-actions/sccache-action@v0.0.10

      # TESTS
      - name: Run tests
        run: cargo test --all-targets --all-features

      # CLIPPY
      - name: Run rust-clippy
2089
2090
2091
2092
2093
2094
2095
2096

2097
2098

2099
2100
2101
2102
2103
2104
2105
2089
2090
2091
2092
2093
2094
2095

2096
2097

2098
2099
2100
2101
2102
2103
2104
2105







-
+

-
+







 "libc",
 "linux-raw-sys",
 "windows-sys 0.61.2",
]

[[package]]
name = "rustls"
version = "0.23.38"
version = "0.23.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e"
dependencies = [
 "aws-lc-rs",
 "once_cell",
 "ring",
 "rustls-pki-types",
 "rustls-webpki",
 "subtle",
2153
2154
2155
2156
2157
2158
2159
2160

2161
2162

2163
2164
2165
2166
2167
2168
2169
2153
2154
2155
2156
2157
2158
2159

2160
2161

2162
2163
2164
2165
2166
2167
2168
2169







-
+

-
+







name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"

[[package]]
name = "rustls-webpki"
version = "0.103.12"
version = "0.103.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
dependencies = [
 "aws-lc-rs",
 "ring",
 "rustls-pki-types",
 "untrusted",
]

3020
3021
3022
3023
3024
3025
3026
3027

3028
3029

3030
3031
3032
3033
3034
3035
3036
3020
3021
3022
3023
3024
3025
3026

3027
3028

3029
3030
3031
3032
3033
3034
3035
3036







-
+

-
+







checksum = "4189890526f0168710b6ee65ceaedf1460c48a14318ceec933cb26baa492096a"
dependencies = [
 "linked-hash-map",
]

[[package]]
name = "typenum"
version = "1.19.0"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"

[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"

3627
3628
3629
3630
3631
3632
3633
3634

3635
3636

3637
3638
3639
3640
3641
3642
3643
3627
3628
3629
3630
3631
3632
3633

3634
3635

3636
3637
3638
3639
3640
3641
3642
3643







-
+

-
+







name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"

[[package]]
name = "winnow"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
dependencies = [
 "memchr",
]

[[package]]
name = "wit-bindgen"
version = "0.57.1"
40
41
42
43
44
45
46
47

48
49
50
51
52
53
54
40
41
42
43
44
45
46

47
48
49
50
51
52
53
54







-
+







	)).await.stack()?;
	Ok(())
}

/// Send the sender's subscription list to the chat.
///
/// Retrieves the message sender's user ID, obtains their subscription list from `core`,
/// and sends the resulting reply into the message chat using MarkdownV2.
/// and sends the resulting reply into the message chat using HTML
pub async fn list (core: &Core, msg: &Message) -> Result<()> {
	let sender = msg.sender.get_user_id()
		.stack_err("Ignoring unreal users.")?;
	let reply = core.list(sender).await.stack()?;
	core.tg.send(MyMessage::html_to(reply, msg.chat.get_id())).await.stack()?;
	Ok(())
}
1
2
3
4
5
6
7
8
9

10
11
12
13
14
15
16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17









+







use crate::{
	Arc,
	command,
	Mutex,
	sql::Db,
	tg_bot::{
		Callback,
		MyMessage,
		Tg,
		validate,
	},
};

use std::{
	borrow::Cow,
	collections::{
		BTreeMap,
110
111
112
113
114
115
116


117
118
119
120
121
122
123
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126







+
+







	pub tg: Tg,
	pub db: Db,
	pub feeds: Arc<Mutex<UserCache>>,
	running: Arc<Mutex<HashSet<i32>>>,
	http_client: reqwest::Client,
}

// XXX Right now that part is unfinished and I guess I need to finish menu first
#[allow(unused)]
pub struct Post {
	uri: String,
	_title: String,
	_authors: String,
	_summary: String,
}

419
420
421
422
423
424
425
426

427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447







448
449







450
451
452
453
454
455
456
422
423
424
425
426
427
428

429

430
431
432
433
434
435
436
437
438
439
440
441
442
443
444





445
446
447
448
449
450
451


452
453
454
455
456
457
458
459
460
461
462
463
464
465







-
+
-















-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+







}

impl UpdateHandler for Core {
	/// Dispatches an incoming Telegram update to a matching command handler and reports handler errors to the originating chat.
	///
	/// This method inspects the update; if it contains a message that can be parsed as a bot command,
	/// it executes the corresponding command handler. If the handler returns an error, the error text
	/// is sent back to the message's chat using MarkdownV2 formatting. Unknown commands produce an error
	/// is sent back to the message's chat. Unknown commands produce an error which is also reported to the chat.
	/// which is also reported to the chat.
	async fn handle (&self, update: Update) -> () {
		match update.update_type {
			UpdateType::Message(msg) => {
				if let Ok(cmd) = Command::try_from(*msg) {
					let msg = cmd.get_message();
					let words = cmd.get_args();
					let command = cmd.get_name();
					let res = match command {
						"/check" | "/clean" | "/enable" | "/delete" | "/disable" => command::command(self, command, msg, words).await,
						"/start" => command::start(self, msg).await,
						"/list" => command::list(self, msg).await,
						"/test" => command::test(self, msg).await,
						"/add" | "/update" => command::update(self, command, msg, words).await,
						any => Err(anyhow!("Unknown command: {any}")),
					};
					if let Err(err) = res 
						&& let Err(err2) = self.tg.send(MyMessage::html_to(
							format!("#error<pre>{err}</pre>"),
							msg.chat.get_id(),
						)).await
					if let Err(err) = res  {
						match validate(&err.to_string()) {
							Ok(text) => {
								if let Err(err2) = self.tg.send(MyMessage::html_to(
									format!("#error<pre>{}</pre>", text),
									msg.chat.get_id(),
								)).await {
					{
						dbg!(err2);
									dbg!(err2);
								}
							},
							Err(err2) => {
								dbg!(err2);
							},
						}
					}
				} else {
					// not a command
				}
			},
			UpdateType::CallbackQuery(query) => {
				if let Some(ref cb) = query.data
232
233
234
235
236
237
238
239



240
241
242
243
244
245
246
232
233
234
235
236
237
238

239
240
241
242
243
244
245
246
247
248







-
+
+
+







	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.stack()?;
		Ok(())
	}

	#[allow(clippy::too_many_arguments)] // XXX at least for now…
	#[allow(clippy::too_many_arguments)] // XXX do I need to make it variadic? I guess it work fine
										 // this way for now, unless there would be a good struct
										 // with all source fields to use that as an argument
	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) => {
					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)
				},
1
2
3
4
5
6
7
8
9
10
11
12

13
14
15
16

17
18
19
20
21
22
23
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












+




+







use crate::{
	Arc,
	Mutex,
	core::FeedList,
};

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

use lazy_static::lazy_static;
use serde::{
	Deserialize,
	Serialize,
};
use regex::Regex;
use smol::Timer;
use stacked_errors::{
	bail,
	Result,
	StackableErr,
};
use tgbot::{
37
38
39
40
41
42
43















44
45
46
47
48
49
50
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







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







		Message,
		ParseMode,
		SendMessage,
	},
};

const CB_VERSION: u8 = 0;

lazy_static! {
	pub static ref RE_CLOSING: Regex = Regex::new(r"</[ \t]*(pre|code)[ \t]*>").unwrap();
}

// validate input as being postable in preformatted block, all html tags are fine, except tags that
// break the block - </pre> and </code>, we don't need to escape anything else, as telegram manages
// that
pub fn validate (text: &str) -> Result<&str> {
	if RE_CLOSING.is_match(text) {
		bail!("Telegram closing tag found.");
	} else {
		Ok(text)
	}
}

#[derive(Serialize, Deserialize, Debug)]
pub enum Callback {
	// Edit one feed (version, name)
	Edit(u8, String),
	// List all feeds (version, name to show, page number)
	List(u8, String, usize),
146
147
148
149
150
151
152
153

154
155
156
157
158
159
160
163
164
165
166
167
168
169

170
171
172
173
174
175
176
177







-
+







				}
			}
			if long {
				kb.push(vec![
					InlineKeyboardButton::for_callback_data("<<",
						Callback::list("", if *page == 0 { *page } else { page - 1 } ).to_string()),
					InlineKeyboardButton::for_callback_data(">>",
						Callback::list("", page + 1).to_string()),
						Callback::list("", page.saturating_add(1)).to_string()),
				]);
			}
			InlineKeyboardMarkup::from(kb)
		},
		Callback::Menu(_) => {
			let kb = vec![
				vec![
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
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







-
-
+
+










-
+







	
	pub fn html_to_kb <'a, S> (text: S, to: ChatPeerId, kb: InlineKeyboardMarkup) -> MyMessage<'a>
	where S: Into<Cow<'a, str>> {
		let text = text.into();
		MyMessage::HtmlToKb { text, to, kb }
	}
	
	fn req (&self, tg: &Tg) -> Result<SendMessage> {
		Ok(match self {
	fn req (&self, tg: &Tg) -> SendMessage {
		match self {
			MyMessage::Html { text } =>
				SendMessage::new(tg.owner, text.as_ref())
					.with_parse_mode(ParseMode::Html),
			MyMessage::HtmlTo { text, to } =>
				SendMessage::new(*to, text.as_ref())
					.with_parse_mode(ParseMode::Html),
			MyMessage::HtmlToKb { text, to, kb } =>
				SendMessage::new(*to, text.as_ref())
					.with_parse_mode(ParseMode::Html)
					.with_reply_markup(kb.clone()),
		})
		}
	}
}

#[derive(Clone)]
pub struct Tg {
	pub me: Bot,
	pub owner: ChatPeerId,
251
252
253
254
255
256
257
258

259
260
261
262
263
264
265
268
269
270
271
272
273
274

275
276
277
278
279
280
281
282







-
+







	}

	/// Send a text message to a chat, using an optional target and parse mode.
	///
	/// # Returns
	/// The sent `Message` on success.
	pub async fn send (&self, msg: MyMessage<'_>) -> Result<Message> {
		self.client.execute(msg.req(self)?).await.stack()
		self.client.execute(msg.req(self)).await.stack()
	}

	pub async fn answer_cb (&self, id: String, text: String) -> Result<bool> {
		self.client.execute(
			AnswerCallbackQuery::new(id)
				.with_text(text)
		).await.stack()
276
277
278
279
280
281
282

283
284
285
286
287
288
289
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307







+







	where O: Into<i64> {
		Tg {
			owner: ChatPeerId::from(owner.into()),
			..self.clone()
		}
	}

	// XXX Can loop indefinitely if API calls results retry_after, add max retries?
	pub async fn update_message (&self, chat_id: i64, message_id: i64, text: String, feeds: &Arc<Mutex<FeedList>>, cb: Callback) -> Result<EditMessageResult> {
		loop {
			let req = EditMessageText::for_chat_message(chat_id, message_id, &text)
				.with_reply_markup(get_kb(&cb, feeds).await.stack()?);
			let res = self.client.execute(req).await;
			match res {
				Ok(res) => return Ok(res),