add tag all function
This commit is contained in:
parent
3ff112b1cd
commit
6e59919c62
8 changed files with 141 additions and 31 deletions
27
CLAUDE.md
27
CLAUDE.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
39
src/handlers/tag_all.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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
20
src/state.rs
Normal 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
5
src/utils/message.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
use teloxide::types::Message;
|
||||
|
||||
pub fn get_text(msg: &Message) -> Option<&str> {
|
||||
msg.text().or_else(|| msg.caption())
|
||||
}
|
||||
|
|
@ -1 +1,2 @@
|
|||
pub mod message;
|
||||
pub mod url;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue