make nix module

This commit is contained in:
Fabian Montero 2026-05-16 14:25:20 -06:00
parent a4b8bdd8c0
commit 3ff112b1cd
Signed by: fabian
GPG key ID: 3EDA9AE3937CCDE3
6 changed files with 96 additions and 35 deletions

View file

@ -1,4 +1,4 @@
# Telegram Bot
# Task Force Beta Bot
General-purpose Telegram bot with modular handler architecture.
@ -6,17 +6,38 @@ General-purpose Telegram bot with modular handler architecture.
```bash
cargo build --release
cargo run
```
## Configuration
## Usage
Bot token must be placed at: `/var/trust/instagram_link_stripper/telegram_token`
```
task_force_beta_bot <token_file> <allowed_chats_file>
```
Both arguments are required:
- `token_file` - Path to file containing the Telegram bot token
- `allowed_chats_file` - Path to file containing allowed chat IDs
Example:
```bash
cargo run -- /path/to/token /path/to/allowed_chats
```
### Allowed Chats File
One chat ID per line. Lines starting with `#` are comments.
```
# My group
-100123456789
```
The bot ignores messages from chats not in this list.
## Architecture
- `src/main.rs` - Entry point, dispatcher setup
- `src/config.rs` - Token loading from /var/trust/
- `src/config.rs` - Token and allowed chats loading from CLI arguments
- `src/handlers/` - Message handlers (modular, each feature in own file)
- `src/utils/` - Shared utilities

23
Cargo.lock generated
View file

@ -757,17 +757,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "instagram_link_stripper"
version = "0.1.0"
dependencies = [
"log",
"pretty_env_logger",
"teloxide",
"tokio",
"url",
]
[[package]]
name = "ipnet"
version = "2.12.0"
@ -1536,6 +1525,18 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e"
[[package]]
name = "task_force_beta_bot"
version = "0.1.0"
dependencies = [
"log",
"once_cell",
"pretty_env_logger",
"teloxide",
"tokio",
"url",
]
[[package]]
name = "teloxide"
version = "0.17.0"

View file

@ -1,5 +1,5 @@
[package]
name = "instagram_link_stripper"
name = "task_force_beta_bot"
version = "0.1.0"
edition = "2024"
@ -9,3 +9,4 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
url = "2"
log = "0.4"
pretty_env_logger = "0.5"
once_cell = "1"

View file

@ -1,18 +1,25 @@
use std::fs;
use std::path::Path;
const TOKEN_PATH: &str = "/var/trust/instagram_link_stripper/telegram_token";
fn get_required_arg(n: usize, name: &str) -> String {
std::env::args()
.nth(n)
.unwrap_or_else(|| panic!("Missing required argument {}: {}", n, name))
}
pub fn load_token() -> String {
let path = Path::new(TOKEN_PATH);
fs::read_to_string(path)
.unwrap_or_else(|e| {
panic!(
"Failed to read Telegram bot token from {}: {}",
TOKEN_PATH, e
)
})
let path = get_required_arg(1, "token_file");
fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Failed to read token"))
.trim()
.to_string()
}
pub fn load_allowed_chats() -> Vec<i64> {
let path = get_required_arg(2, "allowed_chats_file");
fs::read_to_string(&path)
.unwrap_or_else(|_| panic!("Failed to read allowed chats"))
.lines()
.filter(|line| !line.trim().is_empty() && !line.trim().starts_with('#'))
.filter_map(|line| line.trim().parse::<i64>().ok())
.collect()
}

View file

@ -1,22 +1,48 @@
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::url::strip_tracking_params;
const INSTAGRAM_PATTERN: &str = "instagram.com/";
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 contains_instagram_link(text: &str) -> bool {
text.contains(INSTAGRAM_PATTERN)
fn is_instagram_host(url: &Url) -> bool {
url.host_str()
.map(|h| h == "instagram.com" || h == "www.instagram.com")
.unwrap_or(false)
}
fn extract_instagram_urls(text: &str) -> Vec<&str> {
text.split_whitespace()
.filter(|word| word.contains(INSTAGRAM_PATTERN))
.filter(|word| {
Url::parse(word)
.map(|u| is_instagram_host(&u))
.unwrap_or(false)
})
.collect()
}
@ -47,10 +73,13 @@ async fn handle_instagram_links(
Ok(())
}
fn contains_instagram_url(msg: &Message) -> bool {
get_message_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| {
get_message_text(&msg).map_or(false, contains_instagram_link)
})
.filter(|msg: Message| is_chat_allowed(msg.chat.id.0))
.filter(|msg: Message| contains_instagram_url(&msg))
.endpoint(handle_instagram_links)
}

View file

@ -12,6 +12,8 @@ async fn main() {
let token = config::load_token();
let bot = Bot::new(token);
handlers::instagram::init();
Dispatcher::builder(bot, handlers::schema())
.enable_ctrlc_handler()
.build()