initial commit
This commit is contained in:
commit
a4b8bdd8c0
10 changed files with 2644 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
38
CLAUDE.md
Normal file
38
CLAUDE.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Telegram Bot
|
||||
|
||||
General-purpose Telegram bot with modular handler architecture.
|
||||
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
cargo run
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Bot token must be placed at: `/var/trust/instagram_link_stripper/telegram_token`
|
||||
|
||||
## Architecture
|
||||
|
||||
- `src/main.rs` - Entry point, dispatcher setup
|
||||
- `src/config.rs` - Token loading from /var/trust/
|
||||
- `src/handlers/` - Message handlers (modular, each feature in own file)
|
||||
- `src/utils/` - Shared utilities
|
||||
|
||||
## Adding New Handlers
|
||||
|
||||
1. Create `src/handlers/new_feature.rs`
|
||||
2. Export in `src/handlers/mod.rs`
|
||||
3. Add handler branch in `handlers::schema()`
|
||||
|
||||
## Current Features
|
||||
|
||||
### Instagram Link Cleaning (`handlers/instagram.rs`)
|
||||
|
||||
Strips tracking parameters from Instagram links:
|
||||
- `igsh`, `igshid` - Instagram share tracking
|
||||
- `utm_*` - UTM campaign tracking
|
||||
- `ref` - Referral tracking
|
||||
- `fbclid` - Facebook click ID
|
||||
- `si` - Session identifier
|
||||
2409
Cargo.lock
generated
Normal file
2409
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "instagram_link_stripper"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
teloxide = { version = "0.17", default-features = false, features = ["macros", "rustls", "ctrlc_handler"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
url = "2"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
18
src/config.rs
Normal file
18
src/config.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
const TOKEN_PATH: &str = "/var/trust/instagram_link_stripper/telegram_token";
|
||||
|
||||
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
|
||||
)
|
||||
})
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
56
src/handlers/instagram.rs
Normal file
56
src/handlers/instagram.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use teloxide::dispatching::UpdateHandler;
|
||||
use teloxide::prelude::*;
|
||||
use teloxide::types::ReplyParameters;
|
||||
|
||||
use crate::utils::url::strip_tracking_params;
|
||||
|
||||
const INSTAGRAM_PATTERN: &str = "instagram.com/";
|
||||
|
||||
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 extract_instagram_urls(text: &str) -> Vec<&str> {
|
||||
text.split_whitespace()
|
||||
.filter(|word| word.contains(INSTAGRAM_PATTERN))
|
||||
.collect()
|
||||
}
|
||||
|
||||
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 {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let urls = extract_instagram_urls(text);
|
||||
let cleaned: Vec<String> = urls
|
||||
.into_iter()
|
||||
.filter_map(strip_tracking_params)
|
||||
.collect();
|
||||
|
||||
if cleaned.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let reply_text = cleaned.join("\n");
|
||||
|
||||
bot.send_message(msg.chat.id, reply_text)
|
||||
.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| {
|
||||
get_message_text(&msg).map_or(false, contains_instagram_link)
|
||||
})
|
||||
.endpoint(handle_instagram_links)
|
||||
}
|
||||
8
src/handlers/mod.rs
Normal file
8
src/handlers/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
pub mod instagram;
|
||||
|
||||
use teloxide::dispatching::UpdateHandler;
|
||||
use teloxide::prelude::*;
|
||||
|
||||
pub fn schema() -> UpdateHandler<Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||
dptree::entry().branch(instagram::handler())
|
||||
}
|
||||
20
src/main.rs
Normal file
20
src/main.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
mod config;
|
||||
mod handlers;
|
||||
mod utils;
|
||||
|
||||
use teloxide::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
pretty_env_logger::init();
|
||||
log::info!("Starting bot...");
|
||||
|
||||
let token = config::load_token();
|
||||
let bot = Bot::new(token);
|
||||
|
||||
Dispatcher::builder(bot, handlers::schema())
|
||||
.enable_ctrlc_handler()
|
||||
.build()
|
||||
.dispatch()
|
||||
.await;
|
||||
}
|
||||
1
src/utils/mod.rs
Normal file
1
src/utils/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod url;
|
||||
82
src/utils/url.rs
Normal file
82
src/utils/url.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use url::Url;
|
||||
|
||||
const TRACKING_PARAMS: &[&str] = &[
|
||||
"igsh",
|
||||
"igshid",
|
||||
"utm_source",
|
||||
"utm_medium",
|
||||
"utm_campaign",
|
||||
"utm_content",
|
||||
"utm_term",
|
||||
"ref",
|
||||
"fbclid",
|
||||
"si",
|
||||
];
|
||||
|
||||
/// Removes tracking parameters from a URL.
|
||||
/// Returns Some(cleaned_url) if parameters were removed, None if URL was already clean.
|
||||
pub fn strip_tracking_params(url_str: &str) -> Option<String> {
|
||||
let mut url = Url::parse(url_str).ok()?;
|
||||
|
||||
let original_query = url.query().map(String::from);
|
||||
|
||||
let filtered_params: Vec<(String, String)> = url
|
||||
.query_pairs()
|
||||
.filter(|(key, _)| !TRACKING_PARAMS.contains(&key.as_ref()))
|
||||
.map(|(k, v)| (k.into_owned(), v.into_owned()))
|
||||
.collect();
|
||||
|
||||
if filtered_params.is_empty() {
|
||||
url.set_query(None);
|
||||
} else {
|
||||
url.query_pairs_mut()
|
||||
.clear()
|
||||
.extend_pairs(filtered_params);
|
||||
}
|
||||
|
||||
let new_query = url.query().map(String::from);
|
||||
|
||||
if original_query != new_query {
|
||||
Some(url.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_strips_igsh() {
|
||||
let url = "https://www.instagram.com/reel/ABC123/?igsh=xyz789";
|
||||
let cleaned = strip_tracking_params(url).unwrap();
|
||||
assert_eq!(cleaned, "https://www.instagram.com/reel/ABC123/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strips_multiple_params() {
|
||||
let url = "https://www.instagram.com/p/ABC/?igsh=xyz&utm_source=share&fbclid=123";
|
||||
let cleaned = strip_tracking_params(url).unwrap();
|
||||
assert_eq!(cleaned, "https://www.instagram.com/p/ABC/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserves_non_tracking_params() {
|
||||
let url = "https://www.instagram.com/p/ABC/?img_index=2&igsh=xyz";
|
||||
let cleaned = strip_tracking_params(url).unwrap();
|
||||
assert_eq!(cleaned, "https://www.instagram.com/p/ABC/?img_index=2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_returns_none_if_already_clean() {
|
||||
let url = "https://www.instagram.com/reel/ABC123/";
|
||||
assert!(strip_tracking_params(url).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_returns_none_for_clean_url_with_non_tracking_params() {
|
||||
let url = "https://www.instagram.com/p/ABC/?img_index=2";
|
||||
assert!(strip_tracking_params(url).is_none());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue