feat: add comical amounts of logging

This commit is contained in:
Suya1671 2025-06-15 22:30:44 +02:00
parent c6135a629d
commit fb6f80e5b8
No known key found for this signature in database
19 changed files with 622 additions and 249 deletions

14
Cargo.lock generated
View file

@ -369,9 +369,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.39"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
dependencies = [
"clap_builder",
"clap_derive",
@ -379,9 +379,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.39"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
dependencies = [
"anstream",
"anstyle",
@ -391,9 +391,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.32"
version = "4.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
dependencies = [
"heck",
"proc-macro2",
@ -2426,6 +2426,7 @@ dependencies = [
"thiserror 2.0.12",
"time",
"tokio",
"tower-http",
"tracing",
"tracing-error",
"tracing-subscriber",
@ -2950,6 +2951,7 @@ dependencies = [
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]

View file

@ -7,7 +7,7 @@ edition = "2024"
[dependencies]
axum = "0.8.4"
clap = { version = "4.5.39", features = ["derive"] }
clap = { version = "4.5.40", features = ["derive"] }
displaydoc = "0.2.5"
error-stack = { version = "0.5.0", features = [
"eyre",
@ -40,6 +40,7 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
dotenvy = { git = "https://github.com/allan2/dotenvy", features = ["macros"] }
url = "2.5.4"
serde_json = "1.0.140"
tower-http = { version = "0.6.6", features = ["trace"] }
[features]
encrypt = ["libsqlite3-sys/bundled-sqlcipher"]

View file

@ -0,0 +1,87 @@
-- Add migration script here
-- Rename and retype is_prefix (bool) to type (interger)
-- First, drop the trigger that references the triggers table
DROP TRIGGER IF EXISTS ensure_system_id_update_members_table;
-- Create a new table with the updated schema
CREATE TABLE triggers_new (
id INTEGER NOT NULL PRIMARY KEY,
-- The member that will front
member_id INTEGER NOT NULL REFERENCES members (id),
-- The trigger text. This will be the prefix or suffix depending on the is_prefix flag
text TEXT NOT NULL,
-- 0 if suffix, 1 if prefix, see rust implementation for details
typ INTEGER NOT NULL,
system_id INTEGER NOT NULL,
-- Create unique constraints using the system_id and trigger type
CONSTRAINT unique_trigger UNIQUE (system_id, text, typ)
) STRICT;
-- Migrate existing data from the old table
INSERT INTO
triggers_new (member_id, text, typ, system_id)
SELECT
member_id,
text,
-- 0 is suffix, 1 is prefix
is_prefix,
system_id
FROM
triggers;
-- Recreate original state/names
DROP TRIGGER IF EXISTS ensure_system_id;
DROP TRIGGER IF EXISTS ensure_system_id_update;
DROP TABLE triggers;
ALTER TABLE triggers_new
RENAME TO triggers;
CREATE TRIGGER ensure_system_id BEFORE INSERT ON triggers FOR EACH ROW BEGIN
SELECT
RAISE (
ABORT,
'system_id must be the same as the system_id on member'
)
WHERE
NEW.system_id != (
SELECT
system_id
FROM
members
WHERE
id = NEW.member_id
);
END;
CREATE TRIGGER ensure_system_id_update BEFORE
UPDATE ON triggers FOR EACH ROW BEGIN
SELECT
RAISE (
ABORT,
'system_id must be the same as the system_id on member'
)
WHERE
NEW.system_id != (
SELECT
system_id
FROM
members
WHERE
id = NEW.member_id
);
END;
CREATE TRIGGER ensure_system_id_update_members_table BEFORE
UPDATE ON members FOR EACH ROW BEGIN
UPDATE triggers
SET
system_id = NEW.system_id
WHERE
member_id = NEW.id;
END;

View file

@ -2,11 +2,12 @@ use std::sync::Arc;
use error_stack::{Result, ResultExt, report};
use slack_morphism::prelude::*;
use tracing::{debug, info};
use tracing::{debug, info, trace};
use crate::{
BOT_TOKEN,
commands::members,
fields,
models::{
member::{self, Member, View},
system::{ChangeActiveMemberError, System},
@ -53,18 +54,20 @@ pub enum Members {
#[derive(thiserror::Error, displaydoc::Display, Debug)]
pub enum CommandError {
/// Error while calling the Slack API
Slack,
SlackApi,
/// Error while calling the database
Sqlx,
}
impl Members {
#[tracing::instrument(skip_all)]
pub async fn run(
self,
event: SlackCommandEvent,
client: Arc<SlackHyperClient>,
state: SlackClientEventsUserState,
) -> Result<SlackCommandEventResponse, CommandError> {
trace!("Running members command");
match self {
Self::Add => {
let token = &BOT_TOKEN;
@ -72,7 +75,7 @@ impl Members {
Self::create_member(event, session).await
}
Self::Delete { member } => {
info!("Deleting member {member}");
debug!(member_id = member, "Delete member command not implemented");
Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text("Working on it".into()),
))
@ -88,12 +91,14 @@ impl Members {
}
}
#[tracing::instrument(skip(event, state), fields(system_id))]
async fn switch_member(
event: SlackCommandEvent,
state: SlackClientEventsUserState,
member_id: Option<i64>,
base: bool,
) -> Result<SlackCommandEventResponse, CommandError> {
trace!("Switching member");
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
@ -101,30 +106,45 @@ impl Members {
.await
.change_context(CommandError::Sqlx)?
else {
debug!("User has no system configured");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text("You don't have a system yet!".into()),
));
};
fields!(system_id = %system.id);
debug!("Found user system");
let new_active_member_id = if base {
None
} else {
member::Id::new(
member_id.expect("member_id to be Some, as the clap rules require it to be."),
)
.validate_by_system(system.id, &user_state.db)
.await
.ok()
let member_id =
member_id.expect("member_id to be Some, as the clap rules require it to be.");
debug!(requested_member_id = member_id, "Validating member ID");
member::Id::new(member_id)
.validate_by_system(system.id, &user_state.db)
.await
.ok()
};
debug!(target_member_id = ?new_active_member_id, "Changing active member");
let new_member = system
.change_active_member(new_active_member_id, &user_state.db)
.await;
let response = match new_member {
Ok(Some(member)) => format!("Switch to member {}", member.full_name),
Ok(None) => "Switched to base account".into(),
Ok(Some(member)) => {
info!(member_name = %member.full_name, member_id = %member.id, "Successfully switched to member");
format!("Switch to member {}", member.full_name)
}
Ok(None) => {
info!("Successfully switched to base account");
"Switched to base account".into()
}
Err(ChangeActiveMemberError::MemberNotFound) => {
debug!("Requested member not found in system");
"The member you gave doesn't exist!".into()
}
Err(ChangeActiveMemberError::Sqlx(err)) => {
@ -137,31 +157,37 @@ impl Members {
))
}
#[tracing::instrument(skip(event, state), fields(user_id, system_id))]
async fn list_members(
event: SlackCommandEvent,
state: SlackClientEventsUserState,
system: Option<String>,
) -> Result<SlackCommandEventResponse, CommandError> {
trace!("Listing all members");
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
// If the input exists, parse it into a user ID
// If it doesn't exist, use the user ID of the event.
// If the user ID is invalid, return an error.
// Theres probably a better way to write this behaviour but I'm not sure how.
// There's probably a better way to write this behaviour but I'm not sure how.
let Some((user_id, is_author)) = system.map_or_else(
|| Some((user::Id::new(event.user_id), true)),
|u| user::parse_slack_user_id(&u).map(|id| (id, false)),
) else {
debug!("Invalid user ID provided in system parameter");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text("Invalid user ID".into()),
));
};
fields!(user_id = %user_id.clone());
let Some(system) = System::fetch_by_user_id(&user_state.db, &user_id)
.await
.change_context(CommandError::Sqlx)?
else {
debug!(target_user_id = %user_id, is_self = is_author, "Target user has no system");
return if is_author {
Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text("You don't have a system yet!".into()),
@ -173,11 +199,15 @@ impl Members {
};
};
fields!(system_id = %system.id);
let members = system
.get_members(&user_state.db)
.await
.change_context(CommandError::Sqlx)?;
debug!(member_count = members.len(), "Retrieved system members");
let member_blocks = members
.into_iter()
.map(|member| {
@ -213,11 +243,14 @@ impl Members {
))
}
#[tracing::instrument(skip(event, state), fields(user_id = %event.user_id, system_id, member_id))]
async fn member_info(
event: SlackCommandEvent,
state: &SlackClientEventsUserState,
member_id: i64,
) -> Result<SlackCommandEventResponse, CommandError> {
trace!("Running member info command");
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
let member_id = member::Id::new(member_id);
@ -227,6 +260,7 @@ impl Members {
.change_context(CommandError::Sqlx)?
.map(|system| system.id)
else {
debug!("User has no system configured");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text(
"You don't have a system yet! Make one with `/system create <name>`".into(),
@ -234,16 +268,22 @@ impl Members {
));
};
fields!(system_id = %system_id);
let Some(member) = Member::fetch_by_and_trust_id(system_id, member_id, &user_state.db)
.await
.change_context(CommandError::Sqlx)?
else {
debug!("Member not found");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new()
.with_text("Member not found. Make sure you used the correct ID".into()),
));
};
fields!(member_id = %member.id);
debug!("Member found");
let fields = [
Some(md!("Display Name: {}", member.display_name)),
Some(md!("Member ID: {}", member.id)),
@ -270,31 +310,36 @@ impl Members {
))
}
#[tracing::instrument(skip(event, session), fields(view_id))]
async fn create_member(
event: SlackCommandEvent,
session: SlackClientSession<'_, SlackClientHyperHttpsConnector>,
) -> Result<SlackCommandEventResponse, CommandError> {
trace!("Running member creation command");
let view = View::create_add_view();
let view = session
.views_open(&SlackApiViewsOpenRequest::new(event.trigger_id, view))
.await
.attach_printable("Error opening view")
.change_context(CommandError::Slack)?;
.change_context(CommandError::SlackApi)?;
debug!("Opened view: {:#?}", view);
info!(view_id = %view.view.state_params.id, "Successfully opened member creation view");
Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text("View opened!".into()),
))
}
#[tracing::instrument(skip(event, session, state), fields(user_id = %event.user_id, trigger_id = %event.trigger_id))]
async fn edit_member(
event: SlackCommandEvent,
session: SlackClientSession<'_, SlackClientHyperHttpsConnector>,
state: &SlackClientEventsUserState,
member_id: i64,
) -> Result<SlackCommandEventResponse, CommandError> {
trace!("Running member edit command");
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
let user_id = user::Id::new(event.user_id);
@ -305,6 +350,7 @@ impl Members {
.change_context(CommandError::Sqlx)?
.map(|system| system.id)
else {
debug!("User has no system configured");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text(
"You don't have a system yet! Make one with `/system create <name>`".into(),
@ -333,9 +379,9 @@ impl Members {
))
.await
.attach_printable("Error opening view")
.change_context(CommandError::Slack)?;
.change_context(CommandError::SlackApi)?;
debug!("Opened view: {:#?}", view);
info!(view_id = %view.view.state_params.id, member_id = %member_id, "Successfully opened member edit view");
Ok(SlackCommandEventResponse::new(SlackMessageContent::new()))
}

View file

@ -4,15 +4,17 @@ mod members;
mod system;
mod triggers;
use axum::{Extension, Json};
use clap::Parser;
use clap::{Parser, error::ErrorKind};
use error_stack::ResultExt;
use members::Members;
use slack_morphism::prelude::*;
use system::System;
use tracing::{debug, error};
use tracing::{Level, debug, error, trace};
use triggers::Triggers;
use crate::fields;
#[derive(clap::Parser, Debug)]
#[command(color(clap::ColorChoice::Never))]
enum Command {
@ -25,6 +27,7 @@ enum Command {
}
impl Command {
#[tracing::instrument(level = Level::DEBUG, skip(event, client, state), fields(runner_user_id = %event.user_id, runner_channel_id = %event.channel_id, runner_channel_name = ?event.channel_name, trigger_id = %event.trigger_id))]
pub async fn run(
self,
event: SlackCommandEvent,
@ -35,29 +38,30 @@ impl Command {
Self::Members(members) => members
.run(event, client, state)
.await
.attach_printable("Failed to run members command")
.change_context(CommandError::Command),
.change_context(CommandError::Members),
Self::System(system) => system
.run(event, client, state)
.await
.attach_printable("Failed to run system command")
.change_context(CommandError::Command),
.change_context(CommandError::System),
Self::Triggers(triggers) => triggers
.run(event, client, state)
.await
.attach_printable("Failed to run triggers command")
.change_context(CommandError::Command),
.change_context(CommandError::Triggers),
}
}
}
#[derive(thiserror::Error, displaydoc::Display, Debug)]
enum CommandError {
/// Error running the command
Command,
/// Error running the members command
Members,
/// Error running the triggers command
Triggers,
/// Error running the system command
System,
}
// TODO: figure out error handling
// TO-DO: figure out error handling
#[tracing::instrument(skip(environment, event))]
pub async fn process_command_event(
Extension(environment): Extension<Arc<SlackHyperListenerEnvironment>>,
@ -69,7 +73,7 @@ pub async fn process_command_event(
match command_event_callback(event, client, state).await {
Ok(response) => Json(response),
Err(e) => {
error!("Error processing command event: {:#?}", e);
error!(error = ?e, "Error processing command event");
Json(SlackCommandEventResponse::new(
SlackMessageContent::new()
.with_text("Error processing command! Logged to developers".into()),
@ -78,12 +82,13 @@ pub async fn process_command_event(
}
}
#[tracing::instrument(level = Level::TRACE, skip(client, state), fields(command))]
async fn command_event_callback(
event: SlackCommandEvent,
client: Arc<SlackHyperClient>,
state: SlackClientEventsUserState,
) -> Result<SlackCommandEventResponse, CommandError> {
debug!("Received command: {:?}", event.command);
trace!(command = ?event.command, "Received command");
let formatted_command = event.command.0.trim_start_matches('/');
let formatted = event.text.as_ref().map_or_else(
@ -91,22 +96,21 @@ async fn command_event_callback(
|text| format!("slack-system-bot {formatted_command} {text}"),
);
debug!("Formatted command: {formatted}");
fields!(command = &formatted);
let parser = Command::try_parse_from(formatted.split_whitespace());
match parser {
Ok(parser) => {
debug!("Parsed command: {:?}", parser);
debug!(?parser, "Parsed command. Running...");
let result = parser.run(event, client, state).await;
match result {
Ok(res) => {
debug!("Command {} executed successfully", formatted);
debug!("Command executed successfully");
Ok(res)
}
Err(e) => {
error!("Error running command {formatted}");
error!("{e:?}");
error!(error = ?e, "Error running command");
Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text(
"Error running command! TODO: show error info on slack".into(),
@ -116,6 +120,15 @@ async fn command_event_callback(
}
}
Err(error) => {
if !matches!(
error.kind(),
ErrorKind::DisplayHelp
| ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
| ErrorKind::DisplayVersion
) {
debug!(error = ?error, "Error parsing command. Most likely user's fault");
}
let formatted = error.render();
Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text(formatted.to_string()),

View file

@ -4,9 +4,10 @@ use error_stack::{Result, ResultExt};
use oauth2::CsrfToken;
use slack_morphism::prelude::*;
use tokio::runtime::Handle;
use tracing::debug;
use tracing::{debug, trace};
use crate::{
fields,
models::{system, user},
oauth::create_oauth_client,
};
@ -39,6 +40,7 @@ pub enum CommandError {
}
impl System {
#[tracing::instrument(skip_all)]
pub async fn run(
self,
event: SlackCommandEvent,
@ -55,13 +57,14 @@ impl System {
}
}
#[tracing::instrument(skip_all, fields(user_id, system_id))]
async fn get_system_info(
event: SlackCommandEvent,
client: Arc<SlackHyperClient>,
state: SlackClientEventsUserState,
user: Option<String>,
) -> Result<SlackCommandEventResponse, CommandError> {
debug!("Getting system info");
trace!("Getting system info");
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
@ -82,11 +85,16 @@ impl System {
));
};
fields!(user_id = %&user_id);
trace!("Mapped user ID");
let system = system::System::fetch_by_user_id(&user_state.db, &user_id)
.await
.change_context(CommandError::Sqlx)?;
if let Some(system) = system {
fields!(system_id = %system.id);
debug!("Fetched system");
let fronting_member = system
.active_member(&user_state.db)
.await
@ -108,6 +116,7 @@ impl System {
]),
))
} else {
debug!("User does not have a system");
Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_blocks(slack_blocks![some_into(
SlackSectionBlock::new().with_text(md!("This user doesn't have a system!"))
@ -116,12 +125,13 @@ impl System {
}
}
#[tracing::instrument(skip(event, state), fields(system_id))]
async fn edit_system_name(
event: SlackCommandEvent,
state: SlackClientEventsUserState,
name: String,
) -> Result<SlackCommandEventResponse, CommandError> {
debug!("Editing system name {name}");
trace!("Editing system name");
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
@ -141,40 +151,35 @@ impl System {
));
};
sqlx::query!(
r#"
UPDATE systems
SET name = $1
WHERE id = $2
"#,
name,
system_id
)
.execute(&user_state.db)
.await
.change_context(CommandError::Sqlx)?;
fields!(system_id = %system_id);
system_id
.rename(&name, &user_state.db)
.await
.change_context(CommandError::Sqlx)?;
Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text("Successfully updated system name!".into()),
))
}
#[tracing::instrument(skip(event, state))]
async fn create_system(
event: SlackCommandEvent,
state: SlackClientEventsUserState,
name: String,
) -> Result<SlackCommandEventResponse, CommandError> {
debug!("Creating system {name}");
trace!("Creating system");
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
let user_id = user::Id::new(event.user_id);
// todo: somehow remove this clone with cleaner code in the future`
if system::System::fetch_by_user_id(&user_state.db, &user::Id::new(event.user_id.clone()))
if let Some(system) = system::System::fetch_by_user_id(&user_state.db, &user_id)
.await
.change_context(CommandError::Sqlx)?
.is_some()
{
debug!(system_id = %system.id, "User already has a system");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text("You already have a system! If you need to reauthenticate, run /system reauth. If you need to change your system name, run /system rename".into()),
));
@ -182,11 +187,11 @@ impl System {
let oauth_client = create_oauth_client();
// note: we aren't doing PKCE since this is only ran on a trusted server
// Note: we aren't doing PKCE since this is only ran on a trusted server
let (auth_url, csrf_token) = oauth_client
.authorize_url(CsrfToken::new_random)
// so we get a regular token as well. Required by oauth2 for some reason
// So we get a regular token as well. Required by oauth2 for some reason
.add_extra_param("scope", "commands")
.add_extra_param("user_scope", "users.profile:read,chat:write")
.url();
@ -199,7 +204,7 @@ impl System {
VALUES ($1, $2, $3)
"#,
name,
event.user_id.0,
user_id.id,
secret
)
.execute(&user_state.db)

View file

@ -5,7 +5,7 @@ use slack_morphism::prelude::*;
use tracing::debug;
use crate::{
BOT_TOKEN,
BOT_TOKEN, fields,
models::{
member::{self, Member},
system::System,
@ -46,6 +46,7 @@ pub enum CommandError {
}
impl Triggers {
#[tracing::instrument(skip_all)]
pub async fn run(
self,
event: SlackCommandEvent,
@ -68,6 +69,7 @@ impl Triggers {
}
}
#[tracing::instrument(skip(event, state, session), fields(system_id, member_id))]
async fn create_trigger(
event: SlackCommandEvent,
state: &SlackClientEventsUserState,
@ -84,6 +86,7 @@ impl Triggers {
.change_context(CommandError::Sqlx)?
.map(|system| system.id)
else {
debug!("User does not have a system");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text(
"You don't have a system yet! Make one with `/system create <name>`".into(),
@ -91,18 +94,23 @@ impl Triggers {
));
};
fields!(system_id = %system_id);
let Some(member_id) = Member::fetch_by_and_trust_id(system_id, member_id, &user_state.db)
.await
.change_context(CommandError::Sqlx)?
.map(|member| member.id)
else {
debug!("Member not found");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new()
.with_text("Member not found. Make sure you used the correct ID".into()),
));
};
let view = trigger::View::new(String::new(), true).create_add_view(member_id);
fields!(member_id = %member_id);
let view = trigger::View::default().create_add_view(member_id);
let view = session
.views_open(&SlackApiViewsOpenRequest::new(
event.trigger_id.clone(),
@ -112,11 +120,12 @@ impl Triggers {
.attach_printable("Error opening view")
.change_context(CommandError::Slack)?;
debug!("Opened view: {:#?}", view);
debug!(?view, "Opened view");
Ok(SlackCommandEventResponse::new(SlackMessageContent::new()))
}
#[tracing::instrument(skip(event, state), fields(system_id))]
pub async fn delete_trigger(
event: SlackCommandEvent,
state: &SlackClientEventsUserState,
@ -139,11 +148,14 @@ impl Triggers {
));
};
fields!(system_id = %system_id);
// Validate the trigger belongs to the user's system
let Ok(trigger_id) = trigger_id
.validate_by_system(system_id, &user_state.db)
.await
else {
debug!("Trigger not found");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new()
.with_text("Trigger not found. Make sure you used the correct ID".into()),
@ -161,6 +173,7 @@ impl Triggers {
))
}
#[tracing::instrument(skip(event, state), fields(system_id))]
pub async fn list_triggers(
event: SlackCommandEvent,
state: &SlackClientEventsUserState,
@ -175,6 +188,7 @@ impl Triggers {
.change_context(CommandError::Sqlx)?
.map(|system| system.id)
else {
debug!("User doesn't have a system");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text(
"You don't have a system yet! Make one with `/system create <name>`".into(),
@ -182,6 +196,8 @@ impl Triggers {
));
};
fields!(system_id = %system_id);
let triggers = if let Some(member_id) = member_id {
let member_id = member::Id::new(member_id);
@ -190,12 +206,15 @@ impl Triggers {
.validate_by_system(system_id, &user_state.db)
.await
else {
debug!("Member not found");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new()
.with_text("Member not found. Make sure you used the correct ID".into()),
));
};
fields!(member_id = %member_id);
member_id
.fetch_triggers(&user_state.db)
.await
@ -208,26 +227,21 @@ impl Triggers {
};
if triggers.is_empty() {
debug!("No triggers found");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text("No triggers found.".into()),
));
}
debug!(len = triggers.len(), "Found triggers");
let trigger_blocks = triggers
.into_iter()
.map(|trigger| {
let fields = vec![
md!("Trigger ID: {}", trigger.id),
md!("Member ID: {}", trigger.member_id),
md!(
"{}: {}",
if trigger.is_prefix {
"Prefix"
} else {
"Suffix"
},
trigger.text
),
md!("{}: {}", trigger.typ, trigger.text),
];
SlackSectionBlock::new()
@ -242,6 +256,7 @@ impl Triggers {
))
}
#[tracing::instrument(skip(event, state, session), fields(system_id))]
pub async fn edit_trigger(
event: SlackCommandEvent,
state: &SlackClientEventsUserState,
@ -258,6 +273,7 @@ impl Triggers {
.change_context(CommandError::Sqlx)?
.map(|system| system.id)
else {
debug!("User does not have a system");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new().with_text(
"You don't have a system yet! Make one with `/system create <name>`".into(),
@ -265,6 +281,8 @@ impl Triggers {
));
};
fields!(system_id = %system_id);
// Validate the trigger belongs to the user's system
let Ok(trigger_id) = trigger_id
.validate_by_system(system_id, &user_state.db)
@ -276,12 +294,15 @@ impl Triggers {
));
};
fields!(trigger_id = %trigger_id);
// Fetch the trigger to edit
let trigger = trigger::Trigger::fetch_by_id(trigger_id, &user_state.db)
.await
.change_context(CommandError::Sqlx)?;
let Some(trigger) = trigger else {
debug!("Trigger not found");
return Ok(SlackCommandEventResponse::new(
SlackMessageContent::new()
.with_text("Trigger not found. Make sure you used the correct ID".into()),
@ -299,7 +320,7 @@ impl Triggers {
.attach_printable("Error opening view")
.change_context(CommandError::Slack)?;
debug!("Opened view: {:#?}", view);
debug!(?view, "Opened view");
Ok(SlackCommandEventResponse::new(SlackMessageContent::new()))
}

View file

@ -1,22 +1,49 @@
use std::{convert::Infallible, sync::Arc};
use axum::{Extension, body::Bytes, http::Response};
use error_stack::ResultExt;
use error_stack::{Result, ResultExt};
use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody};
use slack_morphism::prelude::*;
use sqlx::SqlitePool;
use tracing::{debug, error, trace};
use crate::{
BOT_TOKEN,
BOT_TOKEN, fields,
models::{
member::{Member, TriggeredMember},
message::MessageLog,
system::System,
trigger::Type,
user,
},
};
#[derive(thiserror::Error, displaydoc::Display, Debug)]
pub enum RewriteMessageError {
/// Error while posting a message to Slack
PostMessage,
/// Error while deleting a message from Slack
DeleteMessage,
/// Error while serializing custom image blocks
SerializeImageBlocks,
/// Error while saving message log to database
MessageLog,
}
#[derive(thiserror::Error, displaydoc::Display, Debug)]
pub enum PushEventError {
/// Error while interacting with the Slack API
SlackApi,
/// Error while fetching system information from database
SystemFetch,
/// Error while fetching member information from database
MemberFetch,
/// Error while attempting to change the active member
MemberChange,
/// Error while attempting to rewrite the message
MessageRewrite,
}
#[tracing::instrument(skip(environment, event))]
pub async fn process_push_event(
Extension(environment): Extension<Arc<SlackHyperListenerEnvironment>>,
@ -44,11 +71,12 @@ pub async fn process_push_event(
}
}
#[tracing::instrument(skip(event, state, client))]
async fn push_event_callback(
event: SlackPushEventCallback,
client: Arc<SlackHyperClient>,
state: SlackClientEventsUserState,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
) -> Result<(), PushEventError> {
match event.event {
SlackEventCallbackBody::Message(message_event)
if message_event
@ -56,14 +84,14 @@ async fn push_event_callback(
.as_ref()
.is_some_and(|subtype| *subtype == SlackMessageEventType::MessageDeleted) =>
{
fields!(event_type = ?SlackMessageEventType::MessageDeleted);
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
MessageLog::delete_by_message_id(message_event.deleted_ts.unwrap().0, &user_state.db)
.await
.attach_printable("Failed to delete message log")?;
Ok(())
.change_context(PushEventError::SlackApi)
.attach_printable("Failed to delete message log")
}
SlackEventCallbackBody::Message(message_event)
if message_event.subtype.is_none()
@ -72,83 +100,117 @@ async fn push_event_callback(
.as_ref()
.is_some_and(|subtype| *subtype == SlackMessageEventType::MessageChanged) =>
{
debug!("Received message event!");
trace!("Message: {:?}", message_event);
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
let Some(user_id) = message_event.sender.user.map(user::Id::new) else {
return Ok(());
};
let Some(mut system) = System::fetch_by_user_id(&user_state.db, &user_id).await? else {
return Ok(());
};
let Some(ref channel_id) = message_event.origin.channel else {
return Ok(());
};
let Some(content) = message_event.content else {
return Ok(());
};
if let Some(ref message_content) = content.text {
let Some(member) = system
.fetch_triggered_member(&user_state.db, message_content)
.await?
else {
return Ok(());
};
debug!("Triggered member: {:#?}", member);
if system.trigger_changes_active_member {
system
.change_active_member(Some(member.id), &user_state.db)
.await?;
}
rewrite_message(
&client,
channel_id,
message_event.origin.ts,
content,
member,
&system,
&user_state.db,
)
.await?;
return Ok(());
}
// No triggers ran, so check if there's any actively fronting member
if let Some(member_id) = system.active_member_id {
let Some(member) = Member::fetch_by_id(member_id, &user_state.db).await? else {
error!("Active member not found. This should not happen.");
return Ok(());
};
rewrite_message(
&client,
channel_id,
message_event.origin.ts,
content,
member.into(),
&system,
&user_state.db,
)
.await?;
}
Ok(())
handle_message(message_event, &client, &state).await
}
_ => Ok(()),
}
}
#[tracing::instrument(skip_all)]
async fn handle_message(
message_event: SlackMessageEvent,
client: &SlackHyperClient,
state: &SlackClientEventsUserState,
) -> error_stack::Result<(), PushEventError> {
fields!(event_type = ?message_event.subtype);
debug!("Received message event!");
let states = state.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
let Some(user_id) = message_event.sender.user.map(user::Id::new) else {
debug!("Failed to get user ID");
return Ok(());
};
fields!(user_id = ?&user_id);
let Some(mut system) = System::fetch_by_user_id(&user_state.db, &user_id)
.await
.change_context(PushEventError::SystemFetch)?
else {
debug!("Failed to fetch system");
return Ok(());
};
fields!(system_id = %&system.id);
let Some(ref channel_id) = message_event.origin.channel else {
debug!("Failed to get channel ID");
return Ok(());
};
fields!(channel_id = %&channel_id);
let Some(content) = message_event.content else {
debug!("Failed to get message content");
return Ok(());
};
if let Some(ref message_content) = content.text {
let Some(member) = system
.fetch_triggered_member(&user_state.db, message_content)
.await
.change_context(PushEventError::MemberFetch)?
else {
return Ok(());
};
fields!(member = ?&member);
debug!("Member triggered");
if system.trigger_changes_active_member {
system
.change_active_member(Some(member.id), &user_state.db)
.await
.change_context(PushEventError::MemberChange)?;
}
rewrite_message(
client,
channel_id,
message_event.origin.ts,
content,
member,
&system,
&user_state.db,
)
.await
.change_context(PushEventError::MessageRewrite)?;
return Ok(());
}
debug!("Member not triggered");
// No triggers ran, so check if there's any actively fronting member
if let Some(member_id) = system.active_member_id {
fields!(member = %&member_id);
let Some(member) = Member::fetch_by_id(member_id, &user_state.db)
.await
.change_context(PushEventError::MemberFetch)?
else {
error!("Active member not found. This should not happen.");
return Ok(());
};
fields!(member = ?&member);
rewrite_message(
client,
channel_id,
message_event.origin.ts,
content,
member.into(),
&system,
&user_state.db,
)
.await
.change_context(PushEventError::MemberFetch)?;
}
Ok(())
}
#[tracing::instrument(skip(client, db, system), fields(system_id = %system.id))]
async fn rewrite_message(
client: &SlackHyperClient,
channel_id: &SlackChannelId,
@ -157,8 +219,7 @@ async fn rewrite_message(
member: TriggeredMember,
system: &System,
db: &SqlitePool,
// TODO: better error handling/custom error enum
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
) -> error_stack::Result<(), RewriteMessageError> {
let token = SlackApiToken::new(system.slack_oauth_token.expose().into())
.with_token_type(SlackApiTokenType::User);
let user_session = client.open_session(&token);
@ -227,7 +288,8 @@ async fn rewrite_message(
let custom_image_blocks = custom_image_blocks
.into_iter()
.map(serde_json::to_value)
.collect::<Result<Vec<serde_json::Value>, serde_json::Error>>()?;
.collect::<std::result::Result<Vec<serde_json::Value>, serde_json::Error>>()
.change_context(RewriteMessageError::SerializeImageBlocks)?;
blocks.extend(custom_image_blocks);
@ -239,18 +301,18 @@ async fn rewrite_message(
Some(&CHAT_POST_MESSAGE_SPECIAL_LIMIT_RATE_CTL),
)
.await
.attach_printable("Error rewriting message")?;
.change_context(RewriteMessageError::PostMessage)?;
MessageLog::insert(member.id, res.ts, db)
.await
.attach_printable("Could not insert message log")?;
.change_context(RewriteMessageError::MessageLog)?;
user_session
.chat_delete(
&SlackApiChatDeleteRequest::new(channel_id.clone(), message_id).with_as_user(true),
)
.await
.attach_printable("Error deleting message")?;
.change_context(RewriteMessageError::DeleteMessage)?;
Ok(())
}
@ -259,12 +321,17 @@ fn rewrite_content(content: &mut SlackMessageContent, member: &TriggeredMember)
debug!("Rewriting message content");
if let Some(text) = &mut content.text {
if member.is_prefix {
if let Some(new_text) = text.strip_prefix(&member.trigger_text) {
*text = new_text.to_string();
match member.typ {
Type::Prefix => {
if let Some(new_text) = text.strip_prefix(&member.trigger_text) {
*text = new_text.to_string();
}
}
Type::Suffix => {
if let Some(new_text) = text.strip_suffix(&member.trigger_text) {
*text = new_text.to_string();
}
}
} else if let Some(new_text) = text.strip_suffix(&member.trigger_text) {
*text = new_text.to_string();
}
}
@ -273,11 +340,11 @@ fn rewrite_content(content: &mut SlackMessageContent, member: &TriggeredMember)
if let SlackBlock::RichText(richtext) = block {
let elements = richtext["elements"].as_array_mut().unwrap();
let len = elements.len();
// the first and last elements would have the prefix and suffix respectively, so we can filter them
// The first and last elements would have the prefix and suffix respectively, so we can filter them
let first = elements.get_mut(0).unwrap();
if let Some(first_text) = first.pointer_mut("/elements/0/text") {
if member.is_prefix {
if member.typ == Type::Prefix {
if let Some(new_text) = first_text
.as_str()
.and_then(|text| text.strip_prefix(&member.trigger_text))
@ -291,7 +358,7 @@ fn rewrite_content(content: &mut SlackMessageContent, member: &TriggeredMember)
let last = elements.get_mut(len - 1).unwrap();
if let Some(last_text) = last.pointer_mut("/elements/0/text") {
if !member.is_prefix {
if member.typ == Type::Suffix {
if let Some(new_text) = last_text
.as_str()
.and_then(|text| text.strip_suffix(&member.trigger_text))

View file

@ -1,14 +1,14 @@
use error_stack::{bail, Result, ResultExt};
use error_stack::{Result, ResultExt, bail};
use slack_morphism::prelude::*;
use tracing::trace;
use crate::{
BOT_TOKEN, fields,
models::{
member,
Trusted, member,
system::System,
user::{self, State},
Trusted,
},
BOT_TOKEN,
};
#[derive(thiserror::Error, displaydoc::Display, Debug)]
@ -23,12 +23,14 @@ pub enum Error {
NoSystem,
}
#[tracing::instrument(skip(view_state, client, user_state), fields(system_id))]
pub async fn create_member(
view_state: SlackViewState,
client: &SlackHyperClient,
user_state: &State,
user_id: user::Id<Trusted>,
) -> Result<(), Error> {
trace!("Creating member");
let data = member::View::try_from(view_state).change_context(Error::ParsingView)?;
let Some(system_id) = System::fetch_by_user_id(&user_state.db, &user_id)
@ -40,6 +42,8 @@ pub async fn create_member(
bail!(Error::NoSystem);
};
fields!(system_id = %system_id);
let id = data
.add(system_id, &user_state.db)
.await
@ -69,6 +73,7 @@ pub async fn create_member(
Ok(())
}
#[tracing::instrument(skip(view_state, client, user_state))]
pub async fn edit_member(
view_state: SlackViewState,
client: &SlackHyperClient,
@ -76,6 +81,7 @@ pub async fn edit_member(
user_id: user::Id<Trusted>,
member_id: member::Id<Trusted>,
) -> Result<(), Error> {
trace!("Editing member");
let data = member::View::try_from(view_state).change_context(Error::ParsingView)?;
data.update(member_id, &user_state.db)

View file

@ -9,6 +9,7 @@ use slack_morphism::prelude::*;
use tracing::{debug, error};
use trigger::{create_trigger, edit_trigger};
use crate::fields;
use crate::models::system::System;
use crate::models::{self, Trusted, user};
@ -20,11 +21,12 @@ pub async fn process_interaction_event(
let client = environment.client.clone();
let states = environment.user_state.clone();
if let Err(err) = interaction_event(client, event, states).await {
error!("Error processing interaction event: {:#?}", err);
if let Err(error) = interaction_event(client, event, states).await {
error!(?error, "Error processing interaction event");
}
}
#[tracing::instrument(skip(client, event, states))]
async fn interaction_event(
client: Arc<SlackHyperClient>,
event: SlackInteractionEvent,
@ -34,17 +36,19 @@ async fn interaction_event(
SlackInteractionEvent::ViewSubmission(slack_interaction_view_submission_event) => {
match slack_interaction_view_submission_event.view.view {
SlackView::Home(view) => {
debug!("Received home view: {:#?}", view);
debug!(?view, "Received home view");
Ok(())
}
SlackView::Modal(ref view) => {
debug!("Received modal view: {:#?}", view);
debug!(?view, "Received modal view");
let user_id: user::Id<Trusted> =
slack_interaction_view_submission_event.user.id.into();
let states = states.read().await;
let user_state = states.get_user_state::<user::State>().unwrap();
fields!(user_id = %&user_id);
let Some(view_state) = slack_interaction_view_submission_event
.view
.state_params
@ -104,8 +108,8 @@ async fn handle_modal_view(
.map(models::member::Id::new)
else {
error!(
"Failed to parse member id from external id {}. Bailing in case this was a malicious call",
id
id,
"Failed to parse member id from external id. Bailing in case this was a malicious call",
);
return Ok(());
};
@ -113,8 +117,8 @@ async fn handle_modal_view(
let Ok(trusted_member_id) = member_id.validate_by_user(&user_id, &user_state.db).await
else {
error!(
"Failed to validate member id from external id {}. Bailing in case this was a malicious call",
id
id,
"Failed to validate member id from external id. Bailing in case this was a malicious call",
);
return Ok(());
};
@ -136,8 +140,8 @@ async fn handle_modal_view(
let Ok(trusted_member_id) = member_id.validate_by_user(&user_id, &user_state.db).await
else {
error!(
"Failed to validate member id from external id {}. Bailing in case this was a malicious call",
id
id,
"Failed to validate member id from external id. Bailing in case this was a malicious call",
);
return Ok(());
};
@ -161,8 +165,8 @@ async fn handle_modal_view(
.flatten()
else {
error!(
"Failed to fetch system id for user id {}. Bailing in case this was a malicious call",
user_id
%user_id,
"Failed to fetch system id for user id. Bailing in case this was a malicious call"
);
return Ok(());
};

View file

@ -1,8 +1,9 @@
use error_stack::{Result, ResultExt, bail};
use slack_morphism::prelude::*;
use tracing::debug;
use crate::{
BOT_TOKEN,
BOT_TOKEN, fields,
models::{
Trusted, member,
system::System,
@ -21,6 +22,7 @@ pub enum Error {
NoSystem,
}
#[tracing::instrument(skip(view_state, client, user_state), fields(system_id))]
pub async fn create_trigger(
view_state: SlackViewState,
client: &SlackHyperClient,
@ -39,11 +41,16 @@ pub async fn create_trigger(
bail!(Error::NoSystem);
};
let _id = data
fields!(system_id = %system_id);
let id = data
.add(system_id, member_id, &user_state.db)
.await
.change_context(Error::Sqlx)?;
fields!(id = %id);
debug!("Trigger created");
let session = client.open_session(&BOT_TOKEN);
let user: SlackUserId = user_id.into();
@ -65,6 +72,7 @@ pub async fn create_trigger(
Ok(())
}
#[tracing::instrument(skip(view_state, client, user_state))]
pub async fn edit_trigger(
view_state: SlackViewState,
client: &SlackHyperClient,

View file

@ -7,24 +7,26 @@ mod events;
mod interactions;
mod models;
mod oauth;
mod util;
use crate::models::Trusted;
use std::str::FromStr;
use std::sync::LazyLock;
use std::{process::ExitCode, sync::Arc};
use axum::{extract::MatchedPath, http::Request};
use commands::process_command_event;
use error_stack::{report, ResultExt};
use error_stack::{ResultExt, report};
use events::process_push_event;
use interactions::process_interaction_event;
use models::{system, user};
use oauth::oauth_handler;
use slack_morphism::prelude::*;
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::SqlitePool;
use tracing::debug;
use tracing::{info, level_filters::LevelFilter};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
use sqlx::{SqlitePool, sqlite::SqliteConnectOptions};
use tower_http::trace::TraceLayer;
use tracing::info_span;
use tracing::{debug, info, level_filters::LevelFilter};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
/// The slack app token. Used for socket mode if we ever decide to use it.
pub static APP_TOKEN: LazyLock<SlackApiToken> =
@ -150,6 +152,22 @@ async fn main() -> error_stack::Result<ExitCode, Error> {
.events_layer(&signing_secret)
.with_event_extractor(SlackEventsExtractors::interaction_event()),
),
)
.layer(
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
// Log the matched route's path (with placeholders not filled in).
// Use request.uri() or OriginalUri if you want the real path.
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
info_span!(
"slack_system_bot::http_request",
method = ?request.method(),
matched_path,
)
}),
);
info!("Slack bot is running");

View file

@ -7,7 +7,7 @@ use crate::id;
use super::{
Trusted, Untrusted, system,
trigger::{self, Trigger},
trigger::{self, Trigger, Type},
user,
};
@ -100,8 +100,9 @@ impl Id<Trusted> {
}
}
// TODO: move sql to rust struct
// TO-DO: move SQL to rust struct
#[derive(FromRow, Debug)]
#[allow(dead_code)]
pub struct Member {
/// The ID of the member
pub id: Id<Trusted>,
@ -143,7 +144,7 @@ impl Member {
WHERE system_id = $1 AND id = $2
"#,
system_id,
// safe because this query also checks if the ID is trusted
// Safe because this query also checks if the ID is trusted
member_id.id
)
.fetch_optional(db)
@ -179,7 +180,7 @@ impl Member {
}
}
/// all information required to display a member
/// All information required to display a member
#[derive(FromRow, Debug)]
pub struct TriggeredMember {
/// The ID of the member
@ -190,8 +191,8 @@ pub struct TriggeredMember {
pub profile_picture_url: Option<String>,
/// The trigger text that was matched
pub trigger_text: String,
/// Whether this was a prefix trigger (true) or suffix trigger (false)
pub is_prefix: bool,
/// The type of trigger
pub typ: Type,
}
impl From<Member> for TriggeredMember {
@ -201,7 +202,7 @@ impl From<Member> for TriggeredMember {
display_name: value.display_name,
profile_picture_url: value.profile_picture_url,
trigger_text: String::new(),
is_prefix: true,
typ: Type::Prefix,
}
}
}

View file

@ -14,22 +14,8 @@ id!(
=> Message
);
impl Id<Trusted> {
pub async fn delete(self, db_pool: &SqlitePool) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
DELETE FROM message_logs
WHERE id = $1
"#,
self.id
)
.execute(db_pool)
.await
.map(|_| ())
}
}
#[derive(FromRow, Debug)]
#[allow(dead_code)]
pub struct MessageLog {
pub id: Id<Trusted>,
pub member_id: member::Id<Trusted>,

View file

@ -1,10 +1,12 @@
use std::fmt::Debug;
pub mod member;
pub mod message;
pub mod system;
pub mod trigger;
pub mod user;
pub trait Trustability: Send + Sync {}
pub trait Trustability: Send + Sync + Debug {}
/// A trusted/valid ID
#[derive(Debug, Clone, Copy)]

View file

@ -25,6 +25,18 @@ impl Id<Trusted> {
pub async fn list_triggers(self, db: &SqlitePool) -> Result<Vec<Trigger>, sqlx::Error> {
Trigger::fetch_by_system_id(db, self).await
}
pub async fn rename(self, new_name: &str, db: &SqlitePool) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE systems SET name = ? WHERE id = ?",
new_name,
self.id
)
.execute(db)
.await?;
Ok(())
}
}
#[derive(Debug, FromRow, PartialEq, Eq, Clone)]
@ -44,6 +56,7 @@ impl From<String> for SlackOauthToken {
}
#[derive(FromRow, Debug)]
#[allow(dead_code)]
pub struct System {
#[sqlx(flatten)]
pub id: Id<Trusted>,
@ -65,6 +78,7 @@ pub enum ChangeActiveMemberError {
}
impl System {
#[tracing::instrument(skip(db))]
pub async fn fetch_by_user_id<T>(
db: &SqlitePool,
user_id: &user::Id<T>,
@ -175,14 +189,15 @@ impl System {
display_name,
profile_picture_url,
triggers.text as trigger_text,
triggers.is_prefix
triggers.typ
FROM
members
JOIN
triggers ON members.id = triggers.member_id
WHERE
(triggers.is_prefix = TRUE AND ?1 LIKE triggers.text || '%') OR
(triggers.is_prefix = FALSE AND ?1 LIKE '%' || triggers.text)
-- See trigger.rs file for all types and names
(triggers.typ = 0 AND ?1 LIKE triggers.text || '%') OR
(triggers.typ = 1 AND ?1 LIKE '%' || triggers.text)
"#,
message
)

View file

@ -1,3 +1,5 @@
use std::str::FromStr;
use crate::id;
use super::{Trustability, Trusted, Untrusted, member, system};
@ -69,13 +71,51 @@ pub enum Error {
Sqlx,
}
#[derive(Debug, sqlx::Type, displaydoc::Display, PartialEq, Eq)]
#[repr(i64)]
pub enum Type {
/// Suffix
Suffix = 0,
/// Prefix
Prefix = 1,
}
impl From<i64> for Type {
fn from(value: i64) -> Self {
match value {
0 => Self::Suffix,
1 => Self::Prefix,
_ => unreachable!(
"Invalid type value. This means the database and rust struct are out of sync"
),
}
}
}
#[derive(Debug, displaydoc::Display)]
/// Unknown type
pub struct UnknownType(String);
impl FromStr for Type {
type Err = UnknownType;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"suffix" => Ok(Self::Suffix),
"prefix" => Ok(Self::Prefix),
_ => Err(UnknownType(s.to_string())),
}
}
}
#[derive(FromRow, Debug)]
#[allow(dead_code)]
pub struct Trigger {
pub id: Id<Trusted>,
pub member_id: member::Id<Trusted>,
pub system_id: system::Id<Trusted>,
pub text: String,
pub is_prefix: bool,
pub typ: Type,
}
impl Trigger {
@ -91,7 +131,7 @@ impl Trigger {
member_id as "member_id: member::Id<Trusted>",
system_id as "system_id: system::Id<Trusted>",
text,
is_prefix
typ
FROM
triggers
WHERE id = $1
@ -114,7 +154,7 @@ impl Trigger {
member_id as "member_id: member::Id<Trusted>",
system_id as "system_id: system::Id<Trusted>",
text,
is_prefix
typ
FROM
triggers
WHERE
@ -138,7 +178,7 @@ impl Trigger {
member_id as "member_id: member::Id<Trusted>",
system_id as "system_id: system::Id<Trusted>",
text,
is_prefix
typ
FROM
triggers
WHERE member_id = $1
@ -154,24 +194,28 @@ impl Trigger {
#[derive(Debug)]
pub struct View {
pub text: String,
pub is_prefix: bool,
pub typ: Type,
}
impl Default for View {
fn default() -> Self {
Self {
text: String::new(),
is_prefix: true,
typ: Type::Prefix,
}
}
}
impl View {
pub fn create_blocks(self) -> Vec<SlackBlock> {
let prefix_choice =
SlackBlockChoiceItem::new(SlackBlockText::Plain("prefix".into()), "prefix".into());
let suffix_choice =
SlackBlockChoiceItem::new(SlackBlockText::Plain("suffix".into()), "suffix".into());
let prefix_choice = SlackBlockChoiceItem::new(
SlackBlockText::Plain(Type::Prefix.to_string().into()),
Type::Prefix.to_string(),
);
let suffix_choice = SlackBlockChoiceItem::new(
SlackBlockText::Plain(Type::Suffix.to_string().into()),
Type::Suffix.to_string(),
);
slack_blocks!(
some_into(
@ -191,14 +235,13 @@ impl View {
SlackInputBlock::new(
"Trigger Type".into(),
SlackBlockRadioButtonsElement::new(
"is_prefix".into(),
vec![prefix_choice.clone(), suffix_choice.clone()]
"type".into(),
vec![prefix_choice, suffix_choice]
)
.with_initial_option(if self.is_prefix {
prefix_choice
} else {
suffix_choice
})
.with_initial_option(SlackBlockChoiceItem::new(
SlackBlockText::Plain(Type::Prefix.to_string().into()),
Type::Prefix.to_string(),
))
.into(),
)
.with_optional(false)
@ -222,14 +265,14 @@ impl View {
sqlx::query!(
r#"
INSERT INTO triggers (system_id, member_id, text, is_prefix)
INSERT INTO triggers (system_id, member_id, text, typ)
VALUES ($1, $2, $3, $4)
RETURNING id
"#,
system_id.id,
member_id.id,
self.text,
self.is_prefix
self.typ
)
.fetch_one(db_pool)
.await
@ -250,11 +293,11 @@ impl View {
sqlx::query!(
r#"
UPDATE triggers
SET text = $1, is_prefix = $2
SET text = $1, typ = $2
WHERE id = $3
"#,
self.text,
self.is_prefix,
self.typ,
trigger_id.id,
)
.execute(db)
@ -264,13 +307,6 @@ impl View {
.map(|_| ())
}
pub const fn new(trigger_text: String, is_prefix: bool) -> Self {
Self {
text: trigger_text,
is_prefix,
}
}
pub fn create_add_view(self, member_id: member::Id<Trusted>) -> SlackView {
SlackView::Modal(
SlackModalView::new("Add a new trigger".into(), self.create_blocks())
@ -299,9 +335,12 @@ impl From<SlackViewState> for View {
view.text = text;
}
}
"is_prefix" => {
"typ" => {
if let Some(option) = content.selected_option {
view.is_prefix = option.value == "prefix";
match option.value.parse::<Type>() {
Ok(typ) => view.typ = typ,
Err(error) => warn!(?error, "Error parsing trigger type"),
}
}
}
other => {
@ -319,7 +358,7 @@ impl From<Trigger> for View {
fn from(trigger: Trigger) -> Self {
Self {
text: trigger.text,
is_prefix: trigger.is_prefix,
typ: trigger.typ,
}
}
}

View file

@ -79,6 +79,7 @@ where
}
}
#[tracing::instrument(skip_all, ret)]
pub async fn oauth_handler(
Query(code): Query<OauthCode>,
State(state): State<user::State>,

51
src/util.rs Normal file
View file

@ -0,0 +1,51 @@
/// Records one or more fields in the current span.
///
/// Use % for recording a field with a [`Display`] value.
///
/// Use ? for recording a field with a [`Debug`] value.
///
/// # Examples
///
/// ```rs
/// fields!(user_name = user.name, system_id = %system.id, member_id = ?member.id);
/// ```
#[macro_export]
macro_rules! fields {
// recursive cases
($name:tt = %$value:expr, $($rest:tt)?) => {
::tracing::span::Span::current()
.record(::std::stringify!($name), ::tracing::field::display($value));
fields!($($rest)+);
};
($name:tt = ?$value:expr, $($rest:tt)?) => {
::tracing::span::Span::current()
.record(::std::stringify!($name), ::tracing::field::debug($value));
fields!($($rest)+);
};
($name:tt = $value:expr, $($rest:tt)?) => {
::tracing::span::Span::current()
.record(::std::stringify!($name), $value);
fields!($($rest)+);
};
// base cases
($name:tt = %$value:expr) => {
::tracing::span::Span::current()
.record(::std::stringify!($name), ::tracing::field::display($value));
};
($name:tt = ?$value:expr) => {
::tracing::span::Span::current()
.record(::std::stringify!($name), ::tracing::field::debug($value));
};
($name:tt = $value:expr) => {
::tracing::span::Span::current()
.record(::std::stringify!($name), $value);
};
}