Merge branch 'master' into sqlite

This commit is contained in:
Syfaro 2020-03-08 20:18:28 -05:00
commit 8e29ac7764
19 changed files with 1248 additions and 712 deletions

View File

@ -8,19 +8,60 @@ platform:
arch: amd64
steps:
- name: test
image: rustlang/rust:nightly-slim
commands:
- apt-get update -y
- apt-get install pkg-config libssl-dev ca-certificates python3 python3-pip nodejs -y
- pip3 install cfscrape
# - rustup component add clippy
- cargo test
# - cargo clippy
- name: sentry-release
image: getsentry/sentry-cli
commands:
- sentry-cli releases new ${DRONE_COMMIT_SHA}
environment:
SENTRY_AUTH_TOKEN:
from_secret: sentry_auth_token
SENTRY_ORG:
from_secret: sentry_org
SENTRY_PROJECT:
from_secret: sentry_project
when:
branch:
- master
- name: docker
image: plugins/docker
settings:
auto_tag: true
build_args_from_env:
- DRONE_COMMIT_SHA
password:
from_secret: docker_password
registry: registry.huefox.com
repo: registry.huefox.com/foxbot
username:
from_secret: docker_username
when:
branch:
- master
trigger:
branch:
- master
- name: sentry-finalize
image: getsentry/sentry-cli
commands:
- sentry-cli releases finalize ${DRONE_COMMIT_SHA}
environment:
SENTRY_AUTH_TOKEN:
from_secret: sentry_auth_token
SENTRY_ORG:
from_secret: sentry_org
SENTRY_PROJECT:
from_secret: sentry_project
when:
branch:
- master
...

650
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ hamming = "0.1"
log = "0.4"
pretty_env_logger = "0.4"
sentry = "0.17"
sentry = { version = "0.17", features = ["with_debug_meta"] }
uuid = "0.7"
failure = "0.1"
influxdb = "0.0.6"
@ -47,4 +47,4 @@ tracing-opentelemetry = "0.1"
rusqlite = { version = "0.21", features = ["bundled"] }
refinery = { version = "0.2", features = ["rusqlite"] }
barrel = { version = "0.6", features = ["sqlite3"] }
quaint = { path = "./quaint", features = ["full-sqlite"] }
quaint = { version = "0.2.0-alpha.9", features = ["full"] }

View File

@ -1,6 +1,8 @@
FROM rustlang/rust:nightly-slim AS builder
WORKDIR /src
RUN apt-get update -y && apt-get install pkg-config libssl-dev python3 python3-dev -y
ARG DRONE_COMMIT_SHA
ENV RELEASE $DRONE_COMMIT_SHA
COPY . .
RUN cargo install --root / --path .

View File

@ -62,5 +62,6 @@ alternate-distance = · { $link } (distance of { $distance })
alternate-multiple-photo = I can only find alternates for a single photo, sorry.
# Error Messages
error-generic = Oh no, something went wrong! Please send a message to my creator { -creatorName } if you continue having issues.
error-uuid = Oh no, something went wrong! Please send a message to my creator { -creatorName } with this ID if you continue having issues: { $uuid }
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 }
error-feedback = Thank you for the feedback, hopefully we can get this issue resolved soon.

View File

@ -1,13 +1,20 @@
use async_trait::async_trait;
use sentry::integrations::failure::capture_fail;
use telegram::*;
use crate::utils::{download_by_id, find_best_photo, get_message, with_user_scope};
use super::Status::*;
use crate::needs_field;
use crate::utils::{download_by_id, find_best_photo, get_message};
// 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 crate::Handler for ChannelPhotoHandler {
impl super::Handler for ChannelPhotoHandler {
fn name(&self) -> &'static str {
"channel"
}
@ -15,74 +22,32 @@ impl crate::Handler for ChannelPhotoHandler {
async fn handle(
&self,
handler: &crate::MessageHandler,
update: Update,
_command: Option<Command>,
) -> Result<bool, Box<dyn std::error::Error>> {
let message = match update.message {
Some(message) => message,
_ => return Ok(false),
};
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.
// 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(false);
return Ok(Ignored);
}
let sizes = match &message.photo {
Some(sizes) => sizes,
_ => return Ok(false),
};
// We can't edit forwarded messages, so we have to ignore.
if message.forward_date.is_some() {
return Ok(true);
return Ok(Completed);
}
// Collect all the links in the message to see if there was a source.
let mut links = vec![];
// Unlikely to be text posts here, but we'll consider anyway.
if let Some(ref text) = message.text {
links.extend(handler.finder.links(&text));
}
// Links could be in an image caption.
if let Some(ref caption) = message.caption {
links.extend(handler.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(handler.finder.links(&url));
}
}
}
}
// Find the highest resolution size of the image and download.
let best_photo = find_best_photo(&sizes).unwrap();
let bytes = download_by_id(&handler.bot, &best_photo.file_id)
.await
.unwrap();
// Check if we had any matches.
let matches = match handler
.fapi
.image_search(&bytes, fautil::MatchType::Close)
.await
{
Ok(matches) if !matches.matches.is_empty() => matches.matches,
_ => return Ok(true),
let first = match get_matches(&handler.bot, &handler.fapi, &sizes).await? {
Some(first) => first,
_ => return Ok(Completed),
};
// We know it's not empty, so get the first item and assert it's a
// comfortable distance to be identical.
let first = matches.first().unwrap();
if first.distance.unwrap() > 2 {
return Ok(true);
// 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
@ -91,16 +56,11 @@ impl crate::Handler for ChannelPhotoHandler {
let edit_caption_markup = EditMessageCaption {
chat_id: message.chat_id(),
message_id: Some(message.message_id),
caption: Some(format!("https://www.furaffinity.net/view/{}/", first.id)),
caption: Some(first.url()),
..Default::default()
};
if let Err(e) = handler.bot.make_request(&edit_caption_markup).await {
tracing::error!("unable to edit channel caption: {:?}", e);
with_user_scope(None, None, || {
capture_fail(&e);
});
}
handler.bot.make_request(&edit_caption_markup).await?;
// Not a media group, we should create an inline keyboard.
} else {
let text = handler
@ -124,14 +84,134 @@ impl crate::Handler for ChannelPhotoHandler {
..Default::default()
};
if let Err(e) = handler.bot.make_request(&edit_reply_markup).await {
tracing::error!("unable to edit channel reply markup: {:?}", e);
with_user_scope(None, None, || {
capture_fail(&e);
});
}
handler.bot.make_request(&edit_reply_markup).await?;
}
Ok(true)
Ok(Completed)
}
}
/// 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>> {
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));
}
}
}
}
links
}
/// Check if a link was contained within a linkify Link.
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,
sizes: &[PhotoSize],
) -> reqwest::Result<Option<fautil::File>> {
// Find the highest resolution size of the image and download.
let best_photo = find_best_photo(&sizes).unwrap();
let bytes = download_by_id(&bot, &best_photo.file_id).await.unwrap();
// Run an image search for these bytes, get the first result.
fapi.image_search(&bytes, fautil::MatchType::Close)
.await
.map(|matches| matches.matches.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",
];
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()
}],
],
}),
..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"
);
}
}

View File

@ -1,11 +1,12 @@
use super::Status::*;
use crate::needs_field;
use async_trait::async_trait;
use sentry::integrations::failure::capture_fail;
use telegram::*;
pub struct ChosenInlineHandler;
#[async_trait]
impl crate::Handler for ChosenInlineHandler {
impl super::Handler for ChosenInlineHandler {
fn name(&self) -> &'static str {
"chosen"
}
@ -13,23 +14,16 @@ impl crate::Handler for ChosenInlineHandler {
async fn handle(
&self,
handler: &crate::MessageHandler,
update: Update,
_command: Option<Command>,
) -> Result<bool, Box<dyn std::error::Error>> {
let handled = if let Some(chosen_result) = update.chosen_inline_result {
let point = influxdb::Query::write_query(influxdb::Timestamp::Now, "chosen")
.add_field("user_id", chosen_result.from.id);
update: &Update,
_command: Option<&Command>,
) -> Result<super::Status, failure::Error> {
let chosen_result = needs_field!(update, chosen_inline_result);
if let Err(e) = handler.influx.query(&point).await {
log::error!("Unable to send chosen inline result to InfluxDB: {:?}", e);
capture_fail(&e);
}
let point = influxdb::Query::write_query(influxdb::Timestamp::Now, "chosen")
.add_field("user_id", chosen_result.from.id);
true
} else {
false
};
let _ = handler.influx.query(&point).await;
Ok(handled)
Ok(Completed)
}
}

View File

@ -1,12 +1,13 @@
use async_trait::async_trait;
use sentry::integrations::failure::capture_fail;
use std::collections::HashMap;
use telegram::*;
use tokio01::runtime::current_thread::block_on_all;
use super::Status::*;
use crate::needs_field;
use crate::utils::{
build_alternate_response, continuous_action, download_by_id, find_best_photo, find_images,
get_message, parse_known_bots, with_user_scope,
get_message, parse_known_bots,
};
// TODO: there's a lot of shared code between these commands.
@ -14,7 +15,7 @@ use crate::utils::{
pub struct CommandHandler;
#[async_trait]
impl crate::Handler for CommandHandler {
impl super::Handler for CommandHandler {
fn name(&self) -> &'static str {
"command"
}
@ -22,17 +23,14 @@ impl crate::Handler for CommandHandler {
async fn handle(
&self,
handler: &crate::MessageHandler,
update: Update,
_command: Option<Command>,
) -> Result<bool, Box<dyn std::error::Error>> {
let message = match update.message {
Some(message) => message,
_ => return Ok(false),
};
update: &Update,
_command: Option<&Command>,
) -> Result<super::Status, failure::Error> {
let message = needs_field!(update, message);
let command = match message.get_command() {
Some(command) => command,
None => return Ok(false),
None => return Ok(Ignored),
};
let now = std::time::Instant::now();
@ -41,40 +39,41 @@ impl crate::Handler for CommandHandler {
let bot_username = handler.bot_user.username.as_ref().unwrap();
if &username != bot_username {
tracing::debug!("got command for other bot: {}", username);
return Ok(false);
return Ok(Ignored);
}
}
tracing::debug!("got command {}", command.name);
let from = message.from.clone();
match command.name.as_ref() {
"/help" | "/start" => handler.handle_welcome(message, &command.name).await,
"/twitter" => self.authenticate_twitter(&handler, message).await,
"/mirror" => self.handle_mirror(&handler, message).await,
"/source" => self.handle_source(&handler, message).await,
"/alts" => self.handle_alts(&handler, message).await,
_ => tracing::info!("unknown command: {}", command.name),
};
"/error" => Err(failure::format_err!("a test error message")),
_ => {
tracing::info!("unknown command: {}", command.name);
Ok(())
}
}?;
let point = influxdb::Query::write_query(influxdb::Timestamp::Now, "command")
.add_tag("command", command.name)
.add_field("duration", now.elapsed().as_millis() as i64);
if let Err(e) = handler.influx.query(&point).await {
tracing::error!("unable to send command to InfluxDB: {:?}", e);
with_user_scope(from.as_ref(), None, || {
capture_fail(&e);
});
}
let _ = handler.influx.query(&point).await;
Ok(true)
Ok(Completed)
}
}
impl CommandHandler {
async fn authenticate_twitter(&self, handler: &crate::MessageHandler, message: Message) {
async fn authenticate_twitter(
&self,
handler: &crate::MessageHandler,
message: &Message,
) -> failure::Fallible<()> {
use quaint::prelude::*;
let now = std::time::Instant::now();
@ -82,43 +81,23 @@ impl CommandHandler {
if message.chat.chat_type != ChatType::Private {
handler
.send_generic_reply(&message, "twitter-private")
.await;
return;
.await?;
return Ok(());
}
let user = message.from.clone().unwrap();
let user = message.from.as_ref().unwrap();
let con_token = egg_mode::KeyPair::new(
handler.config.twitter_consumer_key.clone(),
handler.config.twitter_consumer_secret.clone(),
);
let request_token = match block_on_all(egg_mode::request_token(&con_token, "oob")) {
Ok(req) => req,
Err(e) => {
tracing::warn!("unable to get request token: {:?}", e);
let request_token = block_on_all(egg_mode::request_token(&con_token, "oob"))?;
handler
.report_error(
&message,
Some(vec![("command", "twitter".to_string())]),
|| {
sentry::integrations::failure::capture_error(&format_err!(
"Unable to get request token: {}",
e
))
},
)
.await;
return;
}
};
let conn = handler.conn.check_out().await.expect("Unable to get conn");
let conn = handler.conn.check_out().await?;
conn.delete(Delete::from_table("twitter_auth").so_that("user_id".equals(user.id)))
.await
.expect("Unable to remove auth keys");
.await?;
conn.insert(
Insert::single_into("twitter_auth")
@ -127,8 +106,7 @@ impl CommandHandler {
.value("request_secret", request_token.secret.to_string())
.build(),
)
.await
.expect("Unable to insert request info");
.await?;
drop(conn);
@ -151,27 +129,23 @@ impl CommandHandler {
..Default::default()
};
if let Err(e) = handler.bot.make_request(&send_message).await {
tracing::warn!("unable to send message: {:?}", e);
with_user_scope(Some(&user), None, || {
capture_fail(&e);
});
}
handler.bot.make_request(&send_message).await?;
let point = influxdb::Query::write_query(influxdb::Timestamp::Now, "twitter")
.add_tag("type", "new")
.add_field("duration", now.elapsed().as_millis() as i64);
if let Err(e) = handler.influx.query(&point).await {
tracing::error!("unable to send command to InfluxDB: {:?}", e);
with_user_scope(Some(&user), None, || {
capture_fail(&e);
});
}
let _ = handler.influx.query(&point).await;
Ok(())
}
async fn handle_mirror(&self, handler: &crate::MessageHandler, message: Message) {
let from = message.from.clone().unwrap();
async fn handle_mirror(
&self,
handler: &crate::MessageHandler,
message: &Message,
) -> failure::Fallible<()> {
let from = message.from.as_ref().unwrap();
let _action = continuous_action(
handler.bot.clone(),
@ -181,8 +155,8 @@ impl CommandHandler {
ChatAction::Typing,
);
let (reply_to_id, message) = if let Some(reply_to_message) = message.reply_to_message {
(message.message_id, *reply_to_message)
let (reply_to_id, message) = if let Some(reply_to_message) = &message.reply_to_message {
(message.message_id, &**reply_to_message)
} else {
(message.message_id, message)
};
@ -196,8 +170,8 @@ impl CommandHandler {
if links.is_empty() {
handler
.send_generic_reply(&message, "mirror-no-links")
.await;
return;
.await?;
return Ok(());
}
let mut results: Vec<crate::PostInfo> = Vec::with_capacity(links.len());
@ -208,14 +182,14 @@ impl CommandHandler {
find_images(&from, links, &mut sites, &mut |info| {
results.extend(info.results);
})
.await
.await?
};
if results.is_empty() {
handler
.send_generic_reply(&message, "mirror-no-results")
.await;
return;
.await?;
return Ok(());
}
if results.len() == 1 {
@ -233,17 +207,7 @@ impl CommandHandler {
reply_to_message_id: Some(message.message_id),
};
if let Err(e) = handler.bot.make_request(&video).await {
tracing::error!("unable to make request: {:?}", e);
handler
.report_error(
&message,
Some(vec![("command", "mirror".to_string())]),
|| capture_fail(&e),
)
.await;
return;
}
handler.bot.make_request(&video).await?;
} else {
let photo = SendPhoto {
chat_id: message.chat_id(),
@ -257,17 +221,7 @@ impl CommandHandler {
..Default::default()
};
if let Err(e) = handler.bot.make_request(&photo).await {
tracing::error!("unable to make request: {:?}", e);
handler
.report_error(
&message,
Some(vec![("command", "mirror".to_string())]),
|| capture_fail(&e),
)
.await;
return;
}
handler.bot.make_request(&photo).await?;
}
} else {
for chunk in results.chunks(10) {
@ -302,17 +256,7 @@ impl CommandHandler {
..Default::default()
};
if let Err(e) = handler.bot.make_request(&media_group).await {
tracing::error!("unable to make request: {:?}", e);
handler
.report_error(
&message,
Some(vec![("command", "mirror".to_string())]),
|| capture_fail(&e),
)
.await;
return;
}
handler.bot.make_request(&media_group).await?;
}
}
@ -335,16 +279,17 @@ impl CommandHandler {
..Default::default()
};
if let Err(e) = handler.bot.make_request(&send_message).await {
tracing::error!("unable to make request: {:?}", e);
with_user_scope(message.from.as_ref(), None, || {
capture_fail(&e);
});
}
handler.bot.make_request(&send_message).await?;
}
Ok(())
}
async fn handle_source(&self, handler: &crate::MessageHandler, message: Message) {
async fn handle_source(
&self,
handler: &crate::MessageHandler,
message: &Message,
) -> failure::Fallible<()> {
let _action = continuous_action(
handler.bot.clone(),
12,
@ -353,58 +298,37 @@ impl CommandHandler {
ChatAction::Typing,
);
let (reply_to_id, message) = if let Some(reply_to_message) = message.reply_to_message {
(message.message_id, *reply_to_message)
let (reply_to_id, message) = if let Some(reply_to_message) = &message.reply_to_message {
(message.message_id, &**reply_to_message)
} else {
(message.message_id, message)
};
let photo = match message.photo.clone() {
let photo = match &message.photo {
Some(photo) if !photo.is_empty() => photo,
_ => {
handler
.send_generic_reply(&message, "source-no-photo")
.await;
return;
.await?;
return Ok(());
}
};
let best_photo = find_best_photo(&photo).unwrap();
let bytes = match download_by_id(&handler.bot, &best_photo.file_id).await {
Ok(bytes) => bytes,
Err(e) => {
tracing::error!("unable to download file: {:?}", e);
let tags = Some(vec![("command", "source".to_string())]);
handler
.report_error(&message, tags, || capture_fail(&e))
.await;
return;
}
};
let bytes = download_by_id(&handler.bot, &best_photo.file_id).await?;
let matches = match handler
let matches = handler
.fapi
.image_search(&bytes, fautil::MatchType::Close)
.await
{
Ok(matches) => matches.matches,
Err(e) => {
tracing::error!("unable to find matches: {:?}", e);
let tags = Some(vec![("command", "source".to_string())]);
handler
.report_error(&message, tags, || capture_fail(&e))
.await;
return;
}
};
.await?;
let result = match matches.first() {
let result = match matches.matches.first() {
Some(result) => result,
None => {
handler
.send_generic_reply(&message, "reverse-no-results")
.await;
return;
.await?;
return Ok(());
}
};
@ -423,7 +347,7 @@ impl CommandHandler {
let text = handler
.get_fluent_bundle(
message.from.clone().unwrap().language_code.as_deref(),
message.from.as_ref().unwrap().language_code.as_deref(),
|bundle| get_message(&bundle, name, Some(args)).unwrap(),
)
.await;
@ -436,15 +360,19 @@ impl CommandHandler {
..Default::default()
};
if let Err(e) = handler.bot.make_request(&send_message).await {
tracing::error!("Unable to make request: {:?}", e);
with_user_scope(message.from.as_ref(), None, || {
capture_fail(&e);
});
}
handler
.bot
.make_request(&send_message)
.await
.map(|_msg| ())
.map_err(Into::into)
}
async fn handle_alts(&self, handler: &crate::MessageHandler, message: Message) {
async fn handle_alts(
&self,
handler: &crate::MessageHandler,
message: &Message,
) -> failure::Fallible<()> {
let _action = continuous_action(
handler.bot.clone(),
12,
@ -453,26 +381,17 @@ impl CommandHandler {
ChatAction::Typing,
);
let (reply_to_id, message) = if let Some(reply_to_message) = message.reply_to_message {
(message.message_id, *reply_to_message)
} else {
(message.message_id, message)
};
let (reply_to_id, message): (i32, &Message) =
if let Some(reply_to_message) = &message.reply_to_message {
(message.message_id, &**reply_to_message)
} else {
(message.message_id, message)
};
let bytes = match message.photo.clone() {
let bytes = match &message.photo {
Some(photo) => {
let best_photo = find_best_photo(&photo).unwrap();
match download_by_id(&handler.bot, &best_photo.file_id).await {
Ok(bytes) => bytes,
Err(e) => {
tracing::error!("unable to download file: {:?}", e);
let tags = Some(vec![("command", "source".to_string())]);
handler
.report_error(&message, tags, || capture_fail(&e))
.await;
return;
}
}
download_by_id(&handler.bot, &best_photo.file_id).await?
}
None => {
let mut links = vec![];
@ -493,27 +412,27 @@ impl CommandHandler {
if links.is_empty() {
handler
.send_generic_reply(&message, "source-no-photo")
.await;
return;
.await?;
return Ok(());
} else if links.len() > 1 {
handler
.send_generic_reply(&message, "alternate-multiple-photo")
.await;
return;
.await?;
return Ok(());
}
let mut sites = handler.sites.lock().await;
let links = links.iter().map(|link| link.as_str()).collect();
let mut link = None;
find_images(
&message.from.clone().unwrap(),
&message.from.as_ref().unwrap(),
links,
&mut sites,
&mut |info| {
link = info.results.into_iter().next();
},
)
.await;
.await?;
match link {
Some(link) => reqwest::get(&link.url)
@ -526,34 +445,23 @@ impl CommandHandler {
None => {
handler
.send_generic_reply(&message, "source-no-photo")
.await;
return;
.await?;
return Ok(());
}
}
}
};
let matches = match handler
let matches = handler
.fapi
.image_search(&bytes, fautil::MatchType::Force)
.await
{
Ok(matches) => matches,
Err(e) => {
tracing::error!("unable to find matches: {:?}", e);
let tags = Some(vec![("command", "source".to_string())]);
handler
.report_error(&message, tags, || capture_fail(&e))
.await;
return;
}
};
.await?;
if matches.matches.is_empty() {
handler
.send_generic_reply(&message, "reverse-no-results")
.await;
return;
.await?;
return Ok(());
}
let hash = matches.hash.to_be_bytes();
@ -590,7 +498,7 @@ impl CommandHandler {
let (text, used_hashes) = handler
.get_fluent_bundle(
message.from.clone().unwrap().language_code.as_deref(),
message.from.as_ref().unwrap().language_code.as_deref(),
|bundle| build_alternate_response(&bundle, items),
)
.await;
@ -605,38 +513,19 @@ impl CommandHandler {
..Default::default()
};
let sent = match handler.bot.make_request(&send_message).await {
Ok(message) => message.message_id,
Err(e) => {
tracing::error!("unable to make request: {:?}", e);
with_user_scope(message.from.as_ref(), None, || {
capture_fail(&e);
});
return;
}
};
let sent = handler.bot.make_request(&send_message).await?;
if !has_multiple_matches {
return;
return Ok(());
}
let matches = match handler.fapi.lookup_hashes(used_hashes).await {
Ok(matches) => matches,
Err(e) => {
tracing::error!("unable to find matches: {:?}", e);
let tags = Some(vec![("command", "source".to_string())]);
handler
.report_error(&message, tags, || capture_fail(&e))
.await;
return;
}
};
let matches = handler.fapi.lookup_hashes(used_hashes).await?;
if matches.is_empty() {
handler
.send_generic_reply(&message, "reverse-no-results")
.await;
return;
.await?;
return Ok(());
}
for m in matches {
@ -663,28 +552,28 @@ impl CommandHandler {
let (updated_text, _used_hashes) = handler
.get_fluent_bundle(
message.from.clone().unwrap().language_code.as_deref(),
message.from.as_ref().unwrap().language_code.as_deref(),
|bundle| build_alternate_response(&bundle, items),
)
.await;
if text == updated_text {
return;
return Ok(());
}
let edit = EditMessageText {
chat_id: message.chat_id(),
message_id: Some(sent),
message_id: Some(sent.message_id),
text: updated_text,
disable_web_page_preview: Some(true),
..Default::default()
};
if let Err(e) = handler.bot.make_request(&edit).await {
tracing::error!("unable to make request: {:?}", e);
with_user_scope(message.from.as_ref(), None, || {
capture_fail(&e);
});
}
handler
.bot
.make_request(&edit)
.await
.map(|_msg| ())
.map_err(Into::into)
}
}

141
src/handlers/error_reply.rs Normal file
View File

@ -0,0 +1,141 @@
use super::Status::*;
use crate::needs_field;
use async_trait::async_trait;
use telegram::*;
pub struct ErrorReplyHandler {
client: reqwest::Client,
}
impl ErrorReplyHandler {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl super::Handler for ErrorReplyHandler {
fn name(&self) -> &'static str {
"error_reply"
}
async fn handle(
&self,
handler: &crate::MessageHandler,
update: &Update,
_command: Option<&Command>,
) -> Result<super::Status, failure::Error> {
let message = needs_field!(update, message);
let text = needs_field!(message, text);
let reply_message = needs_field!(message, reply_to_message);
let reply_message_from = needs_field!(reply_message, from);
let reply_message_text = needs_field!(reply_message, text);
let entities = needs_field!(reply_message, entities);
// Only want to look at messages that are replies to this bot
if reply_message_from.id != handler.bot_user.id {
return Ok(Ignored);
}
let code = match get_code_block(&entities, &reply_message_text) {
Some(code) => code,
_ => return Ok(Ignored),
};
let dsn = match &handler.config.sentry_dsn {
Some(dsn) => dsn,
_ => return Ok(Completed),
};
let auth = format!("DSN {}", dsn);
let data = SentryFeedback {
comments: text.to_string(),
event_id: code,
// This field is required, but Telegram doesn't give us emails...
email: "telegram-user@example.com".to_string(),
name: message
.from
.as_ref()
.map(|from| from.username.clone().unwrap_or_else(|| from.id.to_string())),
};
self.client
.post(&format!(
"https://sentry.io/api/0/projects/{}/{}/user-feedback/",
handler.config.sentry_organization_slug.as_ref().unwrap(),
handler.config.sentry_project_slug.as_ref().unwrap()
))
.json(&data)
.header(reqwest::header::AUTHORIZATION, auth)
.send()
.await?;
handler
.send_generic_reply(&message, "error-feedback")
.await?;
Ok(Completed)
}
}
#[derive(serde::Serialize)]
struct SentryFeedback {
comments: String,
event_id: String,
name: Option<String>,
email: String,
}
fn get_code_block(entities: &[MessageEntity], text: &str) -> Option<String> {
// Find any code blocks, ignore if there's more than one
let code_blocks = entities
.iter()
.filter(|entity| entity.entity_type == MessageEntityType::Code)
.collect::<Vec<_>>();
if code_blocks.len() != 1 {
return None;
}
// Make sure the code block is the correct length
let entity = code_blocks[0];
if entity.length != 36 {
return None;
}
// Iterate the text of the message this is replying to in order to
// get the event ID
let code = text
.chars()
.skip(entity.offset as usize)
.take(entity.length as usize)
.filter(|c| *c != '-')
.collect();
Some(code)
}
#[cfg(test)]
mod tests {
#[test]
fn test_get_code_block() {
let entities = vec![telegram::MessageEntity {
entity_type: telegram::MessageEntityType::Code,
offset: 0,
length: 36,
url: None,
user: None,
}];
let text = "e52569fa-99a0-44fc-ae9d-2477177b550b";
assert_eq!(
Some("e52569fa99a044fcae9d2477177b550b".to_string()),
super::get_code_block(&entities, &text)
);
let entities = vec![];
assert_eq!(None, super::get_code_block(&entities, text));
}
}

View File

@ -1,10 +1,12 @@
use super::Status::*;
use crate::needs_field;
use async_trait::async_trait;
use telegram::*;
pub struct GroupAddHandler;
#[async_trait]
impl crate::Handler for GroupAddHandler {
impl super::Handler for GroupAddHandler {
fn name(&self) -> &'static str {
"group"
}
@ -12,26 +14,23 @@ impl crate::Handler for GroupAddHandler {
async fn handle(
&self,
handler: &crate::MessageHandler,
update: Update,
_command: Option<Command>,
) -> Result<bool, Box<dyn std::error::Error>> {
let message = match update.message {
Some(message) => message,
_ => return Ok(false),
};
update: &Update,
_command: Option<&Command>,
) -> Result<super::Status, failure::Error> {
let message = needs_field!(update, message);
let new_members = match &message.new_chat_members {
Some(members) => members,
_ => return Ok(false),
_ => return Ok(Ignored),
};
if new_members
.iter()
.any(|member| member.id == handler.bot_user.id)
{
handler.handle_welcome(message, "group-add").await;
handler.handle_welcome(message, "group-add").await?;
}
Ok(true)
Ok(Completed)
}
}

View File

@ -1,14 +1,15 @@
use super::Status::*;
use crate::generate_id;
use crate::needs_field;
use crate::sites::PostInfo;
use crate::utils::*;
use async_trait::async_trait;
use sentry::integrations::failure::capture_fail;
use telegram::*;
pub struct InlineHandler;
#[async_trait]
impl crate::Handler for InlineHandler {
impl super::Handler for InlineHandler {
fn name(&self) -> &'static str {
"inline"
}
@ -16,13 +17,10 @@ impl crate::Handler for InlineHandler {
async fn handle(
&self,
handler: &crate::MessageHandler,
update: Update,
_command: Option<Command>,
) -> Result<bool, Box<dyn std::error::Error>> {
let inline = match update.inline_query {
Some(inline) => inline,
None => return Ok(false),
};
update: &Update,
_command: Option<&Command>,
) -> Result<super::Status, failure::Error> {
let inline = needs_field!(update, inline_query);
let links: Vec<_> = handler.finder.links(&inline.query).collect();
let mut results: Vec<PostInfo> = Vec::new();
@ -53,7 +51,7 @@ impl crate::Handler for InlineHandler {
results.extend(info.results);
})
.await;
.await?;
}
// Find if any of our results were personal. If they were, we need to
@ -85,7 +83,7 @@ impl crate::Handler for InlineHandler {
}
let mut answer_inline = AnswerInlineQuery {
inline_query_id: inline.id,
inline_query_id: inline.id.to_owned(),
results: responses,
is_personal: Some(personal),
..Default::default()
@ -98,14 +96,9 @@ impl crate::Handler for InlineHandler {
answer_inline.switch_pm_parameter = Some("help".to_string());
}
if let Err(e) = handler.bot.make_request(&answer_inline).await {
with_user_scope(Some(&inline.from), None, || {
capture_fail(&e);
});
log::error!("Unable to respond to inline: {:?}", e);
}
handler.bot.make_request(&answer_inline).await?;
Ok(true)
Ok(Completed)
}
}

View File

@ -1,6 +1,9 @@
use async_trait::async_trait;
mod channel_photo;
mod chosen_inline_handler;
mod commands;
mod error_reply;
mod group_add;
mod inline_handler;
mod photo;
@ -9,7 +12,41 @@ mod text;
pub use channel_photo::ChannelPhotoHandler;
pub use chosen_inline_handler::ChosenInlineHandler;
pub use commands::CommandHandler;
pub use error_reply::ErrorReplyHandler;
pub use group_add::GroupAddHandler;
pub use inline_handler::InlineHandler;
pub use photo::PhotoHandler;
pub use text::TextHandler;
#[derive(PartialEq)]
pub enum Status {
Ignored,
Completed,
}
#[async_trait]
pub trait Handler: Send + Sync {
/// Name of the handler, for debugging/logging uses.
fn name(&self) -> &'static str;
/// Method called for every update received.
///
/// Returns if the update should be absorbed and not passed to the next handler.
/// Errors are logged to log::error and reported to Sentry, if enabled.
async fn handle(
&self,
handler: &super::MessageHandler,
update: &telegram::Update,
command: Option<&telegram::Command>,
) -> failure::Fallible<Status>;
}
#[macro_export]
macro_rules! needs_field {
($message:expr, $field:tt) => {
match $message.$field {
Some(ref field) => field,
_ => return Ok(crate::handlers::Status::Ignored),
}
};
}

View File

@ -1,15 +1,14 @@
use super::Status::*;
use crate::needs_field;
use async_trait::async_trait;
use sentry::integrations::failure::capture_fail;
use telegram::*;
use crate::utils::{
continuous_action, download_by_id, find_best_photo, get_message, with_user_scope,
};
use crate::utils::{continuous_action, download_by_id, find_best_photo, get_message};
pub struct PhotoHandler;
#[async_trait]
impl crate::Handler for PhotoHandler {
impl super::Handler for PhotoHandler {
fn name(&self) -> &'static str {
"photo"
}
@ -17,23 +16,16 @@ impl crate::Handler for PhotoHandler {
async fn handle(
&self,
handler: &crate::MessageHandler,
update: Update,
_command: Option<Command>,
) -> Result<bool, Box<dyn std::error::Error>> {
let message = match update.message {
Some(message) => message,
_ => return Ok(false),
};
let photos = match &message.photo {
Some(photos) => photos,
_ => return Ok(false),
};
update: &Update,
_command: Option<&Command>,
) -> Result<super::Status, failure::Error> {
let message = needs_field!(update, message);
let photos = needs_field!(message, photo);
let now = std::time::Instant::now();
if message.chat.chat_type != ChatType::Private {
return Ok(false);
return Ok(Ignored);
}
let _action = continuous_action(
@ -45,66 +37,17 @@ impl crate::Handler for PhotoHandler {
);
let best_photo = find_best_photo(&photos).unwrap();
let photo = match download_by_id(&handler.bot, &best_photo.file_id).await {
Ok(photo) => photo,
Err(e) => {
tracing::error!("unable to download file: {:?}", e);
let tags = Some(vec![("command", "photo".to_string())]);
handler
.report_error(&message, tags, || capture_fail(&e))
.await;
return Ok(true);
}
};
let photo = download_by_id(&handler.bot, &best_photo.file_id).await?;
let matches = match handler
.fapi
.image_search(&photo, fautil::MatchType::Close)
.await
.await?
{
Ok(matches) if !matches.matches.is_empty() => matches.matches,
Ok(_matches) => {
let text = handler
.get_fluent_bundle(
message.from.clone().unwrap().language_code.as_deref(),
|bundle| get_message(&bundle, "reverse-no-results", None).unwrap(),
)
.await;
let send_message = SendMessage {
chat_id: message.chat_id(),
text,
reply_to_message_id: Some(message.message_id),
..Default::default()
};
if let Err(e) = handler.bot.make_request(&send_message).await {
tracing::error!("unable to respond to photo: {:?}", e);
with_user_scope(message.from.as_ref(), None, || {
capture_fail(&e);
});
}
let point = influxdb::Query::write_query(influxdb::Timestamp::Now, "source")
.add_field("matches", 0)
.add_field("duration", now.elapsed().as_millis() as i64);
if let Err(e) = handler.influx.query(&point).await {
tracing::error!("unable to send command to InfluxDB: {:?}", e);
with_user_scope(message.from.as_ref(), None, || {
capture_fail(&e);
});
}
return Ok(true);
}
Err(e) => {
tracing::error!("unable to reverse search image file: {:?}", e);
let tags = Some(vec![("command", "photo".to_string())]);
handler
.report_error(&message, tags, || capture_fail(&e))
.await;
return Ok(true);
matches if !matches.matches.is_empty() => matches.matches,
_matches => {
no_results(&handler, &message, now).await?;
return Ok(Completed);
}
};
@ -140,7 +83,7 @@ impl crate::Handler for PhotoHandler {
let text = handler
.get_fluent_bundle(
message.from.clone().unwrap().language_code.as_deref(),
message.from.as_ref().unwrap().language_code.as_deref(),
|bundle| get_message(&bundle, name, Some(args)).unwrap(),
)
.await;
@ -153,25 +96,45 @@ impl crate::Handler for PhotoHandler {
..Default::default()
};
if let Err(e) = handler.bot.make_request(&send_message).await {
tracing::error!("unable to respond to photo: {:?}", e);
with_user_scope(message.from.as_ref(), None, || {
capture_fail(&e);
});
}
handler.bot.make_request(&send_message).await?;
let point = influxdb::Query::write_query(influxdb::Timestamp::Now, "source")
.add_tag("good", first.distance.unwrap() < 5)
.add_field("matches", matches.len() as i64)
.add_field("duration", now.elapsed().as_millis() as i64);
if let Err(e) = handler.influx.query(&point).await {
tracing::error!("unable to send command to InfluxDB: {:?}", e);
with_user_scope(message.from.as_ref(), None, || {
capture_fail(&e);
});
}
let _ = handler.influx.query(&point).await;
Ok(true)
Ok(Completed)
}
}
async fn no_results(
handler: &crate::MessageHandler,
message: &Message,
start: std::time::Instant,
) -> failure::Fallible<()> {
let text = handler
.get_fluent_bundle(
message.from.as_ref().unwrap().language_code.as_deref(),
|bundle| get_message(&bundle, "reverse-no-results", None).unwrap(),
)
.await;
let send_message = SendMessage {
chat_id: message.chat_id(),
text,
reply_to_message_id: Some(message.message_id),
..Default::default()
};
handler.bot.make_request(&send_message).await?;
let point = influxdb::Query::write_query(influxdb::Timestamp::Now, "source")
.add_field("matches", 0)
.add_field("duration", start.elapsed().as_millis() as i64);
let _ = handler.influx.query(&point).await;
Ok(())
}

View File

@ -1,14 +1,15 @@
use super::Status::*;
use crate::needs_field;
use async_trait::async_trait;
use sentry::integrations::failure::capture_fail;
use telegram::*;
use tokio01::runtime::current_thread::block_on_all;
use crate::utils::{get_message, with_user_scope};
use crate::utils::get_message;
pub struct TextHandler;
#[async_trait]
impl crate::Handler for TextHandler {
impl super::Handler for TextHandler {
fn name(&self) -> &'static str {
"text"
}
@ -16,37 +17,27 @@ impl crate::Handler for TextHandler {
async fn handle(
&self,
handler: &crate::MessageHandler,
update: Update,
_command: Option<Command>,
) -> Result<bool, Box<dyn std::error::Error>> {
update: &Update,
_command: Option<&Command>,
) -> Result<super::Status, failure::Error> {
use quaint::prelude::*;
let message = match update.message {
Some(message) => message,
_ => return Ok(false),
};
let text = match &message.text {
Some(text) => text,
_ => return Ok(false),
};
let message = needs_field!(update, message);
let text = needs_field!(message, text);
let now = std::time::Instant::now();
let from = message.from.clone().unwrap();
let from = message.from.as_ref().unwrap();
if text.trim().parse::<i32>().is_err() {
tracing::trace!("got text that wasn't oob, ignoring");
return Ok(false);
return Ok(Ignored);
}
tracing::trace!("checking if message was Twitter code");
let conn = handler
.conn
.check_out()
.await
.expect("Unable to get db conn");
let conn = handler.conn.check_out().await?;
let result = conn
.select(
Select::from_table("twitter_auth")
@ -54,12 +45,11 @@ impl crate::Handler for TextHandler {
.column("request_secret")
.so_that("user_id".equals(from.id)),
)
.await
.expect("Unable to query db");
.await?;
let row = match result.first() {
Some(row) => row,
_ => return Ok(true),
_ => return Ok(Completed),
};
tracing::trace!("we had waiting Twitter code");
@ -74,21 +64,7 @@ impl crate::Handler for TextHandler {
handler.config.twitter_consumer_secret.clone(),
);
let token = match block_on_all(egg_mode::access_token(con_token, &request_token, text)) {
Err(e) => {
tracing::warn!("user was unable to verify OOB: {:?}", e);
handler
.report_error(
&message,
Some(vec![("command", "twitter_auth".to_string())]),
|| capture_fail(&e),
)
.await;
return Ok(true);
}
Ok(token) => token,
};
let token = block_on_all(egg_mode::access_token(con_token, &request_token, text))?;
tracing::trace!("got token");
@ -100,8 +76,7 @@ impl crate::Handler for TextHandler {
tracing::trace!("got access token");
conn.delete(Delete::from_table("twitter_account").so_that("user_id".equals(from.id)))
.await
.expect("Unable to remove auth keys");
.await?;
conn.insert(
Insert::single_into("twitter_account")
@ -110,12 +85,10 @@ impl crate::Handler for TextHandler {
.value("consumer_secret", access.secret.to_string())
.build(),
)
.await
.expect("Unable to insert credential info");
.await?;
conn.delete(Delete::from_table("twitter_auth").so_that("user_id".equals(from.id)))
.await
.expect("Unable to remove auth keys");
.await?;
let mut args = fluent::FluentArgs::new();
args.insert("userName", fluent::FluentValue::from(token.2));
@ -133,24 +106,14 @@ impl crate::Handler for TextHandler {
..Default::default()
};
if let Err(e) = handler.bot.make_request(&message).await {
tracing::warn!("unable to send message: {:?}", e);
with_user_scope(Some(&from), None, || {
capture_fail(&e);
});
}
handler.bot.make_request(&message).await?;
let point = influxdb::Query::write_query(influxdb::Timestamp::Now, "twitter")
.add_tag("type", "added")
.add_field("duration", now.elapsed().as_millis() as i64);
if let Err(e) = handler.influx.query(&point).await {
tracing::error!("unable to send command to InfluxDB: {:?}", e);
with_user_scope(Some(&from), None, || {
capture_fail(&e);
});
}
let _ = handler.influx.query(&point).await;
Ok(true)
Ok(Completed)
}
}

View File

@ -1,7 +1,6 @@
#![feature(try_trait)]
use async_trait::async_trait;
use sentry::integrations::failure::capture_fail;
use sentry::integrations::failure::{capture_error, capture_fail};
use sites::{PostInfo, Site};
use std::collections::HashMap;
use std::sync::Arc;
@ -21,7 +20,7 @@ mod utils;
// MARK: Statics and types
type BoxedSite = Box<dyn Site + Send + Sync>;
type BoxedHandler = Box<dyn Handler + Send + Sync>;
type BoxedHandler = Box<dyn handlers::Handler + Send + Sync>;
/// Generates a random 24 character alphanumeric string.
///
@ -68,7 +67,9 @@ pub struct Config {
// Logging
jaeger_collector: Option<String>,
sentry_dsn: Option<String>,
pub sentry_dsn: Option<String>,
pub sentry_organization_slug: Option<String>,
pub sentry_project_slug: Option<String>,
// Telegram config
telegram_apitoken: String,
@ -167,7 +168,7 @@ async fn main() {
run_migrations(&config.database).await;
let pool = quaint::pooled::Quaint::new(&format!("file:test.db"))
let pool = quaint::pooled::Quaint::new(&format!("file:{}", config.database))
.await
.expect("Unable to connect to database");
@ -237,6 +238,7 @@ async fn main() {
Box::new(handlers::PhotoHandler),
Box::new(handlers::CommandHandler),
Box::new(handlers::TextHandler),
Box::new(handlers::ErrorReplyHandler::new()),
];
let handler = Arc::new(MessageHandler {
@ -257,7 +259,11 @@ async fn main() {
let _guard = if let Some(dsn) = config.sentry_dsn {
sentry::integrations::panic::register_panic_handler();
Some(sentry::init(dsn))
Some(sentry::init(sentry::ClientOptions {
dsn: Some(dsn.parse().unwrap()),
release: option_env!("RELEASE").map(std::borrow::Cow::from),
..Default::default()
}))
} else {
None
};
@ -273,8 +279,7 @@ async fn main() {
url: webhook_endpoint,
};
if let Err(e) = bot.make_request(&set_webhook).await {
capture_fail(&e);
tracing::error!("unable to set webhook: {:?}", e);
panic!(e);
}
receive_webhook(
config.http_host.expect("Missing HTTP_HOST"),
@ -285,8 +290,7 @@ async fn main() {
} else {
let delete_webhook = DeleteWebhook;
if let Err(e) = bot.make_request(&delete_webhook).await {
capture_fail(&e);
tracing::error!("unable to clear webhook: {:?}", e);
panic!(e);
}
poll_updates(bot, handler).await;
}
@ -432,25 +436,7 @@ pub struct MessageHandler {
pub conn: quaint::pooled::Quaint,
}
#[async_trait]
pub trait Handler: Send + Sync {
/// Name of the handler, for debugging/logging uses.
fn name(&self) -> &'static str;
/// Method called for every update received.
///
/// Returns if the update should be absorbed and not passed to the next handler.
/// Errors are logged to log::error and reported to Sentry, if enabled.
async fn handle(
&self,
handler: &MessageHandler,
update: Update,
command: Option<Command>,
) -> Result<bool, Box<dyn std::error::Error>>;
}
impl MessageHandler {
#[tracing::instrument(skip(self, callback))]
async fn get_fluent_bundle<C, R>(&self, requested: Option<&str>, callback: C) -> R
where
C: FnOnce(&fluent::FluentBundle<fluent::FluentResource>) -> R,
@ -529,8 +515,8 @@ impl MessageHandler {
let lang_code = message
.from
.clone()
.map(|from| from.language_code)
.as_ref()
.map(|from| from.language_code.clone())
.flatten();
let msg = self
@ -565,10 +551,10 @@ impl MessageHandler {
}
#[tracing::instrument(skip(self, message))]
async fn handle_welcome(&self, message: Message, command: &str) {
async fn handle_welcome(&self, message: &Message, command: &str) -> failure::Fallible<()> {
use rand::seq::SliceRandom;
let from = message.from.clone().unwrap();
let from = message.from.as_ref().unwrap();
let random_artwork = *STARTING_ARTWORK.choose(&mut rand::thread_rng()).unwrap();
@ -605,22 +591,26 @@ impl MessageHandler {
..Default::default()
};
if let Err(e) = self.bot.make_request(&send_message).await {
tracing::error!("unable to send help message: {:?}", e);
let tags = Some(vec![("command", command.to_string())]);
utils::with_user_scope(message.from.as_ref(), tags, || capture_fail(&e));
}
self.bot
.make_request(&send_message)
.await
.map(|_msg| ())
.map_err(Into::into)
}
#[tracing::instrument(skip(self, message))]
async fn send_generic_reply(&self, message: &Message, name: &str) {
let language_code = match &message.from {
Some(from) => &from.language_code,
None => return,
};
async fn send_generic_reply(
&self,
message: &Message,
name: &str,
) -> failure::Fallible<Message> {
let language_code = message
.from
.as_ref()
.and_then(|from| from.language_code.as_deref());
let text = self
.get_fluent_bundle(language_code.as_deref(), |bundle| {
.get_fluent_bundle(language_code, |bundle| {
utils::get_message(&bundle, name, None).unwrap()
})
.await;
@ -632,27 +622,49 @@ impl MessageHandler {
..Default::default()
};
if let Err(e) = self.bot.make_request(&send_message).await {
tracing::error!("unable to make request: {:?}", e);
utils::with_user_scope(message.from.as_ref(), None, || {
capture_fail(&e);
});
}
self.bot
.make_request(&send_message)
.await
.map_err(Into::into)
}
#[tracing::instrument(skip(self, update))]
async fn handle_update(&self, update: Update) {
let user = update
.message
.as_ref()
.and_then(|message| message.from.as_ref());
for handler in &self.handlers {
let command = update
.message
.as_ref()
.and_then(|message| message.get_command());
tracing::trace!("running handler {}", handler.name());
tracing::trace!(handler = handler.name(), "running handler");
match handler.handle(&self, update.clone(), command.clone()).await {
Ok(absorb) if absorb => break,
Err(e) => log::error!("Error handling update: {:#?}", e), // Should this break?
match handler.handle(&self, &update, command.as_ref()).await {
Ok(status) if status == handlers::Status::Completed => break,
Err(e) => {
log::error!("Error handling update: {:#?}", e);
let mut tags = vec![("handler", handler.name().to_string())];
if let Some(user) = user {
tags.push(("user_id", user.id.to_string()));
}
if let Some(command) = command {
tags.push(("command", command.name));
}
if let Some(msg) = &update.message {
self.report_error(&msg, Some(tags), || capture_error(&e))
.await;
} else {
capture_error(&e);
}
break;
}
_ => (),
}
}

View File

@ -208,18 +208,33 @@ pub struct E621 {
client: reqwest::Client,
}
#[derive(Deserialize)]
#[derive(Debug, Deserialize)]
struct E621PostFile {
ext: String,
url: String,
}
#[derive(Debug, Deserialize)]
struct E621PostPreview {
url: String,
}
#[derive(Debug, Deserialize)]
struct E621Post {
id: i32,
file_url: String,
preview_url: String,
file_ext: String,
file: E621PostFile,
preview: E621PostPreview,
}
#[derive(Debug, Deserialize)]
struct E621Resp {
post: E621Post,
}
impl E621 {
pub fn new() -> Self {
Self {
show: regex::Regex::new(r"https?://(?P<host>e(?:621|926)\.net)/post/show/(?P<id>\d+)(?:/(?P<tags>.+))?").unwrap(),
show: regex::Regex::new(r"https?://(?P<host>e(?:621|926)\.net)/(?:post/show/|posts/)(?P<id>\d+)(?:/(?P<tags>.+))?").unwrap(),
data: regex::Regex::new(r"https?://(?P<host>static\d+\.e(?:621|926)\.net)/data/(?:(?P<modifier>sample|preview)/)?[0-9a-f]{2}/[0-9a-f]{2}/(?P<md5>[0-9a-f]{32})\.(?P<ext>.+)").unwrap(),
client: reqwest::Client::new(),
@ -246,15 +261,15 @@ impl Site for E621 {
let captures = self.show.captures(url).unwrap();
let id = &captures["id"];
format!("https://e621.net/post/show.json?id={}", id)
format!("https://e621.net/posts/{}.json", id)
} else {
let captures = self.data.captures(url).unwrap();
let md5 = &captures["md5"];
format!("https://e621.net/post/show.json?md5={}", md5)
format!("https://e621.net/posts.json?md5={}", md5)
};
let resp: E621Post = self
let resp: E621Resp = self
.client
.get(&endpoint)
.header(header::USER_AGENT, USER_AGENT)
@ -264,10 +279,10 @@ impl Site for E621 {
.await?;
Ok(Some(vec![PostInfo {
file_type: resp.file_ext,
url: resp.file_url,
thumb: Some(resp.preview_url),
source_link: Some(format!("https://e621.net/post/show/{}", resp.id)),
file_type: resp.post.file.ext,
url: resp.post.file.url,
thumb: Some(resp.post.preview.url),
source_link: Some(format!("https://e621.net/posts/{}", resp.post.id)),
..Default::default()
}]))
}

View File

@ -20,7 +20,7 @@ pub async fn find_images<'a, C>(
links: Vec<&'a str>,
sites: &mut [BoxedSite],
callback: &mut C,
) -> Vec<&'a str>
) -> failure::Fallible<Vec<&'a str>>
where
C: FnMut(SiteCallback),
{
@ -33,11 +33,10 @@ where
if site.url_supported(link).await {
tracing::debug!("link {} supported by {}", link, site.name());
match site.get_images(user.id, link).await {
// Got results successfully and there were results
// Execute callback with site info and results
Ok(results) if results.is_some() => {
let results = results.unwrap(); // we know this is safe
let images = site.get_images(user.id, link).await?;
match images {
Some(results) => {
tracing::debug!("found images: {:?}", results);
callback(SiteCallback {
site: &site,
@ -47,31 +46,16 @@ where
});
break 'link;
}
// Got results successfully and there were NO results
// Continue onto next link
Ok(_) => {
_ => {
missing.push(link);
continue 'link;
}
// Got error while processing, report and move onto next link
Err(e) => {
missing.push(link);
tracing::warn!("unable to get results: {:?}", e);
with_user_scope(
Some(user),
Some(vec![("site", site.name().to_string())]),
|| {
capture_fail(&e);
},
);
continue 'link;
}
}
}
}
}
missing
Ok(missing)
}
pub fn find_best_photo(sizes: &[telegram::PhotoSize]) -> Option<&telegram::PhotoSize> {

View File

@ -5,9 +5,9 @@ pub enum Error {
#[fail(display = "telegram error: {}", _0)]
Telegram(TelegramError),
#[fail(display = "json parsing error: {}", _0)]
JSON(serde_json::Error),
JSON(#[fail(cause)] serde_json::Error),
#[fail(display = "http error: {}", _0)]
Request(reqwest::Error),
Request(#[fail(cause)] reqwest::Error),
}
impl From<reqwest::Error> for Error {

View File

@ -33,7 +33,7 @@ impl<T> Into<Result<T, Error>> for Response<T> {
}
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Default)]
pub struct Update {
pub update_id: i32,
pub message: Option<Message>,
@ -45,7 +45,7 @@ pub struct Update {
pub callback_query: Option<CallbackQuery>,
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Default)]
pub struct User {
pub id: i32,
pub is_bot: bool,
@ -64,7 +64,13 @@ pub enum ChatType {
Channel,
}
#[derive(Clone, Debug, Deserialize)]
impl Default for ChatType {
fn default() -> Self {
ChatType::Private
}
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct Chat {
pub id: i64,
#[serde(rename = "type")]
@ -100,7 +106,7 @@ pub enum MessageEntityType {
TextMention,
}
#[derive(Clone, Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize, Default)]
pub struct Message {
pub message_id: i32,
pub from: Option<User>,