1use 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#[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 #[must_use]
36 pub const fn client(&self) -> &SlackClient {
37 &self.client
38 }
39
40 #[must_use]
42 pub const fn state(&self) -> &AppState {
43 &self.state
44 }
45}
46
47pub(crate) type Handler<T, R> =
49 Arc<dyn Fn(SlackContext, T) -> BoxFuture<'static, HandlerResult<R>> + Send + Sync>;
50
51pub(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
60pub(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 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#[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 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 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
162pub(crate) enum RunOutcome<R> {
164 Ok(R),
165 Failed,
166 Panic,
167}
168
169impl<R> RunOutcome<R> {
170 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#[derive(Clone)]
181pub(crate) struct Runner {
182 pub tracker: TaskTracker,
183 pub metrics: Arc<SlackMetrics>,
184}
185
186impl Runner {
187 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 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 pub(crate) fn spawn_task(&self, fut: impl Future<Output = ()> + Send + 'static) {
228 drop(self.tracker.spawn(fut));
229 }
230}
231
232struct 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}