Automatic sourcing in groups.

This commit is contained in:
Syfaro 2020-03-09 03:33:02 -05:00
parent 7b012d6c42
commit 98dd62521c
11 changed files with 339 additions and 5 deletions

View File

@ -11,6 +11,8 @@ welcome =
Add me to your group for features like /mirror (where I mirror all the links in a message, including messages you reply to) or /source (where I attempt to find the source of an image you're replying to).
If I'm given edit permissions in your channel, I'll automatically edit posts to include a source link.
Contact my creator { -creatorName } if you have any issues or feature suggestions.
welcome-group =
@ -21,6 +23,8 @@ welcome-group =
I've also got a few commands to help in groups:
· /mirror - I'll look at all the links in your message or the message you're replying to and mirror them
· /source - I'll attempt to find if the photo you're replying to has been posted on FurAffinity
You can also enable automatically finding sources for images posted in here with the /groupsource command. However, I must be an administrator in the group for this to work and it can only be enabled by an administrator.
welcome-try-me = Try Me!
@ -61,6 +65,16 @@ alternate-posted-by = Posted by { $name }
alternate-distance = · { $link } (distance of { $distance })
alternate-multiple-photo = I can only find alternates for a single photo, sorry.
# Automatic group sourcing
automatic-single = It looks like this image may have come from here: { $link }
automatic-multiple = I found a few places this image may have come from:
automatic-multiple-result = · { $link } (distance of { $distance })
automatic-enable-not-admin = Sorry, you must be a group admin to enable this.
automatic-enable-bot-not-admin = Sorry, you must give me admin permissions due to a Telegram limitation.
automatic-enable-success = Automatic group sourcing is now enabled!
automatic-disable = This feature is now turned off.
automatic-enable-not-group = This feature is only supported in groups.
# Error Messages
error-generic = Oh no, something went wrong! Please send a message to my creator, { -creatorName }, saying what happened.
error-uuid = Oh no, something went wrong! Please reply to this message saying what happened. You may also send a message to my creator, { -creatorName }, with this ID if you continue having issues: { $uuid }

1
quaint

@ -1 +0,0 @@
Subproject commit dc628de3342203b71cdedd4ff3c8a86eb7acefa4

View File

@ -93,7 +93,10 @@ impl super::Handler for ChannelPhotoHandler {
/// Extract all possible links from a Message. It looks at the text,
/// caption, and all buttons within an inline keyboard.
fn extract_links<'m>(message: &'m Message, finder: &linkify::LinkFinder) -> Vec<linkify::Link<'m>> {
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.
@ -117,11 +120,20 @@ fn extract_links<'m>(message: &'m Message, finder: &linkify::LinkFinder) -> Vec<
}
}
// 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::URL {
links.extend(finder.links(entity.url.as_ref().unwrap()));
}
}
}
links
}
/// Check if a link was contained within a linkify Link.
fn link_was_seen(links: &[linkify::Link], source: &str) -> bool {
pub fn link_was_seen(links: &[linkify::Link], source: &str) -> bool {
links.iter().any(|link| link.as_str() == source)
}
@ -157,6 +169,7 @@ mod tests {
"https://huefox.com",
"https://e621.net",
"https://www.furaffinity.net",
"https://www.weasyl.com",
];
let message = telegram::Message {
@ -177,6 +190,13 @@ mod tests {
}],
],
}),
entities: Some(vec![telegram::MessageEntity {
entity_type: telegram::MessageEntityType::URL,
offset: 0,
length: 10,
url: Some("https://www.weasyl.com".to_string()),
user: None,
}]),
..Default::default()
};

View File

@ -52,6 +52,7 @@ impl super::Handler for CommandHandler {
"/source" => self.handle_source(&handler, message).await,
"/alts" => self.handle_alts(&handler, message).await,
"/error" => Err(failure::format_err!("a test error message")),
"/groupsource" => self.enable_group_source(&handler, message).await,
_ => {
tracing::info!("unknown command: {}", command.name);
Ok(())
@ -573,4 +574,92 @@ impl CommandHandler {
.map(|_msg| ())
.map_err(Into::into)
}
async fn enable_group_source(
&self,
handler: &crate::MessageHandler,
message: &Message,
) -> failure::Fallible<()> {
use super::group_source::{ENABLE_KEY, ENABLE_VALUE};
use quaint::prelude::*;
if !message.chat.chat_type.is_group() {
handler
.send_generic_reply(&message, "automatic-enable-not-group")
.await?;
return Ok(());
}
let user = message.from.as_ref().unwrap();
let get_chat_member = GetChatMember {
chat_id: message.chat_id(),
user_id: user.id,
};
let chat_member = handler.bot.make_request(&get_chat_member).await?;
if !chat_member.status.is_admin() {
handler
.send_generic_reply(&message, "automatic-enable-not-admin")
.await?;
return Ok(());
}
let get_chat_member = GetChatMember {
user_id: handler.bot_user.id,
..get_chat_member
};
let bot_member = handler.bot.make_request(&get_chat_member).await?;
if !bot_member.status.is_admin() {
handler
.send_generic_reply(&message, "automatic-enable-bot-not-admin")
.await?;
return Ok(());
}
let conn = handler.conn.check_out().await?;
let results = conn
.select(
Select::from_table("group_config").so_that(
"chat_id"
.equals(message.chat.id)
.and("name".equals(ENABLE_KEY)),
),
)
.await?;
if !results.is_empty() {
conn.delete(
Delete::from_table("group_config").so_that(
"chat_id"
.equals(message.chat.id)
.and("name".equals(ENABLE_KEY)),
),
)
.await?;
handler
.send_generic_reply(&message, "automatic-disable")
.await?;
return Ok(());
}
conn.insert(
Insert::single_into("group_config")
.value("chat_id", message.chat.id)
.value("name", ENABLE_KEY)
.value("value", ENABLE_VALUE)
.build(),
)
.await?;
handler
.send_generic_reply(&message, "automatic-enable-success")
.await?;
Ok(())
}
}

View File

@ -0,0 +1,115 @@
use super::Status::*;
use crate::needs_field;
use crate::utils::{find_best_photo, get_message, match_image};
use async_trait::async_trait;
use telegram::*;
pub static ENABLE_KEY: &str = "group_add";
pub static ENABLE_VALUE: &str = "yes";
pub struct GroupSourceHandler;
#[async_trait]
impl super::Handler for GroupSourceHandler {
fn name(&self) -> &'static str {
"group"
}
async fn handle(
&self,
handler: &crate::MessageHandler,
update: &Update,
_command: Option<&Command>,
) -> Result<super::Status, failure::Error> {
use quaint::prelude::*;
let message = needs_field!(update, message);
let photo_sizes = needs_field!(message, photo);
let conn = handler.conn.check_out().await?;
let results = conn
.select(
Select::from_table("group_config").so_that(
"chat_id"
.equals(message.chat.id)
.and("name".equals(ENABLE_KEY)),
),
)
.await?;
let row = match results.first() {
Some(first) => first,
_ => return Ok(Ignored),
};
if row["value"].as_str().unwrap() != ENABLE_VALUE {
return Ok(Ignored);
}
let best_photo = find_best_photo(&photo_sizes).unwrap();
let matches = match_image(&handler.bot, &handler.conn, &handler.fapi, &best_photo).await?;
let wanted_matches = matches
.iter()
.filter(|m| m.distance.unwrap() <= 3)
.collect::<Vec<_>>();
if wanted_matches.is_empty() {
return Ok(Completed);
}
let links = super::channel_photo::extract_links(&message, &handler.finder);
if wanted_matches
.iter()
.any(|m| super::channel_photo::link_was_seen(&links, &m.url()))
{
return Ok(Completed);
}
let lang = message
.from
.as_ref()
.and_then(|from| from.language_code.as_deref());
let text = handler
.get_fluent_bundle(lang, |bundle| {
if wanted_matches.len() == 1 {
let mut args = fluent::FluentArgs::new();
args.insert("link", wanted_matches.first().unwrap().url().into());
get_message(bundle, "automatic-single", Some(args)).unwrap()
} else {
let mut buf = String::new();
buf.push_str(&get_message(bundle, "automatic-multiple", None).unwrap());
buf.push('\n');
for result in wanted_matches {
let mut args = fluent::FluentArgs::new();
args.insert("link", result.url().into());
args.insert("distance", result.distance.unwrap().into());
buf.push_str(
&get_message(bundle, "automatic-multiple-result", Some(args)).unwrap(),
);
buf.push('\n');
}
buf
}
})
.await;
let message = SendMessage {
chat_id: message.chat_id(),
reply_to_message_id: Some(message.message_id),
disable_web_page_preview: Some(true),
text,
..Default::default()
};
handler.bot.make_request(&message).await?;
Ok(Completed)
}
}

View File

@ -5,6 +5,7 @@ mod chosen_inline_handler;
mod commands;
mod error_reply;
mod group_add;
mod group_source;
mod inline_handler;
mod photo;
mod text;
@ -14,6 +15,7 @@ pub use chosen_inline_handler::ChosenInlineHandler;
pub use commands::CommandHandler;
pub use error_reply::ErrorReplyHandler;
pub use group_add::GroupAddHandler;
pub use group_source::GroupSourceHandler;
pub use inline_handler::InlineHandler;
pub use photo::PhotoHandler;
pub use text::TextHandler;

View File

@ -237,6 +237,7 @@ async fn main() {
Box::new(handlers::GroupAddHandler),
Box::new(handlers::PhotoHandler),
Box::new(handlers::CommandHandler),
Box::new(handlers::GroupSourceHandler),
Box::new(handlers::TextHandler),
Box::new(handlers::ErrorReplyHandler::new()),
];

View File

@ -0,0 +1,26 @@
use barrel::{types, Migration, backend::Sqlite};
pub fn migration() -> String {
let mut m = Migration::new();
m.create_table("group_config", |t| {
t.add_column("id", types::primary());
t.add_column("chat_id", types::integer().nullable(false));
t.add_column("name", types::varchar(255).nullable(false));
t.add_column("value", types::varchar(255).nullable(false));
t.add_index("group_config_lookup", types::index(vec!["chat_id", "name"]).unique(true).nullable(false));
});
m.create_table("user_group_config", |t| {
t.add_column("id", types::primary());
t.add_column("chat_id", types::integer().nullable(false));
t.add_column("user_id", types::integer().nullable(false));
t.add_column("name", types::varchar(255).nullable(false));
t.add_column("value", types::varchar(255).nullable(false));
t.add_index("user_group_config_lookup", types::index(vec!["chat_id", "user_id", "name"]).unique(true).nullable(false));
});
m.make::<Sqlite>()
}

View File

@ -256,6 +256,7 @@ impl Drop for ContinuousAction {
}
}
#[tracing::instrument(skip(bot, conn, fapi))]
pub async fn match_image(
bot: &telegram::Telegram,
conn: &quaint::pooled::Quaint,
@ -286,7 +287,8 @@ pub async fn match_image(
let file_info = bot.make_request(&get_file).await?;
let data = bot.download_file(file_info.file_path.unwrap()).await?;
let hash = fautil::hash_bytes(&data)?;
let hash = tokio::task::spawn_blocking(move || fautil::hash_bytes(&data)).await??;
conn.insert(
Insert::single_into("file_id_cache")
.value("hash", hash)

View File

@ -5,7 +5,7 @@ use crate::types::*;
use crate::TelegramRequest;
/// ChatID represents a possible type of value for requests.
#[derive(Serialize, Debug)]
#[derive(Serialize, Debug, Clone)]
#[serde(untagged)]
pub enum ChatID {
/// A chat's numeric ID.
@ -683,3 +683,17 @@ impl TelegramRequest for EditMessageReplyMarkup {
"editMessageReplyMarkup"
}
}
#[derive(Default, Debug, Serialize, Clone)]
pub struct GetChatMember {
pub chat_id: ChatID,
pub user_id: i32,
}
impl TelegramRequest for GetChatMember {
type Response = ChatMember;
fn endpoint(&self) -> &str {
"getChatMember"
}
}

View File

@ -70,6 +70,12 @@ impl Default for ChatType {
}
}
impl ChatType {
pub fn is_group(&self) -> bool {
*self == Self::Group || *self == Self::Supergroup
}
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct Chat {
pub id: i64,
@ -233,3 +239,49 @@ pub struct InlineKeyboardButton {
pub struct InlineKeyboardMarkup {
pub inline_keyboard: Vec<Vec<InlineKeyboardButton>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ChatMemberStatus {
Creator,
Administrator,
Member,
Restricted,
Left,
Kicked
}
impl Default for ChatMemberStatus {
fn default() -> Self {
Self::Member
}
}
impl ChatMemberStatus {
pub fn is_admin(&self) -> bool {
*self == Self::Creator || *self == Self::Administrator
}
}
#[derive(Deserialize, Debug, Clone, Default)]
pub struct ChatMember {
pub user: User,
pub status: ChatMemberStatus,
pub custom_title: Option<String>,
pub until_date: Option<i32>,
pub can_be_edited: Option<bool>,
pub can_post_messages: Option<bool>,
pub can_edit_messages: Option<bool>,
pub can_delete_messages: Option<bool>,
pub can_restrict_members: Option<bool>,
pub can_promote_members: Option<bool>,
pub can_change_info: Option<bool>,
pub can_invite_users: Option<bool>,
pub can_pin_messages: Option<bool>,
pub is_member: Option<bool>,
pub can_send_messages: Option<bool>,
pub can_send_media_messages: Option<bool>,
pub can_send_polls: Option<bool>,
pub can_send_other_messages: Option<bool>,
pub can_add_web_page_previews: Option<bool>,
}