use serde::Deserialize;
use crate::domain::channel_events::{
ChannelIdentity, ConversationId, IncomingEvent, InteractionEvent, Platform, TextMessage,
};
#[derive(Debug, Deserialize)]
pub struct SlackEventCallback {
pub token: String,
pub team_id: String,
pub api_app_id: String,
pub event: SlackEvent,
#[serde(rename = "type")]
pub payload_type: String,
pub event_id: Option<String>,
pub event_time: Option<i64>,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type")]
pub enum SlackEvent {
#[serde(rename = "message")]
Message(SlackMessageEvent),
}
#[derive(Debug, Deserialize)]
pub struct SlackMessageEvent {
#[serde(rename = "type")]
pub event_type: String,
pub channel: String,
pub user: Option<String>,
pub text: Option<String>,
pub ts: Option<String>,
pub thread_ts: Option<String>,
pub subtype: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SlackInteractionPayload {
pub type_field: Option<String>,
#[serde(rename = "type")]
pub payload_type: String,
pub channel: Option<SlackInteractionChannel>,
pub user: Option<SlackInteractionUser>,
pub actions: Option<Vec<SlackAction>>,
pub message: Option<SlackInteractionMessage>,
pub response_url: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SlackInteractionChannel {
pub id: String,
}
#[derive(Debug, Deserialize)]
pub struct SlackInteractionUser {
pub id: String,
}
#[derive(Debug, Deserialize)]
pub struct SlackInteractionMessage {
pub ts: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SlackAction {
#[serde(rename = "type")]
pub action_type: String,
pub action_id: Option<String>,
pub value: Option<String>,
}
pub fn normalize_event(callback: &SlackEventCallback) -> Option<IncomingEvent> {
let SlackEvent::Message(msg) = &callback.event;
if msg.subtype.is_some() {
return None;
}
let text = msg.text.as_deref()?.to_string();
let channel_id = msg.channel.clone();
let user_id = msg.user.as_deref()?.to_string();
let _ts = msg.ts.as_deref()?.to_string();
let channel = ChannelIdentity::new(
Platform::Slack,
channel_id.clone(),
user_id.clone(),
msg.thread_ts.clone(),
None,
);
let conversation_id = ConversationId::from_platform(Platform::Slack, &channel_id);
let reply_to_id = msg.thread_ts.clone();
Some(IncomingEvent::TextMessage(TextMessage {
conversation_id,
channel,
text,
reply_to_id,
}))
}
pub fn normalize_interaction(payload: &SlackInteractionPayload) -> Option<IncomingEvent> {
let actions = payload.actions.as_ref()?;
let first_action = actions.first()?;
let raw_action_id = first_action.action_id.as_deref()?;
let slack_channel = payload.channel.as_ref()?;
let slack_user = payload.user.as_ref()?;
let slack_message = payload.message.as_ref()?;
let ts = slack_message.ts.as_deref()?;
let channel = ChannelIdentity::new(
Platform::Slack,
slack_channel.id.clone(),
slack_user.id.clone(),
None,
None,
);
let conversation_id = ConversationId::from_platform(Platform::Slack, &slack_channel.id);
let (action_id, message_ref) = match raw_action_id.split_once(':') {
Some((action, payload)) => (action.to_string(), payload.to_string()),
None => (raw_action_id.to_string(), String::new()),
};
let callback_message_id = Some(format!("{}:{}", slack_channel.id, ts));
Some(IncomingEvent::Interaction(InteractionEvent {
conversation_id,
channel,
action_id,
message_ref,
callback_message_id,
callback_query_id: None,
original_text: None,
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_message_event(channel: &str, user: &str, text: &str, ts: &str) -> SlackEventCallback {
SlackEventCallback {
token: "test_token".to_string(),
team_id: "T123".to_string(),
api_app_id: "A123".to_string(),
event: SlackEvent::Message(SlackMessageEvent {
event_type: "message".to_string(),
channel: channel.to_string(),
user: Some(user.to_string()),
text: Some(text.to_string()),
ts: Some(ts.to_string()),
thread_ts: None,
subtype: None,
}),
payload_type: "event_callback".to_string(),
event_id: Some("Ev123".to_string()),
event_time: Some(1234567890),
}
}
#[test]
fn normalize_event_basic_message() {
let callback = make_message_event("C123", "U456", "hello world", "1234567890.123456");
let event = normalize_event(&callback).expect("should normalize");
match event {
IncomingEvent::TextMessage(msg) => {
assert_eq!(msg.text, "hello world");
assert_eq!(msg.channel.channel_id, "C123");
assert_eq!(msg.channel.user_id, "U456");
assert_eq!(msg.channel.platform, Platform::Slack);
assert!(msg.conversation_id.0.starts_with("slack-C123"));
}
_ => panic!("expected TextMessage"),
}
}
#[test]
fn normalize_event_with_thread() {
let mut callback = make_message_event("C123", "U456", "reply", "1234567890.123456");
let SlackEvent::Message(ref mut msg) = callback.event;
msg.thread_ts = Some("1234567890.000000".to_string());
let event = normalize_event(&callback).expect("should normalize");
match event {
IncomingEvent::TextMessage(msg) => {
assert_eq!(msg.reply_to_id, Some("1234567890.000000".to_string()));
assert_eq!(msg.channel.thread_id, Some("1234567890.000000".to_string()));
}
_ => panic!("expected TextMessage"),
}
}
#[test]
fn normalize_event_ignores_bot_messages() {
let mut callback = make_message_event("C123", "U456", "bot says hi", "1234567890.123456");
let SlackEvent::Message(ref mut msg) = callback.event;
msg.subtype = Some("bot_message".to_string());
assert!(normalize_event(&callback).is_none());
}
#[test]
fn normalize_event_ignores_no_text() {
let mut callback = make_message_event("C123", "U456", "has text", "1234567890.123456");
let SlackEvent::Message(ref mut msg) = callback.event;
msg.text = None;
assert!(normalize_event(&callback).is_none());
}
#[test]
fn normalize_event_ignores_no_user() {
let mut callback = make_message_event("C123", "U456", "text", "1234567890.123456");
let SlackEvent::Message(ref mut msg) = callback.event;
msg.user = None;
assert!(normalize_event(&callback).is_none());
}
#[test]
fn normalize_interaction_basic() {
let payload = SlackInteractionPayload {
type_field: None,
payload_type: "block_actions".to_string(),
channel: Some(SlackInteractionChannel {
id: "C789".to_string(),
}),
user: Some(SlackInteractionUser {
id: "U012".to_string(),
}),
actions: Some(vec![SlackAction {
action_type: "button".to_string(),
action_id: Some("approve_btn".to_string()),
value: Some("yes".to_string()),
}]),
message: Some(SlackInteractionMessage {
ts: Some("1234567890.654321".to_string()),
}),
response_url: None,
};
let event = normalize_interaction(&payload).expect("should normalize");
match event {
IncomingEvent::Interaction(interaction) => {
assert_eq!(interaction.action_id, "approve_btn");
assert_eq!(interaction.message_ref, "");
assert_eq!(
interaction.callback_message_id,
Some("C789:1234567890.654321".to_string())
);
assert_eq!(interaction.channel.channel_id, "C789");
assert_eq!(interaction.channel.user_id, "U012");
}
_ => panic!("expected Interaction"),
}
}
#[test]
fn normalize_interaction_with_colon_action() {
let payload = SlackInteractionPayload {
type_field: None,
payload_type: "block_actions".to_string(),
channel: Some(SlackInteractionChannel {
id: "C789".to_string(),
}),
user: Some(SlackInteractionUser {
id: "U012".to_string(),
}),
actions: Some(vec![SlackAction {
action_type: "button".to_string(),
action_id: Some("choice:1. Use Redis".to_string()),
value: None,
}]),
message: Some(SlackInteractionMessage {
ts: Some("1234567890.654321".to_string()),
}),
response_url: None,
};
let event = normalize_interaction(&payload).expect("should normalize");
match event {
IncomingEvent::Interaction(interaction) => {
assert_eq!(interaction.action_id, "choice");
assert_eq!(interaction.message_ref, "1. Use Redis");
assert_eq!(
interaction.callback_message_id,
Some("C789:1234567890.654321".to_string())
);
}
_ => panic!("expected Interaction"),
}
}
#[test]
fn normalize_interaction_no_actions() {
let payload = SlackInteractionPayload {
type_field: None,
payload_type: "block_actions".to_string(),
channel: Some(SlackInteractionChannel {
id: "C789".to_string(),
}),
user: Some(SlackInteractionUser {
id: "U012".to_string(),
}),
actions: None,
message: Some(SlackInteractionMessage {
ts: Some("1234567890.654321".to_string()),
}),
response_url: None,
};
assert!(normalize_interaction(&payload).is_none());
}
#[test]
fn normalize_interaction_no_channel() {
let payload = SlackInteractionPayload {
type_field: None,
payload_type: "block_actions".to_string(),
channel: None,
user: Some(SlackInteractionUser {
id: "U012".to_string(),
}),
actions: Some(vec![SlackAction {
action_type: "button".to_string(),
action_id: Some("btn".to_string()),
value: None,
}]),
message: Some(SlackInteractionMessage {
ts: Some("1234567890.654321".to_string()),
}),
response_url: None,
};
assert!(normalize_interaction(&payload).is_none());
}
}