321 lines
9.3 KiB
Rust
321 lines
9.3 KiB
Rust
use sentry::integrations::failure::capture_fail;
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
use tracing_futures::Instrument;
|
|
|
|
use crate::BoxedSite;
|
|
|
|
type Bundle<'a> = &'a fluent::FluentBundle<fluent::FluentResource>;
|
|
|
|
pub struct SiteCallback<'a> {
|
|
pub site: &'a BoxedSite,
|
|
pub link: &'a str,
|
|
pub duration: i64,
|
|
pub results: Vec<crate::PostInfo>,
|
|
}
|
|
|
|
#[tracing::instrument(skip(user, sites, callback))]
|
|
pub async fn find_images<'a, C>(
|
|
user: &telegram::User,
|
|
links: Vec<&'a str>,
|
|
sites: &mut [BoxedSite],
|
|
callback: &mut C,
|
|
) -> failure::Fallible<Vec<&'a str>>
|
|
where
|
|
C: FnMut(SiteCallback),
|
|
{
|
|
let mut missing = vec![];
|
|
|
|
'link: for link in links {
|
|
for site in sites.iter_mut() {
|
|
let start = Instant::now();
|
|
|
|
if site.url_supported(link).await {
|
|
tracing::debug!("link {} supported by {}", link, site.name());
|
|
|
|
let images = site.get_images(user.id, link).await?;
|
|
|
|
match images {
|
|
Some(results) => {
|
|
tracing::debug!("found images: {:?}", results);
|
|
callback(SiteCallback {
|
|
site: &site,
|
|
link,
|
|
duration: start.elapsed().as_millis() as i64,
|
|
results,
|
|
});
|
|
break 'link;
|
|
}
|
|
_ => {
|
|
missing.push(link);
|
|
continue 'link;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(missing)
|
|
}
|
|
|
|
pub fn find_best_photo(sizes: &[telegram::PhotoSize]) -> Option<&telegram::PhotoSize> {
|
|
sizes.iter().max_by_key(|size| size.height * size.width)
|
|
}
|
|
|
|
pub fn get_message(
|
|
bundle: Bundle,
|
|
name: &str,
|
|
args: Option<fluent::FluentArgs>,
|
|
) -> Result<String, Vec<fluent::FluentError>> {
|
|
let msg = bundle.get_message(name).expect("Message doesn't exist");
|
|
let pattern = msg.value.expect("Message has no value");
|
|
let mut errors = vec![];
|
|
let value = bundle.format_pattern(&pattern, args.as_ref(), &mut errors);
|
|
if errors.is_empty() {
|
|
Ok(value.to_string())
|
|
} else {
|
|
Err(errors)
|
|
}
|
|
}
|
|
|
|
type SentryTags<'a> = Option<Vec<(&'a str, String)>>;
|
|
|
|
pub fn with_user_scope<C, R>(from: Option<&telegram::User>, tags: SentryTags, callback: C) -> R
|
|
where
|
|
C: FnOnce() -> R,
|
|
{
|
|
sentry::with_scope(
|
|
|scope| {
|
|
if let Some(user) = from {
|
|
scope.set_user(Some(sentry::User {
|
|
id: Some(user.id.to_string()),
|
|
username: user.username.clone(),
|
|
..Default::default()
|
|
}));
|
|
};
|
|
|
|
if let Some(tags) = tags {
|
|
for tag in tags {
|
|
scope.set_tag(tag.0, tag.1);
|
|
}
|
|
}
|
|
},
|
|
callback,
|
|
)
|
|
}
|
|
|
|
type AlternateItems<'a> = Vec<(&'a Vec<String>, &'a Vec<fautil::File>)>;
|
|
|
|
pub fn build_alternate_response(bundle: Bundle, mut items: AlternateItems) -> (String, Vec<i64>) {
|
|
let mut used_hashes = vec![];
|
|
|
|
items.sort_by(|a, b| {
|
|
let a_distance: u64 = a.1.iter().map(|item| item.distance.unwrap()).sum();
|
|
let b_distance: u64 = b.1.iter().map(|item| item.distance.unwrap()).sum();
|
|
|
|
a_distance.partial_cmp(&b_distance).unwrap()
|
|
});
|
|
|
|
let mut s = String::new();
|
|
s.push_str(&get_message(&bundle, "alternate-title", None).unwrap());
|
|
s.push_str("\n\n");
|
|
|
|
for item in items {
|
|
let total_dist: u64 = item.1.iter().map(|item| item.distance.unwrap()).sum();
|
|
tracing::trace!("total distance: {}", total_dist);
|
|
if total_dist > (6 * item.1.len() as u64) {
|
|
tracing::trace!("too high, aborting");
|
|
continue;
|
|
}
|
|
let artist_name = item
|
|
.1
|
|
.first()
|
|
.unwrap()
|
|
.artists
|
|
.clone()
|
|
.unwrap_or_else(|| vec!["Unknown".to_string()])
|
|
.join(", ");
|
|
let mut args = fluent::FluentArgs::new();
|
|
args.insert("name", artist_name.into());
|
|
s.push_str(&get_message(&bundle, "alternate-posted-by", Some(args)).unwrap());
|
|
s.push_str("\n");
|
|
let mut subs: Vec<fautil::File> = item.1.to_vec();
|
|
subs.sort_by(|a, b| a.id.partial_cmp(&b.id).unwrap());
|
|
subs.dedup_by(|a, b| a.id == b.id);
|
|
subs.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
|
|
for sub in subs {
|
|
tracing::trace!("looking at {}-{}", sub.site_name(), sub.id);
|
|
let mut args = fluent::FluentArgs::new();
|
|
args.insert("link", sub.url().into());
|
|
args.insert("distance", sub.distance.unwrap().into());
|
|
s.push_str(&get_message(&bundle, "alternate-distance", Some(args)).unwrap());
|
|
s.push_str("\n");
|
|
used_hashes.push(sub.hash.unwrap());
|
|
}
|
|
s.push_str("\n");
|
|
}
|
|
|
|
(s, used_hashes)
|
|
}
|
|
|
|
pub fn parse_known_bots(message: &telegram::Message) -> Option<Vec<String>> {
|
|
let from = if let Some(ref forward_from) = message.forward_from {
|
|
Some(forward_from)
|
|
} else {
|
|
message.from.as_ref()
|
|
};
|
|
|
|
let from = match &from {
|
|
Some(from) => from,
|
|
None => return None,
|
|
};
|
|
|
|
tracing::trace!("evaluating if known bot: {}", from.id);
|
|
|
|
match from.id {
|
|
// FAwatchbot
|
|
190_600_517 => {
|
|
let urls = match message.entities {
|
|
Some(ref entities) => entities.iter().filter_map(|entity| {
|
|
{ entity.url.as_ref().map(|url| url.to_string()) }
|
|
.filter(|url| url.contains("furaffinity.net/view/"))
|
|
}),
|
|
None => return None,
|
|
};
|
|
|
|
Some(urls.collect())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub struct ContinuousAction {
|
|
tx: Option<tokio::sync::oneshot::Sender<bool>>,
|
|
}
|
|
|
|
#[tracing::instrument(skip(bot, user))]
|
|
pub fn continuous_action(
|
|
bot: Arc<telegram::Telegram>,
|
|
max: usize,
|
|
chat_id: telegram::ChatID,
|
|
user: Option<telegram::User>,
|
|
action: telegram::ChatAction,
|
|
) -> ContinuousAction {
|
|
use futures::future::FutureExt;
|
|
use futures_util::stream::StreamExt;
|
|
use std::time::Duration;
|
|
|
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
|
|
tokio::spawn(
|
|
async move {
|
|
let chat_action = telegram::SendChatAction { chat_id, action };
|
|
|
|
let mut count: usize = 0;
|
|
|
|
let timer = Box::pin(
|
|
tokio::time::interval(Duration::from_secs(5))
|
|
.take_while(|_| {
|
|
tracing::trace!(count, "evaluating chat action");
|
|
count += 1;
|
|
futures::future::ready(count < max)
|
|
})
|
|
.for_each(|_| async {
|
|
if let Err(e) = bot.make_request(&chat_action).await {
|
|
tracing::warn!("unable to send chat action: {:?}", e);
|
|
with_user_scope(user.as_ref(), None, || {
|
|
capture_fail(&e);
|
|
});
|
|
}
|
|
}),
|
|
)
|
|
.fuse();
|
|
|
|
let was_ended = if let futures::future::Either::Right(_) =
|
|
futures::future::select(timer, rx).await
|
|
{
|
|
true
|
|
} else {
|
|
false
|
|
};
|
|
|
|
tracing::trace!(count, was_ended, "chat action ended");
|
|
}
|
|
.in_current_span(),
|
|
);
|
|
|
|
ContinuousAction { tx: Some(tx) }
|
|
}
|
|
|
|
impl Drop for ContinuousAction {
|
|
fn drop(&mut self) {
|
|
let tx = std::mem::replace(&mut self.tx, None);
|
|
if let Some(tx) = tx {
|
|
tx.send(true).unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn match_image(
|
|
bot: &telegram::Telegram,
|
|
conn: &quaint::pooled::Quaint,
|
|
fapi: &fautil::FAUtil,
|
|
file: &telegram::PhotoSize,
|
|
) -> failure::Fallible<Vec<fautil::File>> {
|
|
use quaint::prelude::*;
|
|
|
|
let conn = conn.check_out().await?;
|
|
|
|
let result = conn
|
|
.select(
|
|
Select::from_table("file_id_cache")
|
|
.column("hash")
|
|
.so_that("file_id".equals(file.file_unique_id.to_string())),
|
|
)
|
|
.await?;
|
|
|
|
if let Some(row) = result.first() {
|
|
let hash = row["hash"].as_i64().unwrap();
|
|
return lookup_single_hash(&fapi, hash).await;
|
|
}
|
|
|
|
let get_file = telegram::GetFile {
|
|
file_id: file.file_id.clone(),
|
|
};
|
|
|
|
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)?;
|
|
conn.insert(
|
|
Insert::single_into("file_id_cache")
|
|
.value("hash", hash)
|
|
.value("file_id", file.file_unique_id.clone())
|
|
.build(),
|
|
)
|
|
.await?;
|
|
|
|
lookup_single_hash(&fapi, hash).await
|
|
}
|
|
|
|
async fn lookup_single_hash(
|
|
fapi: &fautil::FAUtil,
|
|
hash: i64,
|
|
) -> failure::Fallible<Vec<fautil::File>> {
|
|
let mut matches = fapi.lookup_hashes(vec![hash]).await?;
|
|
|
|
for mut m in &mut matches {
|
|
m.distance =
|
|
hamming::distance_fast(&m.hash.unwrap().to_be_bytes(), &hash.to_be_bytes()).ok();
|
|
}
|
|
|
|
matches.sort_by(|a, b| {
|
|
a.distance
|
|
.unwrap()
|
|
.partial_cmp(&b.distance.unwrap())
|
|
.unwrap()
|
|
});
|
|
|
|
Ok(matches)
|
|
}
|