add tag all function

This commit is contained in:
Fabian Montero 2026-05-16 19:26:12 -06:00
parent 3ff112b1cd
commit 6e59919c62
Signed by: fabian
GPG key ID: 3EDA9AE3937CCDE3
8 changed files with 141 additions and 31 deletions

View file

@ -41,6 +41,25 @@ The bot ignores messages from chats not in this list.
- `src/handlers/` - Message handlers (modular, each feature in own file)
- `src/utils/` - Shared utilities
## NixOS Deployment
Module location: `/home/fabian/nix/sys/modules/task-force-beta-bot.nix`
```nix
local.sys.task-force-beta-bot = {
enable = true;
tokenFile = "/var/trust/task_force_beta_bot/telegram_token";
allowedChats = [ (-1001234567890) ]; # Negative IDs for groups
};
```
The module:
- Fetches and builds the package from git
- Generates the allowed chats file in the nix store
- Runs as a hardened systemd service (`task-force-beta-bot.service`)
After code changes: update `rev` and `hash` in the module's `fetchgit` block.
## Adding New Handlers
1. Create `src/handlers/new_feature.rs`
@ -57,3 +76,11 @@ Strips tracking parameters from Instagram links:
- `ref` - Referral tracking
- `fbclid` - Facebook click ID
- `si` - Session identifier
### Tag All (`handlers/tag_all.rs`)
WhatsApp-style @all mentions. When a message contains "@all", bot replies tagging everyone it has seen.
- Tracks users by observing messages (requires privacy mode disabled)
- In-memory only - resets on restart
- Uses MarkdownV2 text mentions

View file

@ -1,35 +1,11 @@
use once_cell::sync::Lazy;
use teloxide::dispatching::UpdateHandler;
use teloxide::prelude::*;
use teloxide::types::ReplyParameters;
use url::Url;
use crate::config;
use crate::utils::message;
use crate::utils::url::strip_tracking_params;
static ALLOWED_CHATS: Lazy<Vec<i64>> = Lazy::new(config::load_allowed_chats);
/// Force initialization and log the allowed chat IDs. Call at startup.
pub fn init() {
let chats = &*ALLOWED_CHATS;
log::info!("Loaded {} allowed chat IDs", chats.len());
for chat_id in chats {
log::debug!(" Allowed chat: {}", chat_id);
}
}
fn is_chat_allowed(chat_id: i64) -> bool {
let allowed = ALLOWED_CHATS.contains(&chat_id);
if !allowed {
log::debug!("Message from unauthorized chat {}, ignoring", chat_id);
}
allowed
}
fn get_message_text(msg: &Message) -> Option<&str> {
msg.text().or_else(|| msg.caption())
}
fn is_instagram_host(url: &Url) -> bool {
url.host_str()
.map(|h| h == "instagram.com" || h == "www.instagram.com")
@ -50,7 +26,7 @@ async fn handle_instagram_links(
bot: Bot,
msg: Message,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let Some(text) = get_message_text(&msg) else {
let Some(text) = message::get_text(&msg) else {
return Ok(());
};
@ -74,12 +50,12 @@ async fn handle_instagram_links(
}
fn contains_instagram_url(msg: &Message) -> bool {
get_message_text(msg).map_or(false, |text| !extract_instagram_urls(text).is_empty())
message::get_text(msg).map_or(false, |text| !extract_instagram_urls(text).is_empty())
}
pub fn handler() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
Update::filter_message()
.filter(|msg: Message| is_chat_allowed(msg.chat.id.0))
.filter(|msg: Message| super::is_chat_allowed(msg.chat.id.0))
.filter(|msg: Message| contains_instagram_url(&msg))
.endpoint(handle_instagram_links)
}

View file

@ -1,8 +1,49 @@
pub mod instagram;
pub mod tag_all;
use once_cell::sync::Lazy;
use teloxide::dispatching::UpdateHandler;
use teloxide::prelude::*;
pub fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
dptree::entry().branch(instagram::handler())
use crate::config;
use crate::state;
static ALLOWED_CHATS: Lazy<Vec<i64>> = Lazy::new(config::load_allowed_chats);
pub fn init() {
let chats = &*ALLOWED_CHATS;
log::info!("Loaded {} allowed chat IDs", chats.len());
for chat_id in chats {
log::debug!(" Allowed chat: {}", chat_id);
}
}
pub fn is_chat_allowed(chat_id: i64) -> bool {
let allowed = ALLOWED_CHATS.contains(&chat_id);
if !allowed {
log::debug!("Message from unauthorized chat {}, ignoring", chat_id);
}
allowed
}
fn user_tracker() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
Update::filter_message()
.filter_map(|msg: Message| {
if is_chat_allowed(msg.chat.id.0) {
if let Some(user) = msg.from.as_ref() {
if let Some(username) = &user.username {
state::track_user(msg.chat.id.0, username.clone());
}
}
}
None::<Message>
})
.endpoint(|_: Message| async { Ok(()) })
}
pub fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
dptree::entry()
.branch(user_tracker())
.branch(instagram::handler())
.branch(tag_all::handler())
}

39
src/handlers/tag_all.rs Normal file
View file

@ -0,0 +1,39 @@
use teloxide::dispatching::UpdateHandler;
use teloxide::prelude::*;
use teloxide::types::ReplyParameters;
use crate::state;
use crate::utils::message;
fn contains_at_all(msg: &Message) -> bool {
message::get_text(msg).map_or(false, |text| text.contains("@all"))
}
async fn handle_tag_all(
bot: Bot,
msg: Message,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let users = state::get_tracked_users(msg.chat.id.0);
if users.is_empty() {
bot.send_message(msg.chat.id, "No users tracked yet.")
.reply_parameters(ReplyParameters::new(msg.id))
.await?;
return Ok(());
}
let mentions: Vec<String> = users.iter().map(|u| format!("@{}", u)).collect();
bot.send_message(msg.chat.id, mentions.join(" "))
.reply_parameters(ReplyParameters::new(msg.id))
.await?;
Ok(())
}
pub fn handler() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
Update::filter_message()
.filter(|msg: Message| super::is_chat_allowed(msg.chat.id.0))
.filter(|msg: Message| contains_at_all(&msg))
.endpoint(handle_tag_all)
}

View file

@ -1,5 +1,6 @@
mod config;
mod handlers;
mod state;
mod utils;
use teloxide::prelude::*;
@ -12,7 +13,7 @@ async fn main() {
let token = config::load_token();
let bot = Bot::new(token);
handlers::instagram::init();
handlers::init();
Dispatcher::builder(bot, handlers::schema())
.enable_ctrlc_handler()

20
src/state.rs Normal file
View file

@ -0,0 +1,20 @@
use once_cell::sync::Lazy;
use std::collections::{HashMap, HashSet};
use std::sync::RwLock;
static SEEN_USERS: Lazy<RwLock<HashMap<i64, HashSet<String>>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
pub fn track_user(chat_id: i64, username: String) {
let mut map = SEEN_USERS.write().unwrap();
map.entry(chat_id).or_default().insert(username);
}
pub fn get_tracked_users(chat_id: i64) -> Vec<String> {
SEEN_USERS
.read()
.unwrap()
.get(&chat_id)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default()
}

5
src/utils/message.rs Normal file
View file

@ -0,0 +1,5 @@
use teloxide::types::Message;
pub fn get_text(msg: &Message) -> Option<&str> {
msg.text().or_else(|| msg.caption())
}

View file

@ -1 +1,2 @@
pub mod message;
pub mod url;