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