diff --git a/.drone.yml b/.drone.yml index e8e13e4..2d7c77f 100644 --- a/.drone.yml +++ b/.drone.yml @@ -14,7 +14,9 @@ steps: - 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 @@ -35,6 +37,8 @@ steps: image: plugins/docker settings: auto_tag: true + build_args_from_env: + - DRONE_COMMIT_SHA password: from_secret: docker_password registry: registry.huefox.com diff --git a/Dockerfile b/Dockerfile index 54908b7..73bbca6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 . diff --git a/langs/en-US/foxbot.ftl b/langs/en-US/foxbot.ftl index 882bd13..2b210ee 100644 --- a/langs/en-US/foxbot.ftl +++ b/langs/en-US/foxbot.ftl @@ -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. diff --git a/quaint b/quaint new file mode 160000 index 0000000..dc628de --- /dev/null +++ b/quaint @@ -0,0 +1 @@ +Subproject commit dc628de3342203b71cdedd4ff3c8a86eb7acefa4 diff --git a/src/handlers/chosen_inline_handler.rs b/src/handlers/chosen_inline_handler.rs index 8187ffe..0d92588 100644 --- a/src/handlers/chosen_inline_handler.rs +++ b/src/handlers/chosen_inline_handler.rs @@ -1,4 +1,5 @@ use super::Status::*; +use crate::needs_field; use async_trait::async_trait; use telegram::*; @@ -16,15 +17,13 @@ impl super::Handler for ChosenInlineHandler { update: &Update, _command: Option<&Command>, ) -> Result { - Ok(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); + let chosen_result = needs_field!(update, chosen_inline_result); - let _ = handler.influx.query(&point).await; + let point = influxdb::Query::write_query(influxdb::Timestamp::Now, "chosen") + .add_field("user_id", chosen_result.from.id); - Completed - } else { - Ignored - }) + let _ = handler.influx.query(&point).await; + + Ok(Completed) } } diff --git a/src/handlers/commands.rs b/src/handlers/commands.rs index dfd5874..67deac2 100644 --- a/src/handlers/commands.rs +++ b/src/handlers/commands.rs @@ -51,6 +51,7 @@ impl super::Handler for CommandHandler { "/mirror" => self.handle_mirror(&handler, message).await, "/source" => self.handle_source(&handler, message).await, "/alts" => self.handle_alts(&handler, message).await, + "/error" => Err(failure::format_err!("a test error message")), _ => { tracing::info!("unknown command: {}", command.name); Ok(()) diff --git a/src/handlers/error_reply.rs b/src/handlers/error_reply.rs new file mode 100644 index 0000000..162a392 --- /dev/null +++ b/src/handlers/error_reply.rs @@ -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 { + 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, + email: String, +} + +fn get_code_block(entities: &[MessageEntity], text: &str) -> Option { + // Find any code blocks, ignore if there's more than one + let code_blocks = entities + .iter() + .filter(|entity| entity.entity_type == MessageEntityType::Code) + .collect::>(); + 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)); + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 45965c8..f3b60a5 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -3,6 +3,7 @@ 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; @@ -11,6 +12,7 @@ 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; diff --git a/src/main.rs b/src/main.rs index a510931..62c5310 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,7 +67,9 @@ pub struct Config { // Logging jaeger_collector: Option, - sentry_dsn: Option, + pub sentry_dsn: Option, + pub sentry_organization_slug: Option, + pub sentry_project_slug: Option, // Telegram config telegram_apitoken: String, @@ -236,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 { @@ -256,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 }; @@ -432,7 +439,6 @@ pub struct MessageHandler { } impl MessageHandler { - #[tracing::instrument(skip(self, callback))] async fn get_fluent_bundle(&self, requested: Option<&str>, callback: C) -> R where C: FnOnce(&fluent::FluentBundle) -> R, @@ -643,14 +649,23 @@ impl MessageHandler { 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)); + } - utils::with_user_scope(user, Some(tags), || { + if let Some(msg) = &update.message { + self.report_error(&msg, Some(tags), || capture_error(&e)) + .await; + } else { capture_error(&e); - }); + } + + break; } _ => (), }