Automatic sourcing in groups.
This commit is contained in:
parent
7b012d6c42
commit
98dd62521c
|
@ -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
quaint
|
@ -1 +0,0 @@
|
|||
Subproject commit dc628de3342203b71cdedd4ff3c8a86eb7acefa4
|
|
@ -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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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()),
|
||||
];
|
||||
|
|
|
@ -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>()
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue