Skip to main content

autumn_plugin_slack/
handlers.rs

1//! Handler types, the registry, and the task runner.
2
3use std::collections::HashMap;
4use std::future::Future;
5use std::panic::AssertUnwindSafe;
6use std::sync::Arc;
7
8use futures::FutureExt as _;
9
10use autumn_web::AppState;
11use futures::future::BoxFuture;
12use tokio_util::task::TaskTracker;
13
14use crate::client::SlackClient;
15use crate::error::{HandlerResult, SlackError};
16use crate::metrics::{SlackMetrics, Surface};
17use crate::payload::{
18    BlockAction, CommandResponse, EventCallback, Message, Shortcut, SlashCommand, ViewPayload,
19    ViewResponse,
20};
21
22/// What a handler gets besides the payload.
23#[derive(Clone, Debug)]
24pub struct SlackContext {
25    client: SlackClient,
26    state: AppState,
27}
28
29impl SlackContext {
30    pub(crate) const fn new(client: SlackClient, state: AppState) -> Self {
31        Self { client, state }
32    }
33
34    /// The Web API client.
35    #[must_use]
36    pub const fn client(&self) -> &SlackClient {
37        &self.client
38    }
39
40    /// The app state.
41    #[must_use]
42    pub const fn state(&self) -> &AppState {
43        &self.state
44    }
45}
46
47/// A boxed handler.
48pub(crate) type Handler<T, R> =
49    Arc<dyn Fn(SlackContext, T) -> BoxFuture<'static, HandlerResult<R>> + Send + Sync>;
50
51/// Boxes a handler function.
52pub(crate) fn boxed<T, R, F, Fut>(f: F) -> Handler<T, R>
53where
54    F: Fn(SlackContext, T) -> Fut + Send + Sync + 'static,
55    Fut: Future<Output = HandlerResult<R>> + Send + 'static,
56{
57    Arc::new(move |ctx, payload| Box::pin(f(ctx, payload)))
58}
59
60/// One registration from the builder.
61pub(crate) enum Registration {
62    Event(String, Handler<EventCallback, ()>),
63    Command(String, Handler<SlashCommand, CommandResponse>),
64    Action(String, Handler<BlockAction, Option<Message>>),
65    ViewSubmission(String, Handler<ViewPayload, ViewResponse>),
66    ViewClosed(String, Handler<ViewPayload, ()>),
67    Shortcut(String, Handler<Shortcut, ()>),
68}
69
70impl Registration {
71    /// Kind and key, for logs and errors.
72    pub(crate) fn describe(&self) -> (&'static str, &str) {
73        match self {
74            Self::Event(k, _) => ("event", k),
75            Self::Command(k, _) => ("command", k),
76            Self::Action(k, _) => ("action", k),
77            Self::ViewSubmission(k, _) => ("view_submission", k),
78            Self::ViewClosed(k, _) => ("view_closed", k),
79            Self::Shortcut(k, _) => ("shortcut", k),
80        }
81    }
82}
83
84/// Handlers by key.
85#[derive(Default)]
86pub(crate) struct Registry {
87    pub events: HashMap<String, Handler<EventCallback, ()>>,
88    pub commands: HashMap<String, Handler<SlashCommand, CommandResponse>>,
89    pub actions: HashMap<String, Handler<BlockAction, Option<Message>>>,
90    pub view_submissions: HashMap<String, Handler<ViewPayload, ViewResponse>>,
91    pub view_closed: HashMap<String, Handler<ViewPayload, ()>>,
92    pub shortcuts: HashMap<String, Handler<Shortcut, ()>>,
93}
94
95impl Registry {
96    /// Builds the registry.
97    ///
98    /// # Errors
99    /// Returns [`SlackError::Config`] for an empty key, a command with no
100    /// leading `/`, or a key that is registered twice.
101    pub(crate) fn build(regs: Vec<Registration>) -> Result<Self, SlackError> {
102        let mut r = Self::default();
103        for reg in regs {
104            let (kind, key) = reg.describe();
105            if key.trim().is_empty() || key.chars().any(char::is_whitespace) {
106                return Err(SlackError::Config(format!(
107                    "{kind} key {key:?} is empty or has a space"
108                )));
109            }
110            if kind == "command" && !key.starts_with('/') {
111                return Err(SlackError::Config(format!(
112                    "command {key:?} must start with /"
113                )));
114            }
115            let dup = |kind: &str, key: &str| {
116                SlackError::Config(format!("{kind} {key} is registered twice"))
117            };
118            match reg {
119                Registration::Event(k, h) => insert(&mut r.events, k, h, |k| dup("event", k))?,
120                Registration::Command(k, h) => {
121                    insert(&mut r.commands, k, h, |k| dup("command", k))?;
122                }
123                Registration::Action(k, h) => insert(&mut r.actions, k, h, |k| dup("action", k))?,
124                Registration::ViewSubmission(k, h) => {
125                    insert(&mut r.view_submissions, k, h, |k| dup("view_submission", k))?;
126                }
127                Registration::ViewClosed(k, h) => {
128                    insert(&mut r.view_closed, k, h, |k| dup("view_closed", k))?;
129                }
130                Registration::Shortcut(k, h) => {
131                    insert(&mut r.shortcuts, k, h, |k| dup("shortcut", k))?;
132                }
133            }
134        }
135        Ok(r)
136    }
137
138    /// Number of handlers.
139    pub(crate) fn len(&self) -> usize {
140        self.events.len()
141            + self.commands.len()
142            + self.actions.len()
143            + self.view_submissions.len()
144            + self.view_closed.len()
145            + self.shortcuts.len()
146    }
147}
148
149fn insert<V>(
150    map: &mut HashMap<String, V>,
151    key: String,
152    value: V,
153    dup: impl Fn(&str) -> SlackError,
154) -> Result<(), SlackError> {
155    if map.contains_key(&key) {
156        return Err(dup(&key));
157    }
158    map.insert(key, value);
159    Ok(())
160}
161
162/// How a handler run ended.
163pub(crate) enum RunOutcome<R> {
164    Ok(R),
165    Failed,
166    Panic,
167}
168
169impl<R> RunOutcome<R> {
170    /// The value of a run that ended well.
171    pub(crate) fn into_ok(self) -> Option<R> {
172        match self {
173            Self::Ok(r) => Some(r),
174            Self::Failed | Self::Panic => None,
175        }
176    }
177}
178
179/// Spawns handler runs. Counts them. Catches panics.
180#[derive(Clone)]
181pub(crate) struct Runner {
182    pub tracker: TaskTracker,
183    pub metrics: Arc<SlackMetrics>,
184}
185
186impl Runner {
187    /// Spawns `handler(ctx, payload)` as a tracked task.
188    pub(crate) fn spawn<T, R>(
189        &self,
190        surface: Surface,
191        handler: Handler<T, R>,
192        ctx: SlackContext,
193        payload: T,
194    ) -> tokio::task::JoinHandle<RunOutcome<R>>
195    where
196        T: Send + 'static,
197        R: Send + 'static,
198    {
199        let guard = InFlight::new(Arc::clone(&self.metrics));
200        self.tracker.spawn(async move {
201            // `catch_unwind` also catches a panic before the first poll.
202            let out = AssertUnwindSafe(async move { handler(ctx, payload).await })
203                .catch_unwind()
204                .await;
205            let metrics = Arc::clone(&guard.metrics);
206            drop(guard);
207            match out {
208                Ok(Ok(r)) => {
209                    metrics.handler(surface, "ok");
210                    RunOutcome::Ok(r)
211                }
212                Ok(Err(e)) => {
213                    metrics.handler(surface, "error");
214                    tracing::warn!(surface = surface.as_str(), error = %e, "slack handler failed");
215                    RunOutcome::Failed
216                }
217                Err(_) => {
218                    metrics.handler(surface, "panic");
219                    tracing::error!(surface = surface.as_str(), "slack handler panicked");
220                    RunOutcome::Panic
221                }
222            }
223        })
224    }
225
226    /// Spawns a tracked task with no handler, for a late reply.
227    pub(crate) fn spawn_task(&self, fut: impl Future<Output = ()> + Send + 'static) {
228        drop(self.tracker.spawn(fut));
229    }
230}
231
232/// Counts one handler that runs. `drop` also runs when the task aborts.
233struct InFlight {
234    metrics: Arc<SlackMetrics>,
235}
236
237impl InFlight {
238    fn new(metrics: Arc<SlackMetrics>) -> Self {
239        metrics.in_flight_add(1);
240        Self { metrics }
241    }
242}
243
244impl Drop for InFlight {
245    fn drop(&mut self) {
246        self.metrics.in_flight_add(-1);
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    fn ok_cmd() -> Handler<SlashCommand, CommandResponse> {
255        boxed(|_c, _p| async { Ok(CommandResponse::Ack) })
256    }
257
258    #[test]
259    fn registry_rules() {
260        let r = Registry::build(vec![
261            Registration::Command("/a".into(), ok_cmd()),
262            Registration::Event("message".into(), boxed(|_c, _p| async { Ok(()) })),
263        ])
264        .unwrap();
265        assert_eq!(r.len(), 2);
266        for bad in ["", "/a b", "a"] {
267            let e = Registry::build(vec![Registration::Command(bad.into(), ok_cmd())])
268                .err()
269                .unwrap();
270            assert!(matches!(e, SlackError::Config(_)), "{bad}");
271        }
272        let e = Registry::build(vec![
273            Registration::Shortcut("s".into(), boxed(|_c, _p| async { Ok(()) })),
274            Registration::Shortcut("s".into(), boxed(|_c, _p| async { Ok(()) })),
275        ])
276        .err()
277        .unwrap();
278        assert!(e.to_string().contains("shortcut s"), "{e}");
279    }
280}