Skip to content

Commit 8a2ca7a

Browse files
committed
feat(sync): add unread count to topic output, prune-after flag
1 parent 2ec5dcb commit 8a2ca7a

7 files changed

Lines changed: 202 additions & 33 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "tgcli"
3-
version = "0.3.2"
3+
version = "0.3.3"
44
edition = "2021"
55
authors = ["Dario <me@dgrp.es>"]
66
description = "Telegram CLI tool using grammers (pure Rust MTProto)"

src/app/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ impl App {
121121
&topic.title,
122122
topic.icon_color,
123123
icon_emoji.as_deref(),
124+
topic.unread_count,
124125
)
125126
.await?;
126127
count += 1;

src/app/sync.rs

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ pub struct SyncOptions {
4242
pub concurrency: usize,
4343
/// If set, only sync this specific chat
4444
pub chat_filter: Option<i64>,
45+
/// After sync, prune messages keeping only the N most recent per chat
46+
pub prune_after: Option<usize>,
4547
}
4648

4749
/// Get media type string and file extension from grammers Media enum
@@ -134,6 +136,7 @@ pub struct TopicSyncSummary {
134136
pub topic_id: i32,
135137
pub topic_name: String,
136138
pub messages_synced: u64,
139+
pub unread_count: i32,
137140
}
138141

139142
/// Summary of messages synced for a single chat
@@ -145,6 +148,8 @@ pub struct ChatSyncSummary {
145148
/// For forum chats, breakdown by topic
146149
#[serde(skip_serializing_if = "Vec::is_empty")]
147150
pub topics: Vec<TopicSyncSummary>,
151+
/// Unread message count for this chat at sync time
152+
pub unread_count: i32,
148153
}
149154

150155
pub struct SyncResult {
@@ -998,18 +1003,22 @@ impl App {
9981003
if result.is_forum && !result.topic_counts.is_empty() {
9991004
let mut topic_summaries = Vec::new();
10001005
for (tid, msg_count) in &result.topic_counts {
1001-
let topic_name = self
1006+
let topic = self
10021007
.store
10031008
.get_topic(result.chat_id, *tid)
10041009
.await
10051010
.ok()
1006-
.flatten()
1011+
.flatten();
1012+
let topic_name = topic
1013+
.as_ref()
10071014
.map(|t| t.name.clone())
10081015
.unwrap_or_else(|| format!("Topic {}", tid));
1016+
let unread_count = topic.map(|t| t.unread_count).unwrap_or(0);
10091017
topic_summaries.push(TopicSyncSummary {
10101018
topic_id: *tid,
10111019
topic_name,
10121020
messages_synced: *msg_count,
1021+
unread_count,
10131022
});
10141023
}
10151024
topic_summaries
@@ -1028,6 +1037,7 @@ impl App {
10281037
.find(|t| t.topic_id == new_topic.topic_id)
10291038
{
10301039
existing_topic.messages_synced += new_topic.messages_synced;
1040+
existing_topic.unread_count = new_topic.unread_count;
10311041
} else {
10321042
existing.topics.push(new_topic.clone());
10331043
}
@@ -1038,6 +1048,7 @@ impl App {
10381048
chat_name: result.chat_name.clone(),
10391049
messages_synced: result.messages.len() as u64,
10401050
topics: new_topics,
1051+
unread_count: 0, // Not available in msgs-only sync
10411052
});
10421053
}
10431054
}
@@ -1047,6 +1058,29 @@ impl App {
10471058
chats_processed, messages_stored, concurrency
10481059
);
10491060

1061+
// Prune old messages if --prune-after is set
1062+
if let Some(keep_count) = opts.prune_after {
1063+
if show_progress {
1064+
eprint!("Pruning old messages (keeping {} per chat)...", keep_count);
1065+
}
1066+
match self.store.prune_all_chats(keep_count).await {
1067+
Ok(deleted) => {
1068+
if show_progress {
1069+
eprint!("\r\x1b[K");
1070+
}
1071+
if deleted > 0 {
1072+
eprintln!("Pruned {} old messages", deleted);
1073+
}
1074+
}
1075+
Err(e) => {
1076+
if show_progress {
1077+
eprint!("\r\x1b[K");
1078+
}
1079+
log::warn!("Failed to prune messages: {}", e);
1080+
}
1081+
}
1082+
}
1083+
10501084
// Convert HashMap to Vec and sort topics by message count descending
10511085
let per_chat: Vec<ChatSyncSummary> = per_chat_map
10521086
.into_values()
@@ -1154,6 +1188,9 @@ impl App {
11541188
.await?;
11551189
}
11561190

1191+
// Track unread_count for filtering output later
1192+
let unread_count = extract_unread_count(&dialog.raw);
1193+
11571194
// Fetch messages for this chat
11581195
let peer_ref = PeerRef::from(peer);
11591196
let mut message_iter = client.iter_messages(peer_ref);
@@ -1347,18 +1384,17 @@ impl App {
13471384
let new_topics: Vec<TopicSyncSummary> = if is_forum && !topic_counts.is_empty() {
13481385
let mut topic_summaries = Vec::new();
13491386
for (tid, msg_count) in &topic_counts {
1350-
let topic_name = self
1351-
.store
1352-
.get_topic(id, *tid)
1353-
.await
1354-
.ok()
1355-
.flatten()
1387+
let topic = self.store.get_topic(id, *tid).await.ok().flatten();
1388+
let topic_name = topic
1389+
.as_ref()
13561390
.map(|t| t.name.clone())
13571391
.unwrap_or_else(|| format!("Topic {}", tid));
1392+
let unread_count = topic.map(|t| t.unread_count).unwrap_or(0);
13581393
topic_summaries.push(TopicSyncSummary {
13591394
topic_id: *tid,
13601395
topic_name,
13611396
messages_synced: *msg_count,
1397+
unread_count,
13621398
});
13631399
}
13641400
topic_summaries
@@ -1371,14 +1407,16 @@ impl App {
13711407
.entry(id)
13721408
.and_modify(|existing| {
13731409
existing.messages_synced += count as u64;
1374-
// Merge topics by topic_id
1410+
existing.unread_count = unread_count; // Update with latest unread count
1411+
// Merge topics by topic_id
13751412
for new_topic in &new_topics {
13761413
if let Some(existing_topic) = existing
13771414
.topics
13781415
.iter_mut()
13791416
.find(|t| t.topic_id == new_topic.topic_id)
13801417
{
13811418
existing_topic.messages_synced += new_topic.messages_synced;
1419+
existing_topic.unread_count = new_topic.unread_count;
13821420
} else {
13831421
existing.topics.push(new_topic.clone());
13841422
}
@@ -1389,6 +1427,7 @@ impl App {
13891427
chat_name: name.clone(),
13901428
messages_synced: count as u64,
13911429
topics: new_topics,
1430+
unread_count,
13921431
});
13931432
}
13941433
}
@@ -1608,26 +1647,25 @@ impl App {
16081647
let new_topics: Vec<TopicSyncSummary> = if is_forum && !topic_counts.is_empty() {
16091648
let mut topic_summaries = Vec::new();
16101649
for (tid, msg_count) in &topic_counts {
1611-
let topic_name = self
1612-
.store
1613-
.get_topic(id, *tid)
1614-
.await
1615-
.ok()
1616-
.flatten()
1650+
let topic = self.store.get_topic(id, *tid).await.ok().flatten();
1651+
let topic_name = topic
1652+
.as_ref()
16171653
.map(|t| t.name.clone())
16181654
.unwrap_or_else(|| format!("Topic {}", tid));
1655+
let unread_count = topic.map(|t| t.unread_count).unwrap_or(0);
16191656
topic_summaries.push(TopicSyncSummary {
16201657
topic_id: *tid,
16211658
topic_name,
16221659
messages_synced: *msg_count,
1660+
unread_count,
16231661
});
16241662
}
16251663
topic_summaries
16261664
} else {
16271665
Vec::new()
16281666
};
16291667

1630-
// Aggregate into per_chat_map
1668+
// Aggregate into per_chat_map (archived chats don't have unread info)
16311669
per_chat_map
16321670
.entry(id)
16331671
.and_modify(|existing| {
@@ -1640,6 +1678,7 @@ impl App {
16401678
.find(|t| t.topic_id == new_topic.topic_id)
16411679
{
16421680
existing_topic.messages_synced += new_topic.messages_synced;
1681+
existing_topic.unread_count = new_topic.unread_count;
16431682
} else {
16441683
existing.topics.push(new_topic.clone());
16451684
}
@@ -1650,6 +1689,7 @@ impl App {
16501689
chat_name: name.clone(),
16511690
messages_synced: count as u64,
16521691
topics: new_topics,
1692+
unread_count: 0, // Archived chats don't have unread info
16531693
});
16541694
}
16551695
}
@@ -1672,6 +1712,29 @@ impl App {
16721712
);
16731713
}
16741714

1715+
// Prune old messages if --prune-after is set
1716+
if let Some(keep_count) = opts.prune_after {
1717+
if opts.show_progress {
1718+
eprint!("Pruning old messages (keeping {} per chat)...", keep_count);
1719+
}
1720+
match self.store.prune_all_chats(keep_count).await {
1721+
Ok(deleted) => {
1722+
if opts.show_progress {
1723+
eprint!("\r\x1b[K");
1724+
}
1725+
if deleted > 0 {
1726+
eprintln!("Pruned {} old messages", deleted);
1727+
}
1728+
}
1729+
Err(e) => {
1730+
if opts.show_progress {
1731+
eprint!("\r\x1b[K");
1732+
}
1733+
log::warn!("Failed to prune messages: {}", e);
1734+
}
1735+
}
1736+
}
1737+
16751738
// Convert HashMap to Vec and sort topics by message count descending
16761739
let per_chat: Vec<ChatSyncSummary> = per_chat_map
16771740
.into_values()
@@ -2006,6 +2069,14 @@ async fn download_message_media_static(
20062069
}
20072070
}
20082071

2072+
/// Extract unread_count from a raw Dialog enum
2073+
fn extract_unread_count(raw: &tl::enums::Dialog) -> i32 {
2074+
match raw {
2075+
tl::enums::Dialog::Dialog(d) => d.unread_count,
2076+
tl::enums::Dialog::Folder(_) => 0,
2077+
}
2078+
}
2079+
20092080
/// Returns (kind, name, username, is_forum, access_hash)
20102081
fn peer_info(peer: &Peer) -> (String, String, Option<String>, bool, Option<i64>) {
20112082
match peer {

src/cmd/daemon.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ pub async fn run(cli: &Cli, args: &DaemonArgs) -> Result<()> {
215215
messages_per_chat: 50,
216216
concurrency: 4,
217217
chat_filter: None,
218+
prune_after: None,
218219
};
219220

220221
let result = backfill_app.sync(opts).await;

src/cmd/sync.rs

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ pub struct CommonSyncArgs {
4646
/// Suppress summary output (just show "Sync complete")
4747
#[arg(long, default_value_t = false)]
4848
pub quiet: bool,
49+
50+
/// After sync, prune messages keeping only the N most recent per chat
51+
#[arg(long, value_name = "N")]
52+
pub prune_after: Option<usize>,
4953
}
5054

5155
#[derive(Subcommand, Debug, Clone)]
@@ -102,6 +106,7 @@ fn build_sync_options(common: &CommonSyncArgs) -> crate::app::sync::SyncOptions
102106
messages_per_chat: common.messages_per_chat,
103107
concurrency: common.concurrency,
104108
chat_filter: None,
109+
prune_after: common.prune_after,
105110
}
106111
}
107112

@@ -160,21 +165,37 @@ fn print_sync_result(
160165
);
161166

162167
for chat in &chats_with_messages {
163-
println!(
164-
" {:<width$} +{} {}",
165-
chat.chat_name,
166-
chat.messages_synced,
167-
plural(chat.messages_synced),
168-
width = max_name_len
169-
);
170-
// Show topic breakdown for forums
168+
// Forum parent: show messages only (no unread)
169+
// Regular chat: show messages + unread
170+
if !chat.topics.is_empty() {
171+
// Forum parent line - no unread count
172+
println!(
173+
" {:<width$} +{} {}",
174+
chat.chat_name,
175+
chat.messages_synced,
176+
plural(chat.messages_synced),
177+
width = max_name_len
178+
);
179+
} else {
180+
// Regular chat - show unread
181+
println!(
182+
" {:<width$} +{} {} +{} unread",
183+
chat.chat_name,
184+
chat.messages_synced,
185+
plural(chat.messages_synced),
186+
chat.unread_count,
187+
width = max_name_len
188+
);
189+
}
190+
// Show topic breakdown for forums - with unread count
171191
for topic in &chat.topics {
172192
if topic.messages_synced > 0 {
173193
println!(
174-
" └ {:<width$} +{} {}",
194+
" └ {:<width$} +{} {} +{} unread",
175195
topic.topic_name,
176196
topic.messages_synced,
177197
plural(topic.messages_synced),
198+
topic.unread_count,
178199
width = max_name_len - 4
179200
);
180201
}

0 commit comments

Comments
 (0)