use eyre::Context;
use hanabot::{Hanabi, MessageProxy};
use slack_morphism::prelude::*;
use slack_morphism::{SlackApiToken, SlackApiTokenValue};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let app_token_value: SlackApiTokenValue = std::env::var("SLACK_APP_TOKEN")
.expect("SLACK_APP_TOKEN was not set")
.into();
let app_token: SlackApiToken = SlackApiToken::new(app_token_value);
let api_token_value: SlackApiTokenValue = std::env::var("SLACK_API_TOKEN")
.expect("SLACK_API_TOKEN was not set")
.into();
let api_token: SlackApiToken = SlackApiToken::new(api_token_value);
let hanabi = Hanabi::resume()
.await
.context("resume from saved game states")?
.unwrap_or_default();
let state = Arc::new(State {
api_token,
hanabi: Mutex::new(hanabi),
});
let socket_mode_callbacks =
SlackSocketModeListenerCallbacks::new().with_push_events(on_push_event);
let client = Arc::new(SlackClient::new(SlackClientHyperConnector::new()?));
let listener_environment = Arc::new(
SlackClientEventsListenerEnvironment::new(client.clone())
.with_error_handler(on_error)
.with_user_state(Arc::clone(&state)),
);
let socket_mode_listener = SlackClientSocketModeListener::new(
&SlackClientSocketModeConfig::new(),
listener_environment.clone(),
socket_mode_callbacks,
);
socket_mode_listener
.listen_for(&app_token)
.await
.context("listen in socket mode")?;
socket_mode_listener.serve().await;
{
let hanabi = state.hanabi.lock().await;
hanabi.save().await
}
}
struct State {
api_token: SlackApiToken,
hanabi: Mutex<Hanabi>,
}
fn on_error(
err: Box<dyn std::error::Error + Send + Sync>,
_client: Arc<SlackHyperClient>,
_states: SlackClientEventsUserState,
) -> http::StatusCode {
eprintln!("{err:?}");
http::StatusCode::OK
}
async fn on_push_event(
event: SlackPushEventCallback,
client: Arc<SlackHyperClient>,
states: SlackClientEventsUserState,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let SlackEventCallbackBody::Message(m) = event.event else {
return Ok(());
};
if !m
.subtype
.is_none_or(|ct| ct == SlackMessageEventType::MeMessage)
{
return Ok(());
}
let Some(user) = m.sender.user else {
return Ok(());
};
let Some(text) = m.content.as_ref().and_then(|v| v.text.as_ref()) else {
return Ok(());
};
let Some(_channel) = m.origin.channel else {
return Ok(());
};
if m.origin.channel_type.is_none_or(|ct| ct.0 != "im") {
return Ok(());
}
let states = states.read().await;
let state = states
.get_user_state::<Arc<State>>()
.expect("we always use hanabi as user state");
let mut hanabi = state.hanabi.lock().await;
let cli = client.open_session(&state.api_token);
let mut messages = ApiMessageProxy::new(cli);
hanabi
.on_dm_recv(text, user, &mut messages)
.await
.context("handle dm message")?;
messages.flush().await.context("flush user messages")?;
Ok(())
}
pub struct ApiMessageProxy<'a> {
cli: SlackClientSession<'a, SlackClientHyperHttpsConnector>,
msgs: HashMap<String, Vec<String>>,
}
impl<'a> ApiMessageProxy<'a> {
pub fn new(cli: SlackClientSession<'a, SlackClientHyperHttpsConnector>) -> Self {
Self {
cli,
msgs: Default::default(),
}
}
async fn flush(&mut self) -> eyre::Result<()> {
for (user, msgs) in self.msgs.drain() {
let _ = self
.cli
.chat_post_message(
&SlackApiChatPostMessageRequest::new(
SlackChannelId(user.clone()),
SlackMessageContent::new().with_text(msgs.join("\n")),
)
.without_unfurl_links(),
)
.await
.with_context(|| format!("send to {user}"))?;
}
Ok(())
}
}
impl<'a> MessageProxy for ApiMessageProxy<'a> {
fn send(&mut self, user: &str, text: &str) {
self.msgs
.entry(user.to_string())
.or_default()
.push(text.to_owned());
}
}