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/handlers/` - Message handlers (modular, each feature in own file)
|
||||||
- `src/utils/` - Shared utilities
|
- `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
|
## Adding New Handlers
|
||||||
|
|
||||||
1. Create `src/handlers/new_feature.rs`
|
1. Create `src/handlers/new_feature.rs`
|
||||||
|
|
@ -57,3 +76,11 @@ Strips tracking parameters from Instagram links:
|
||||||
- `ref` - Referral tracking
|
- `ref` - Referral tracking
|
||||||
- `fbclid` - Facebook click ID
|
- `fbclid` - Facebook click ID
|
||||||
- `si` - Session identifier
|
- `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::dispatching::UpdateHandler;
|
||||||
use teloxide::prelude::*;
|
use teloxide::prelude::*;
|
||||||
use teloxide::types::ReplyParameters;
|
use teloxide::types::ReplyParameters;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::config;
|
use crate::utils::message;
|
||||||
use crate::utils::url::strip_tracking_params;
|
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 {
|
fn is_instagram_host(url: &Url) -> bool {
|
||||||
url.host_str()
|
url.host_str()
|
||||||
.map(|h| h == "instagram.com" || h == "www.instagram.com")
|
.map(|h| h == "instagram.com" || h == "www.instagram.com")
|
||||||
|
|
@ -50,7 +26,7 @@ async fn handle_instagram_links(
|
||||||
bot: Bot,
|
bot: Bot,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> 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(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,12 +50,12 @@ async fn handle_instagram_links(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contains_instagram_url(msg: &Message) -> bool {
|
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>> {
|
pub fn handler() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||||
Update::filter_message()
|
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))
|
.filter(|msg: Message| contains_instagram_url(&msg))
|
||||||
.endpoint(handle_instagram_links)
|
.endpoint(handle_instagram_links)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,49 @@
|
||||||
pub mod instagram;
|
pub mod instagram;
|
||||||
|
pub mod tag_all;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use teloxide::dispatching::UpdateHandler;
|
use teloxide::dispatching::UpdateHandler;
|
||||||
use teloxide::prelude::*;
|
use teloxide::prelude::*;
|
||||||
|
|
||||||
pub fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
|
use crate::config;
|
||||||
dptree::entry().branch(instagram::handler())
|
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 config;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
|
mod state;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use teloxide::prelude::*;
|
use teloxide::prelude::*;
|
||||||
|
|
@ -12,7 +13,7 @@ async fn main() {
|
||||||
let token = config::load_token();
|
let token = config::load_token();
|
||||||
let bot = Bot::new(token);
|
let bot = Bot::new(token);
|
||||||
|
|
||||||
handlers::instagram::init();
|
handlers::init();
|
||||||
|
|
||||||
Dispatcher::builder(bot, handlers::schema())
|
Dispatcher::builder(bot, handlers::schema())
|
||||||
.enable_ctrlc_handler()
|
.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;
|
pub mod url;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue