Files
is-this-a-repost-bot/src/main.rs
Dylan Knutson e48651612e initial commit
2024-11-22 00:00:07 -08:00

170 lines
4.7 KiB
Rust

mod async_error;
mod db;
mod media_hash;
mod sql_types;
use async_error::AsyncError;
use clap::Parser as _;
use db::{get_db, Db};
use media_hash::MediaHash;
use teloxide::{
dispatching::UpdateFilterExt,
net::Download as _,
prelude::*,
types::{Chat, ChatMemberStatus, MediaKind, MessageCommon, MessageKind},
};
#[derive(clap::Parser)]
struct CommandLineArgs {
/// Path to the database file
#[clap(short, long)]
db_path: String,
}
#[tokio::main]
async fn main() -> Result<(), AsyncError> {
let args = CommandLineArgs::parse();
pretty_env_logger::init();
log::info!("Starting bot");
// open up the database
let db = get_db(&args.db_path).await?;
db.print_info().await?;
let bot = Bot::from_env();
let handler = dptree::entry()
.branch(Update::filter_my_chat_member().endpoint(handle_bot_status_change))
.branch(Update::filter_channel_post().endpoint(handle_channel_message))
.branch(Update::filter_message().endpoint(handle_direct_message));
log::info!("Starting dispatcher");
Dispatcher::builder(bot, handler)
.enable_ctrlc_handler()
.dependencies(dptree::deps![db])
.build()
.dispatch()
.await;
Ok(())
}
fn chat_name(chat: &Chat) -> &str {
chat.username().or(chat.title()).unwrap_or("<no name>")
}
async fn handle_bot_status_change(updated: ChatMemberUpdated, db: Db) -> Result<(), AsyncError> {
let status = updated.new_chat_member.status();
let by_user = updated.from.id;
let chat_id = updated.chat.id;
log::info!(
"Bot status in Chat({}) changed by User({}) to {:?}",
chat_id,
by_user,
status
);
match status {
ChatMemberStatus::Administrator => {
log::info!(
"Bot is now an administrator of Chat({}, {})",
chat_id,
chat_name(&updated.chat)
);
db.insert_channel_ownership(by_user, chat_id).await?;
}
ChatMemberStatus::Left => {
log::info!(
"Bot is no longer in Chat({}, {})",
chat_id,
chat_name(&updated.chat)
);
db.delete_channel_ownership(by_user, chat_id).await?;
}
_ => {}
}
Ok(())
}
async fn handle_direct_message(bot: Bot, message: Message, db: Db) -> Result<(), AsyncError> {
let from_user = message.from.clone().ok_or("No sender in direct message")?;
log::info!(
"Got direct message from User({}, {}): {}",
from_user.id,
from_user.username.as_deref().unwrap_or("<no username>"),
message.text().unwrap_or("<no text>")
);
let media_hash = download_message_photo(&bot, &message).await?;
if let Some(media_hash) = media_hash {
let results = db
.search_chat_messages(media_hash, from_user.id, 1, 1.0)
.await?;
log::info!("Found {} similar medias", results.len());
bot.send_message(
message.chat.id,
format!("Found {} similar medias", results.len()),
)
.await?;
for result in results {
log::info!(" {:?}", result);
bot.forward_message(message.chat.id, result.chat_id, result.message_id)
.await?;
}
}
Ok(())
}
async fn handle_channel_message(bot: Bot, message: Message, db: Db) -> Result<(), AsyncError> {
let chat_id = message.chat.id;
let media_hash = match download_message_photo(&bot, &message).await? {
Some(media_hash) => media_hash,
None => return Ok(()),
};
let media_id = db
.insert_chat_message(chat_id, message.id, media_hash)
.await?;
log::info!("Inserted message: {:?}", media_id);
Ok(())
}
async fn download_message_photo(
bot: &Bot,
message: &Message,
) -> Result<Option<MediaHash>, AsyncError> {
let photo = match &message.kind {
MessageKind::Common(MessageCommon {
media_kind: MediaKind::Photo(photo),
..
}) => {
let photo = match photo.photo.iter().max_by_key(|p| p.file.size) {
Some(photo) => photo,
None => {
log::info!("No photo found in message");
return Ok(None);
}
};
photo
}
_ => return Ok(None),
};
log::info!("Downloading photo {}...", photo.file.id,);
let file_path = bot.get_file(&photo.file.id).await?.path;
let mut dst = Vec::new();
bot.download_file(&file_path, &mut dst).await?;
log::info!(
"Downloaded {}.",
humansize::format_size(dst.len(), humansize::BINARY)
);
Ok(Some(MediaHash::from_bytes(&dst)?))
}