#![feature(control_flow_enum)] use eh2telegraph::{ collector::Registry, config::{self}, http_proxy::ProxiedClient, storage, sync::Synchronizer, telegraph::Telegraph, }; use clap::Parser; use teloxide::{ adaptors::DefaultParseMode, dispatching::update_listeners, error_handlers::IgnoringErrorHandler, prelude2::*, types::{AllowedUpdate, ChatPermissions, ParseMode, UpdateKind}, }; use handler::{Command, Handler}; use crate::{ handler::AdminCommand, util::{wrap_endpoint, PrettyChat}, }; mod handler; mod util; mod version; #[derive(Debug, serde::Deserialize)] pub struct BaseConfig { pub bot_token: String, pub telegraph: TelegraphConfig, #[serde(default)] pub admins: Vec, } #[derive(Debug, serde::Deserialize)] pub struct TelegraphConfig { pub tokens: Vec, pub author_name: Option, pub author_url: Option, } #[derive(Parser, Debug)] #[clap(author, version=version::VERSION, about, long_about = "eh2telegraph sync bot")] struct Args { #[clap(short, long, help = "Config file path")] config: Option, } #[tokio::main] async fn main() { let args = Args::parse(); let timer = tracing_subscriber::fmt::time::LocalTime::new(time::macros::format_description!( "[month]-[day] [hour]:[minute]:[second]" )); tracing_subscriber::fmt().with_timer(timer).init(); tracing::info!("initializing..."); config::init(args.config); let base_config: BaseConfig = config::parse("base") .expect("unable to parse base config") .expect("base config can not be empty"); let telegraph_config = base_config.telegraph; let telegraph = Telegraph::new(telegraph_config.tokens).with_proxy(ProxiedClient::new_from_config()); let registry = Registry::new_from_config(); #[cfg(debug_assertions)] let cache = storage::SimpleMemStorage::default(); #[cfg(not(debug_assertions))] let cache = storage::cloudflare_kv::CFStorage::new_from_config().expect("unable to build storage"); let mut synchronizer = Synchronizer::new(telegraph, registry, cache); if telegraph_config.author_name.is_some() { synchronizer = synchronizer.with_author(telegraph_config.author_name, telegraph_config.author_url); } let admins = base_config.admins.into_iter().collect(); let handler = Box::leak(Box::new(Handler::new(synchronizer, admins))) as &Handler<_>; // === Bot related === let command_handler = move |bot: AutoSend>, message: Message, command: Command| async move { handler.respond_cmd(bot, message, command).await }; let admin_command_handler = move |bot: AutoSend>, message: Message, command: AdminCommand| async move { handler.respond_admin_cmd(bot, message, command).await }; let text_handler = move |bot: AutoSend>, message: Message| async move { handler.respond_text(bot, message).await }; let caption_handler = move |bot: AutoSend>, message: Message| async move { handler.respond_caption(bot, message).await }; let photo_handler = move |bot: AutoSend>, message: Message| async move { handler.respond_photo(bot, message).await }; let default_handler = move |bot: AutoSend>, message: Message| async move { handler.respond_default(bot, message).await }; let permission_filter = |bot: AutoSend>, message: Message| async move { // If the bot is blocked, we will leave chat and not respond. let blocked = message .chat .permissions() .map(|p| !p.contains(ChatPermissions::SEND_MESSAGES)) .unwrap_or_default(); if blocked { tracing::info!( "[permission filter] leave chat {:?}", PrettyChat(&message.chat) ); let _ = bot.leave_chat(message.chat.id).await; None } else { Some(message) } }; let bot = Bot::new(base_config.bot_token) .parse_mode(ParseMode::MarkdownV2) .auto_send(); let mut bot_dispatcher = Dispatcher::builder( bot.clone(), dptree::entry() .chain(dptree::filter_map(move |update: Update| { match update.kind { UpdateKind::Message(x) | UpdateKind::EditedMessage(x) => Some(x), _ => None, } })) .chain(dptree::filter_map_async(permission_filter)) .branch( dptree::entry() .chain(dptree::filter(move |message: Message| { handler.admins.contains(&message.chat.id) })) .filter_command::() .branch(wrap_endpoint(admin_command_handler)), ) .branch( dptree::entry() .filter_command::() .branch(wrap_endpoint(command_handler)), ) .branch( dptree::entry() .chain(dptree::filter_map(move |message: Message| { // Ownership mechanism does not allow using map. #[allow(clippy::manual_map)] match message.text() { Some(v) if !v.is_empty() => Some(message), _ => None, } })) .branch(wrap_endpoint(text_handler)), ) .branch( dptree::entry() .chain(dptree::filter_map(move |message: Message| { // Ownership mechanism does not allow using map. #[allow(clippy::manual_map)] match message.caption_entities() { Some(v) if !v.is_empty() => Some(message), _ => None, } })) .branch(wrap_endpoint(caption_handler)), ) .branch( dptree::entry() .chain(dptree::filter_map(move |message: Message| { // Ownership mechanism does not allow using map. #[allow(clippy::manual_map)] match message.photo() { Some(v) if !v.is_empty() => Some(message), _ => None, } })) .branch(wrap_endpoint(photo_handler)), ) .branch(wrap_endpoint(default_handler)), ) .default_handler(Box::new(|_upd| { #[cfg(debug_assertions)] tracing::warn!("Unhandled update: {:?}", _upd); Box::pin(async {}) })) .error_handler(std::sync::Arc::new(IgnoringErrorHandler)) .build(); bot_dispatcher.setup_ctrlc_handler(); let bot_listener = update_listeners::polling( bot, Some(std::time::Duration::from_secs(10)), None, Some(vec![AllowedUpdate::Message]), ); tracing::info!("initializing finished, bot is running"); bot_dispatcher .dispatch_with_listener( bot_listener, LoggingErrorHandler::with_custom_text("An error from the update listener"), ) .await; }