use std::collections::HashMap;
use std::future::Future;
use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use futures::FutureExt as _;
use autumn_web::AppState;
use futures::future::BoxFuture;
use tokio_util::task::TaskTracker;
use crate::client::SlackClient;
use crate::error::{HandlerResult, SlackError};
use crate::metrics::{SlackMetrics, Surface};
use crate::payload::{
BlockAction, CommandResponse, EventCallback, Message, Shortcut, SlashCommand, ViewPayload,
ViewResponse,
};
#[derive(Clone, Debug)]
pub struct SlackContext {
client: SlackClient,
state: AppState,
}
impl SlackContext {
pub(crate) const fn new(client: SlackClient, state: AppState) -> Self {
Self { client, state }
}
#[must_use]
pub const fn client(&self) -> &SlackClient {
&self.client
}
#[must_use]
pub const fn state(&self) -> &AppState {
&self.state
}
}
pub(crate) type Handler<T, R> =
Arc<dyn Fn(SlackContext, T) -> BoxFuture<'static, HandlerResult<R>> + Send + Sync>;
pub(crate) fn boxed<T, R, F, Fut>(f: F) -> Handler<T, R>
where
F: Fn(SlackContext, T) -> Fut + Send + Sync + 'static,
Fut: Future<Output = HandlerResult<R>> + Send + 'static,
{
Arc::new(move |ctx, payload| Box::pin(f(ctx, payload)))
}
pub(crate) enum Registration {
Event(String, Handler<EventCallback, ()>),
Command(String, Handler<SlashCommand, CommandResponse>),
Action(String, Handler<BlockAction, Option<Message>>),
ViewSubmission(String, Handler<ViewPayload, ViewResponse>),
ViewClosed(String, Handler<ViewPayload, ()>),
Shortcut(String, Handler<Shortcut, ()>),
}
impl Registration {
pub(crate) fn describe(&self) -> (&'static str, &str) {
match self {
Self::Event(k, _) => ("event", k),
Self::Command(k, _) => ("command", k),
Self::Action(k, _) => ("action", k),
Self::ViewSubmission(k, _) => ("view_submission", k),
Self::ViewClosed(k, _) => ("view_closed", k),
Self::Shortcut(k, _) => ("shortcut", k),
}
}
}
#[derive(Default)]
pub(crate) struct Registry {
pub events: HashMap<String, Handler<EventCallback, ()>>,
pub commands: HashMap<String, Handler<SlashCommand, CommandResponse>>,
pub actions: HashMap<String, Handler<BlockAction, Option<Message>>>,
pub view_submissions: HashMap<String, Handler<ViewPayload, ViewResponse>>,
pub view_closed: HashMap<String, Handler<ViewPayload, ()>>,
pub shortcuts: HashMap<String, Handler<Shortcut, ()>>,
}
impl Registry {
pub(crate) fn build(regs: Vec<Registration>) -> Result<Self, SlackError> {
let mut r = Self::default();
for reg in regs {
let (kind, key) = reg.describe();
if key.trim().is_empty() || key.chars().any(char::is_whitespace) {
return Err(SlackError::Config(format!(
"{kind} key {key:?} is empty or has a space"
)));
}
if kind == "command" && !key.starts_with('/') {
return Err(SlackError::Config(format!(
"command {key:?} must start with /"
)));
}
let dup = |kind: &str, key: &str| {
SlackError::Config(format!("{kind} {key} is registered twice"))
};
match reg {
Registration::Event(k, h) => insert(&mut r.events, k, h, |k| dup("event", k))?,
Registration::Command(k, h) => {
insert(&mut r.commands, k, h, |k| dup("command", k))?;
}
Registration::Action(k, h) => insert(&mut r.actions, k, h, |k| dup("action", k))?,
Registration::ViewSubmission(k, h) => {
insert(&mut r.view_submissions, k, h, |k| dup("view_submission", k))?;
}
Registration::ViewClosed(k, h) => {
insert(&mut r.view_closed, k, h, |k| dup("view_closed", k))?;
}
Registration::Shortcut(k, h) => {
insert(&mut r.shortcuts, k, h, |k| dup("shortcut", k))?;
}
}
}
Ok(r)
}
pub(crate) fn len(&self) -> usize {
self.events.len()
+ self.commands.len()
+ self.actions.len()
+ self.view_submissions.len()
+ self.view_closed.len()
+ self.shortcuts.len()
}
}
fn insert<V>(
map: &mut HashMap<String, V>,
key: String,
value: V,
dup: impl Fn(&str) -> SlackError,
) -> Result<(), SlackError> {
if map.contains_key(&key) {
return Err(dup(&key));
}
map.insert(key, value);
Ok(())
}
pub(crate) enum RunOutcome<R> {
Ok(R),
Failed,
Panic,
}
impl<R> RunOutcome<R> {
pub(crate) fn into_ok(self) -> Option<R> {
match self {
Self::Ok(r) => Some(r),
Self::Failed | Self::Panic => None,
}
}
}
#[derive(Clone)]
pub(crate) struct Runner {
pub tracker: TaskTracker,
pub metrics: Arc<SlackMetrics>,
}
impl Runner {
pub(crate) fn spawn<T, R>(
&self,
surface: Surface,
handler: Handler<T, R>,
ctx: SlackContext,
payload: T,
) -> tokio::task::JoinHandle<RunOutcome<R>>
where
T: Send + 'static,
R: Send + 'static,
{
let guard = InFlight::new(Arc::clone(&self.metrics));
self.tracker.spawn(async move {
let out = AssertUnwindSafe(async move { handler(ctx, payload).await })
.catch_unwind()
.await;
let metrics = Arc::clone(&guard.metrics);
drop(guard);
match out {
Ok(Ok(r)) => {
metrics.handler(surface, "ok");
RunOutcome::Ok(r)
}
Ok(Err(e)) => {
metrics.handler(surface, "error");
tracing::warn!(surface = surface.as_str(), error = %e, "slack handler failed");
RunOutcome::Failed
}
Err(_) => {
metrics.handler(surface, "panic");
tracing::error!(surface = surface.as_str(), "slack handler panicked");
RunOutcome::Panic
}
}
})
}
pub(crate) fn spawn_task(&self, fut: impl Future<Output = ()> + Send + 'static) {
drop(self.tracker.spawn(fut));
}
}
struct InFlight {
metrics: Arc<SlackMetrics>,
}
impl InFlight {
fn new(metrics: Arc<SlackMetrics>) -> Self {
metrics.in_flight_add(1);
Self { metrics }
}
}
impl Drop for InFlight {
fn drop(&mut self) {
self.metrics.in_flight_add(-1);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ok_cmd() -> Handler<SlashCommand, CommandResponse> {
boxed(|_c, _p| async { Ok(CommandResponse::Ack) })
}
#[test]
fn registry_rules() {
let r = Registry::build(vec![
Registration::Command("/a".into(), ok_cmd()),
Registration::Event("message".into(), boxed(|_c, _p| async { Ok(()) })),
])
.unwrap();
assert_eq!(r.len(), 2);
for bad in ["", "/a b", "a"] {
let e = Registry::build(vec![Registration::Command(bad.into(), ok_cmd())])
.err()
.unwrap();
assert!(matches!(e, SlackError::Config(_)), "{bad}");
}
let e = Registry::build(vec![
Registration::Shortcut("s".into(), boxed(|_c, _p| async { Ok(()) })),
Registration::Shortcut("s".into(), boxed(|_c, _p| async { Ok(()) })),
])
.err()
.unwrap();
assert!(e.to_string().contains("shortcut s"), "{e}");
}
}