foxbot/src/handlers/channel_photo.rs

252 lines
7.9 KiB
Rust

use async_trait::async_trait;
use telegram::*;
use super::Status::*;
use crate::needs_field;
use crate::utils::{find_best_photo, get_message, match_image};
// TODO: Configuration options
// It should be possible to:
// * Link to multiple sources (change button to source name)
// * Edit messages with artist names
// * Configure localization for channel
pub struct ChannelPhotoHandler;
#[async_trait]
impl super::Handler for ChannelPhotoHandler {
fn name(&self) -> &'static str {
"channel"
}
async fn handle(
&self,
handler: &crate::MessageHandler,
update: &Update,
_command: Option<&Command>,
) -> failure::Fallible<super::Status> {
// Ensure we have a channel_post Message and a photo within.
let message = needs_field!(update, channel_post);
let sizes = needs_field!(&message, photo);
// We only want messages from channels. I think this is always true
// because this came from a channel_post.
if message.chat.chat_type != ChatType::Channel {
return Ok(Ignored);
}
// We can't edit forwarded messages, so we have to ignore.
if message.forward_date.is_some() {
return Ok(Completed);
}
let first = match get_matches(&handler.bot, &handler.fapi, &handler.conn, &sizes).await? {
Some(first) => first,
_ => return Ok(Completed),
};
// If this link was already in the message, we can ignore it.
if link_was_seen(&extract_links(&message, &handler.finder), &first.url) {
return Ok(Completed);
}
// If this photo was part of a media group, we should set a caption on
// the image because we can't make an inline keyboard on it.
if message.media_group_id.is_some() {
let edit_caption_markup = EditMessageCaption {
chat_id: message.chat_id(),
message_id: Some(message.message_id),
caption: Some(first.url()),
..Default::default()
};
handler.bot.make_request(&edit_caption_markup).await?;
// Not a media group, we should create an inline keyboard.
} else {
let text = handler
.get_fluent_bundle(None, |bundle| {
get_message(&bundle, "inline-source", None).unwrap()
})
.await;
let markup = InlineKeyboardMarkup {
inline_keyboard: vec![vec![InlineKeyboardButton {
text,
url: Some(first.url()),
..Default::default()
}]],
};
let edit_reply_markup = EditMessageReplyMarkup {
chat_id: message.chat_id(),
message_id: Some(message.message_id),
reply_markup: Some(ReplyMarkup::InlineKeyboardMarkup(markup)),
..Default::default()
};
handler.bot.make_request(&edit_reply_markup).await?;
}
Ok(Completed)
}
}
/// Extract all possible links from a Message. It looks at the text,
/// caption, and all buttons within an inline keyboard.
pub fn extract_links<'m>(
message: &'m Message,
finder: &linkify::LinkFinder,
) -> Vec<linkify::Link<'m>> {
let mut links = vec![];
// Unlikely to be text posts here, but we'll consider anyway.
if let Some(ref text) = message.text {
links.extend(finder.links(&text));
}
// Links could be in an image caption.
if let Some(ref caption) = message.caption {
links.extend(finder.links(&caption));
}
// See if it was posted with a bot that included an inline keyboard.
if let Some(ref markup) = message.reply_markup {
for row in &markup.inline_keyboard {
for button in row {
if let Some(url) = &button.url {
links.extend(finder.links(&url));
}
}
}
}
// Possible there's links generated by bots (or users).
if let Some(ref entities) = message.entities {
for entity in entities {
if entity.entity_type == MessageEntityType::TextLink {
links.extend(finder.links(entity.url.as_ref().unwrap()));
}
}
}
if let Some(ref entities) = message.caption_entities {
for entity in entities {
if entity.entity_type == MessageEntityType::TextLink {
links.extend(finder.links(entity.url.as_ref().unwrap()));
}
}
}
links
}
/// Check if a link was contained within a linkify Link.
pub fn link_was_seen(links: &[linkify::Link], source: &str) -> bool {
links.iter().any(|link| link.as_str() == source)
}
async fn get_matches(
bot: &Telegram,
fapi: &fautil::FAUtil,
conn: &quaint::pooled::Quaint,
sizes: &[PhotoSize],
) -> failure::Fallible<Option<fautil::File>> {
// Find the highest resolution size of the image and download.
let best_photo = find_best_photo(&sizes).unwrap();
Ok(match_image(&bot, &conn, &fapi, &best_photo)
.await?
.into_iter()
.next())
}
#[cfg(test)]
mod tests {
fn get_finder() -> linkify::LinkFinder {
let mut finder = linkify::LinkFinder::new();
finder.kinds(&[linkify::LinkKind::Url]);
finder
}
#[test]
fn test_find_links() {
let finder = get_finder();
let expected_links = vec![
"https://syfaro.net",
"https://huefox.com",
"https://e621.net",
"https://www.furaffinity.net",
"https://www.weasyl.com",
"https://furrynetwork.com",
];
let message = telegram::Message {
text: Some(
"My message has a link like this: https://syfaro.net and some words after it."
.into(),
),
caption: Some("There can also be links in the caption: https://huefox.com".into()),
reply_markup: Some(telegram::InlineKeyboardMarkup {
inline_keyboard: vec![
vec![telegram::InlineKeyboardButton {
url: Some("https://e621.net".into()),
..Default::default()
}],
vec![telegram::InlineKeyboardButton {
url: Some("https://www.furaffinity.net".into()),
..Default::default()
}],
],
}),
entities: Some(vec![telegram::MessageEntity {
entity_type: telegram::MessageEntityType::TextLink,
offset: 0,
length: 10,
url: Some("https://www.weasyl.com".to_string()),
user: None,
}]),
caption_entities: Some(vec![telegram::MessageEntity {
entity_type: telegram::MessageEntityType::TextLink,
offset: 11,
length: 20,
url: Some("https://furrynetwork.com".to_string()),
user: None,
}]),
..Default::default()
};
let links = super::extract_links(&message, &finder);
assert_eq!(
links.len(),
expected_links.len(),
"found different number of links"
);
for (link, expected) in links.iter().zip(expected_links.iter()) {
assert_eq!(&link.as_str(), expected);
}
}
#[test]
fn test_link_was_seen() {
let finder = get_finder();
let test = "https://www.furaffinity.net/";
let found_links = finder.links(&test);
let mut links = vec![];
links.extend(found_links);
assert!(
super::link_was_seen(&links, "https://www.furaffinity.net/"),
"seen link was not found"
);
assert!(
!super::link_was_seen(&links, "https://e621.net/"),
"unseen link was found"
);
}
}