1use std::collections::HashSet;
24use std::sync::Arc;
25use tokio::sync::Mutex;
26use tokio::time::{self, Duration};
27
28pub use toptl::{StatsPayload, TopTL};
29
30#[derive(Clone)]
32pub struct TopTLPlugin {
33 client: Arc<TopTL>,
34 username: String,
35 state: Arc<Mutex<PluginState>>,
36}
37
38#[derive(Default)]
39struct PluginState {
40 user_ids: HashSet<i64>,
41 group_ids: HashSet<i64>,
42 channel_ids: HashSet<i64>,
43}
44
45#[derive(Debug, Clone, Copy)]
47pub enum ChatKind {
48 Private,
49 Group,
50 Supergroup,
51 Channel,
52}
53
54impl TopTLPlugin {
55 pub fn new(client: TopTL, username: impl Into<String>) -> Self {
56 Self {
57 client: Arc::new(client),
58 username: username.into(),
59 state: Arc::new(Mutex::new(PluginState::default())),
60 }
61 }
62
63 pub async fn record(&self, user_id: Option<i64>, chat: Option<(i64, ChatKind)>) {
67 let mut state = self.state.lock().await;
68 if let Some(uid) = user_id {
69 state.user_ids.insert(uid);
70 }
71 if let Some((cid, kind)) = chat {
72 match kind {
73 ChatKind::Group | ChatKind::Supergroup => {
74 state.group_ids.insert(cid);
75 }
76 ChatKind::Channel => {
77 state.channel_ids.insert(cid);
78 }
79 ChatKind::Private => { }
80 }
81 }
82 }
83
84 pub fn start(&self, interval: Duration) {
87 let client = self.client.clone();
88 let username = self.username.clone();
89 let state = self.state.clone();
90
91 tokio::spawn(async move {
92 let mut ticker = time::interval(interval);
93 ticker.tick().await;
96 loop {
97 ticker.tick().await;
98 let payload = {
99 let s = state.lock().await;
100 StatsPayload {
101 member_count: Some(s.user_ids.len() as u64),
102 group_count: Some(s.group_ids.len() as u64),
103 channel_count: Some(s.channel_ids.len() as u64),
104 bot_serves: None,
105 }
106 };
107 match client.post_stats(&username, &payload).await {
108 Ok(_) => log::debug!("toptl: posted stats for @{username}"),
109 Err(e) => log::warn!("toptl: post_stats for @{username} failed: {e}"),
110 }
111 }
112 });
113 }
114
115 pub async fn has_voted(&self, user_id: i64) -> bool {
119 match self.client.has_voted(&self.username, user_id as u64).await {
120 Ok(check) => check.voted,
121 Err(e) => {
122 log::warn!("toptl: has_voted({user_id}) failed: {e}");
123 false
124 }
125 }
126 }
127
128 pub async fn post_now(&self) -> Result<(), toptl::Error> {
130 let payload = {
131 let s = self.state.lock().await;
132 StatsPayload {
133 member_count: Some(s.user_ids.len() as u64),
134 group_count: Some(s.group_ids.len() as u64),
135 channel_count: Some(s.channel_ids.len() as u64),
136 bot_serves: None,
137 }
138 };
139 self.client.post_stats(&self.username, &payload).await?;
140 Ok(())
141 }
142}
143
144#[cfg(feature = "teloxide")]
156pub async fn record_update(plugin: &TopTLPlugin, msg: &teloxide::types::Message) {
157 let user_id = msg.from.as_ref().map(|u| u.id.0 as i64);
158 let kind = match msg.chat.kind {
159 teloxide::types::ChatKind::Private(_) => ChatKind::Private,
160 teloxide::types::ChatKind::Public(ref p) => match p.kind {
161 teloxide::types::PublicChatKind::Group(_) => ChatKind::Group,
162 teloxide::types::PublicChatKind::Supergroup(_) => ChatKind::Supergroup,
163 teloxide::types::PublicChatKind::Channel(_) => ChatKind::Channel,
164 },
165 };
166 plugin.record(user_id, Some((msg.chat.id.0, kind))).await;
167}