use slack_morphism::prelude::*;
use std::sync::Arc;
pub(crate) fn glyph_for_slack_name(name: &str) -> &'static str {
match name {
"+1" | "thumbsup" => "👍",
"-1" | "thumbsdown" => "👎",
"ok_hand" => "👌",
"muscle" => "💪",
"saluting_face" => "🫡",
"fire" => "🔥",
"100" => "💯",
"no_entry" => "⛔",
"no_entry_sign" => "🚫",
"octagonal_sign" => "🛑",
"x" => "❌",
_ => "❔",
}
}
pub(crate) fn slack_name_for_glyph(emoji: &str) -> &'static str {
match emoji.trim().trim_end_matches('\u{fe0f}') {
"👍" => "thumbsup",
"👎" => "thumbsdown",
"👌" => "ok_hand",
"👀" => "eyes",
"🔥" => "fire",
"🎉" => "tada",
"✅" => "white_check_mark",
"❌" => "x",
"❤" | "❤️" => "heart",
"😂" | "🤣" => "joy",
"💯" => "100",
"🙏" => "pray",
"👏" => "clap",
"🚀" => "rocket",
"🤔" => "thinking_face",
"🏆" => "trophy",
"⚡" => "zap",
"🤝" => "handshake",
"💪" => "muscle",
_ => "thumbsup",
}
}
pub(crate) async fn add_reaction(
client: &Arc<SlackHyperClient>,
token: &SlackApiToken,
channel: SlackChannelId,
ts: SlackTs,
name: &str,
) {
let session = client.open_session(token);
let req = SlackApiReactionsAddRequest {
channel,
name: SlackReactionName::new(name.to_string()),
timestamp: ts.clone(),
};
if let Err(e) = session.reactions_add(&req).await {
tracing::warn!("Slack: reactions_add failed (name={name}, ts={ts}): {e}");
}
}
pub(crate) async fn handle_reaction_added(
ev: SlackReactionAddedEvent,
client: Arc<SlackHyperClient>,
) {
let state = match super::handler::handler_state() {
Some(s) => s,
None => {
tracing::error!("Slack: handler state not initialized for reaction event");
return;
}
};
let bot_user = match state.bot_user_id.as_deref() {
Some(b) => b,
None => return, };
if ev.user.0 == bot_user {
return;
}
if ev.item_user.as_ref().map(|u| u.0.as_str()) != Some(bot_user) {
tracing::debug!("Slack reaction: not on a bot message — ignoring");
return;
}
let SlackReactionsItem::Message(item) = &ev.item else {
return;
};
let Some(channel) = item.origin.channel.clone() else {
return;
};
let reacted_ts = item.origin.ts.clone();
let name = ev.reaction.to_string();
let user_id = ev.user.0.to_string();
let is_dm = channel.0.starts_with('D');
let token = SlackApiToken::new(SlackApiTokenValue::from(state.current_bot_token()));
let user_name = {
let session = client.open_session(&token);
match session
.users_info(&SlackApiUsersInfoRequest::new(SlackUserId::new(
user_id.clone(),
)))
.await
{
Ok(resp) => resp
.user
.profile
.as_ref()
.and_then(|p| p.display_name.clone().or_else(|| p.real_name.clone()))
.filter(|n| !n.is_empty())
.unwrap_or_else(|| user_id.clone()),
Err(_) => user_id.clone(),
}
};
let session_id = {
use crate::channels::session_resolve;
let (id_str, legacy_title) = if is_dm {
(
format!("slack-dm-{user_id}"),
format!("Slack: DM {user_id}"),
)
} else {
(
format!("slack-{}", channel.0),
format!("Slack: #{}", channel.0),
)
};
let suffix = session_resolve::chat_id_suffix(&id_str);
let session_title = format!("{legacy_title} {suffix}");
let idle_hours = state.config_rx.borrow().channels.slack.session_idle_hours;
match session_resolve::resolve_or_create_channel_session(
&state.session_svc,
&suffix,
&legacy_title,
&session_title,
idle_hours,
"Slack",
)
.await
{
Ok(id) => id,
Err(e) => {
tracing::error!("Slack reaction: failed to resolve session: {e}");
return;
}
}
};
let glyph = glyph_for_slack_name(&name);
let prompt = crate::channels::telegram::reaction_prompt::build_reaction_prompt(
&user_name,
glyph,
&format!(":{name}: (Slack reaction)"),
!is_dm,
);
tracing::info!(
"Slack reaction: {user_name} reacted :{name}: on bot message {reacted_ts} — session {session_id}"
);
let display = format!("[System: {user_name} reacted with :{name}:]");
let response = match state
.agent
.send_message_with_display(session_id, prompt, Some(display), None)
.await
{
Ok(r) => r,
Err(e) => {
tracing::warn!("Slack reaction: agent error for session {session_id}: {e}");
return;
}
};
let (text_only, _imgs) = crate::utils::extract_img_markers(&response.content);
let text_only = crate::utils::sanitize::strip_llm_artifacts(&text_only);
let (text_only, react_emoji) = crate::utils::extract_react_marker(&text_only);
if let Some(em) = react_emoji {
add_reaction(
&client,
&token,
channel.clone(),
reacted_ts,
slack_name_for_glyph(&em),
)
.await;
}
let trimmed = text_only.trim();
if !trimmed.is_empty() {
let body = crate::utils::slack_fmt::markdown_to_mrkdwn(trimmed);
let session = client.open_session(&token);
let req = SlackApiChatPostMessageRequest::new(
channel,
SlackMessageContent::new().with_text(body),
);
if let Err(e) = session.chat_post_message(&req).await {
tracing::warn!("Slack reaction: failed to deliver reply: {e}");
}
}
}