Merge branch 'master' into sqlite
This commit is contained in:
commit
8e29ac7764
47
.drone.yml
47
.drone.yml
|
@ -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
|
||||
|
||||
...
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"] }
|
||||
|
|
|
@ -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 .
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
118
src/main.rs
118
src/main.rs
|
@ -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;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
|
39
src/sites.rs
39
src/sites.rs
|
@ -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()
|
||||
}]))
|
||||
}
|
||||
|
|
30
src/utils.rs
30
src/utils.rs
|
@ -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> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
|
|
Loading…
Reference in New Issue