Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ edition = "2024"

[features]
default = ["telegram"]
telegram = ["dep:teloxide", "twitter"]
telegram = ["dep:teloxide", "twitter", "tiktok"]
twitter = []
tiktok = []

[dependencies]
async-trait = "0.1.89"
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A small project to explore the world of Telegram, Discord, Instagram, X (formerl

## ☀️ Overview

Bot-RS is a cross-platform bot framework that scrapes real-time data from Instagram, X (formerly Twitter), and TikTok, delivering it to users through stylish and intuitive bots — whether you’re chatting on Telegram or hanging out on Discord.
Bot-RS is a cross-platform bot framework that scrapes real-time data from Instagram, X, and TikTok, delivering it to users through stylish and intuitive bots — whether you’re chatting on Telegram or hanging out on Discord.

## 🚀 Quick Start

Expand All @@ -24,7 +24,8 @@ cargo run
Once the bot is running, you can interact with it using the following commands:

* `/help` (aliases: `/h`, `/?`) - Display available commands
* `/twitter <url>` (alias: `/t`) - Download media from a X (Twitter) post
* `/twitter <url>` (alias: `/t`) - Download media from a X post
* `/tiktok <url>` (alias: `/tk`) - Download media from a TikTok post

## 🌍 Why this project?

Expand Down
111 changes: 56 additions & 55 deletions src/core/error.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,77 @@
#![allow(unused_macros, unused_imports)]
use std::fmt;

// --- Structs and Enums ---

#[allow(dead_code)]
#[derive(Debug)]
pub enum BotError {
CommandNotFound,
NoMediaFound,
InvalidLink,
InvalidUrl,
MediaSendFailed,
InvalidScraperResponse,
FileTypeNotSupported,
InvalidMedia,
Unknown,
Custom(String),
}

// --- Type Aliases ---

pub type BotResult<T> = Result<T, BotError>;

// --- Trait Impl ---

impl fmt::Display for BotError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = match self {
BotError::CommandNotFound => "Command not found.",
BotError::NoMediaFound => {
"No media items found for this link. The post might be private or not contain any media."
}
BotError::InvalidLink => "Invalid link.",
BotError::InvalidUrl => "Invalid URL.",
BotError::MediaSendFailed => {
"Failed to send media. The media might be unavailable or the format unsupported."
}
BotError::InvalidScraperResponse => {
"The scraper returned an unexpected response. The link might be invalid or the content might be unavailable."
}
BotError::FileTypeNotSupported => "The media format is not currently supported.",
BotError::InvalidMedia => "The media might be corrupted or in an unrecognized format.",
BotError::Unknown => "An unexpected error occurred, please retry later...",
BotError::Custom(msg) => msg,
};

write!(f, "{msg}")
}
}

impl std::error::Error for BotError {}

// --- Macros ---

macro_rules! error {
($err:expr, $fmt:expr, $($arg:expr),* $(,)?) => {{
($err:expr, $fmt:literal, $($arg:expr),* $(,)?) => {{
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The macro pattern expects a literal string ($fmt:literal) for the format string, which is more restrictive than the previous $fmt:expr. While this is safer for compile-time format string validation, it means you cannot use dynamic format strings. Ensure all call sites use string literals and not expressions that evaluate to strings.

Suggested change
($err:expr, $fmt:literal, $($arg:expr),* $(,)?) => {{
($err:expr, $fmt:expr, $($arg:expr),* $(,)?) => {{

Copilot uses AI. Check for mistakes.
let err = $err;
let enum_variant = format!("{err:?}");
let cause = format!($fmt, $($arg,)*);
::tracing::error!("{enum_variant}: {cause}");
::tracing::warn!("{enum_variant}: {cause}");
err
}};

($err:expr, $cause:expr $(,)?) => {{
let err = $err;
let enum_variant = format!("{err:?}");
let cause = format!($cause);
::tracing::error!("{enum_variant}: {cause}");
let cause = $cause.to_string();
::tracing::warn!("{enum_variant}: {cause}");
err
}};
($err:expr $(,)?) => {{
let err = $err;
::tracing::error!("{err:?}");
::tracing::warn!("{err:?}");
err
}};
}
Expand Down Expand Up @@ -86,53 +137,3 @@ pub(crate) use invalid_url;
pub(crate) use media_send_failed;
pub(crate) use no_media_found;
pub(crate) use unknown;

// --- Structs and Enums ---

#[allow(dead_code)]
#[derive(Debug)]
pub enum BotError {
CommandNotFound,
NoMediaFound,
InvalidLink,
InvalidUrl,
MediaSendFailed,
InvalidScraperResponse,
FileTypeNotSupported,
InvalidMedia,
Unknown,
Custom(String),
}

// --- Type Aliases ---

pub type BotResult<T> = Result<T, BotError>;

// --- Trait Impl ---

impl fmt::Display for BotError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let msg = match self {
BotError::CommandNotFound => "Command not found.",
BotError::NoMediaFound => {
"No media items found for this link. The post might be private or not contain any media."
}
BotError::InvalidLink => "Invalid link.",
BotError::InvalidUrl => "Invalid URL.",
BotError::MediaSendFailed => {
"Failed to send media. The media might be unavailable or the format unsupported."
}
BotError::InvalidScraperResponse => {
"The scraper returned an unexpected response. The link might be invalid or the content might be unavailable."
}
BotError::FileTypeNotSupported => "The media format is not currently supported.",
BotError::InvalidMedia => "The media might be corrupted or in an unrecognized format.",
BotError::Unknown => "An unexpected error occured, please retry later...",
BotError::Custom(msg) => msg,
};

write!(f, "{msg}")
}
}

impl std::error::Error for BotError {}
2 changes: 2 additions & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(unused_imports)]

pub mod error;
pub mod traits;
pub mod types;
Expand Down
2 changes: 1 addition & 1 deletion src/core/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::core::{BotResult, MediaMetadata};
pub trait MediaScraper {
type Input;

async fn scrape(input: Self::Input) -> BotResult<Vec<BotResult<MediaMetadata>>>;
async fn get_medias(input: Self::Input) -> BotResult<Vec<BotResult<MediaMetadata>>>;
}

#[async_trait]
Expand Down
8 changes: 6 additions & 2 deletions src/core/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ pub enum MediaKind {
Video,
}

#[allow(dead_code)]
#[derive(Debug)]
pub struct MediaMetadata {
pub id: String,
pub kind: MediaKind,
pub url: Url,
}

impl MediaMetadata {
pub fn new(kind: MediaKind, url: Url) -> Self {
Self { kind, url }
}
}

impl std::fmt::Display for MediaMetadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}({})", self.kind, self.url.as_str())
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ pub mod telegram;
#[cfg(feature = "twitter")]
pub mod twitter;

#[cfg(feature = "tiktok")]
pub mod tiktok;

pub mod prelude {
pub use crate::core::error::{BotError, BotResult};
pub use crate::core::traits::{MediaScraper, MediaSender};
Expand Down
30 changes: 21 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,38 @@ use tokio::task::JoinSet;

#[cfg(feature = "telegram")]
use media_bot::telegram::TelegramBot;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() {
// load env files
dotenvy::dotenv().ok();

tracing_subscriber::fmt()
// .with_env_filter(EnvFilter::from_default_env())
.with_env_filter(tracing_subscriber::EnvFilter::new("media_bot=trace"))
.pretty()
.with_line_number(true)
.with_target(true) // Include module target in logs
.init();
// enable tracing logs
tracing_subscriber().init();

#[allow(unused_mut)]
let mut jobs: JoinSet<()> = JoinSet::new();

#[cfg(feature = "telegram")]
{
let telegram_bot = TelegramBot::new();
jobs.spawn(async move { telegram_bot.run().await });
jobs.spawn(TelegramBot::run());
}

jobs.join_all().await;
}

fn tracing_subscriber() -> impl SubscriberInitExt {
let filter_layer = tracing_subscriber::filter::Targets::new()
// .with_filter(EnvFilter::from_default_env());
.with_target("media_bot", tracing::Level::DEBUG);

let fmt_layer = tracing_subscriber::fmt::layer()
.pretty()
.with_line_number(true)
.with_target(true); // Include module target in logs

tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
}
Loading