slack_morphism/
listener.rs

1use crate::models::*;
2use crate::{BoxError, ClientResult, SlackClient, SlackClientHttpConnector};
3use futures::executor::block_on;
4use futures::FutureExt;
5use rsb_derive::Builder;
6use std::any::{Any, TypeId};
7use std::collections::HashMap;
8use std::fmt::Debug;
9use std::sync::Arc;
10use tracing::*;
11use url::Url;
12
13type UserStatesMap = HashMap<TypeId, Box<dyn Any + Send + Sync + 'static>>;
14
15pub type HttpStatusCode = http::StatusCode;
16
17pub struct SlackClientEventsListenerEnvironment<SCHC>
18where
19    SCHC: SlackClientHttpConnector + Send + Sync,
20{
21    pub client: Arc<SlackClient<SCHC>>,
22    pub error_handler: BoxedErrorHandler<SCHC>,
23    pub user_state: SlackClientEventsUserState,
24}
25
26pub type SlackClientEventsUserState = futures_locks::RwLock<SlackClientEventsUserStateStorage>;
27
28impl<SCHC> SlackClientEventsListenerEnvironment<SCHC>
29where
30    SCHC: SlackClientHttpConnector + Send + Sync,
31{
32    pub fn new(client: Arc<SlackClient<SCHC>>) -> Self {
33        Self {
34            client,
35            error_handler: Box::new(Self::empty_error_handler),
36            user_state: SlackClientEventsUserState::new(SlackClientEventsUserStateStorage::new()),
37        }
38    }
39
40    pub fn with_error_handler(self, error_handler: ErrorHandler<SCHC>) -> Self {
41        Self {
42            error_handler: Box::new(error_handler),
43            ..self
44        }
45    }
46
47    fn empty_error_handler(
48        err: BoxError,
49        _client: Arc<SlackClient<SCHC>>,
50        _user_state_storage: SlackClientEventsUserState,
51    ) -> http::StatusCode {
52        error!("Slack listener error occurred: {:?}", err);
53        http::StatusCode::BAD_REQUEST
54    }
55
56    pub fn with_user_state<T: Send + Sync + 'static>(self, state: T) -> Self {
57        let future_init_state = self
58            .user_state
59            .write()
60            .map(|mut guard| guard.set_user_state(state));
61        block_on(future_init_state);
62        self
63    }
64}
65
66pub struct SlackClientEventsUserStateStorage {
67    user_state_map: UserStatesMap,
68}
69
70impl SlackClientEventsUserStateStorage {
71    pub fn new() -> Self {
72        SlackClientEventsUserStateStorage {
73            user_state_map: HashMap::new(),
74        }
75    }
76
77    pub fn get_user_state<T: Send + Sync + 'static>(&self) -> Option<&T> {
78        self.user_state_map
79            .get(&TypeId::of::<T>())
80            .and_then(|boxed| (&**boxed as &(dyn Any + 'static)).downcast_ref())
81    }
82
83    pub fn set_user_state<T: Send + Sync + 'static>(&mut self, state: T) {
84        self.user_state_map
85            .insert(TypeId::of::<T>(), Box::new(state));
86    }
87
88    pub fn len(&self) -> usize {
89        self.user_state_map.len()
90    }
91
92    pub fn is_empty(&self) -> bool {
93        self.user_state_map.is_empty()
94    }
95}
96
97pub type BoxedErrorHandler<SCHC> = Box<ErrorHandler<SCHC>>;
98
99pub type ErrorHandler<SCHC> =
100    fn(BoxError, Arc<SlackClient<SCHC>>, SlackClientEventsUserState) -> HttpStatusCode;
101
102#[derive(Debug, PartialEq, Eq, Clone, Builder)]
103pub struct SlackCommandEventsListenerConfig {
104    pub events_signing_secret: SlackSigningSecret,
105    #[default = "SlackCommandEventsListenerConfig::DEFAULT_EVENTS_URL_VALUE.into()"]
106    pub events_path: String,
107}
108
109impl SlackCommandEventsListenerConfig {
110    pub const DEFAULT_EVENTS_URL_VALUE: &'static str = "/command";
111}
112
113#[derive(Debug, PartialEq, Eq, Clone, Builder)]
114pub struct SlackPushEventsListenerConfig {
115    pub events_signing_secret: SlackSigningSecret,
116    #[default = "SlackPushEventsListenerConfig::DEFAULT_EVENTS_URL_VALUE.into()"]
117    pub events_path: String,
118}
119
120impl SlackPushEventsListenerConfig {
121    const DEFAULT_EVENTS_URL_VALUE: &'static str = "/push";
122}
123
124#[derive(Debug, PartialEq, Eq, Clone, Builder)]
125pub struct SlackInteractionEventsListenerConfig {
126    pub events_signing_secret: SlackSigningSecret,
127    #[default = "SlackInteractionEventsListenerConfig::DEFAULT_EVENTS_URL_VALUE.into()"]
128    pub events_path: String,
129}
130
131impl SlackInteractionEventsListenerConfig {
132    pub const DEFAULT_EVENTS_URL_VALUE: &'static str = "/interaction";
133}
134
135#[derive(Debug, PartialEq, Eq, Clone, Builder)]
136pub struct SlackOAuthListenerConfig {
137    pub client_id: SlackClientId,
138    pub client_secret: SlackClientSecret,
139    pub bot_scope: String,
140    pub redirect_callback_host: String,
141    #[default = "SlackOAuthListenerConfig::DEFAULT_INSTALL_PATH_VALUE.into()"]
142    pub install_path: String,
143    #[default = "SlackOAuthListenerConfig::DEFAULT_CALLBACK_PATH_VALUE.into()"]
144    pub redirect_callback_path: String,
145    #[default = "SlackOAuthListenerConfig::DEFAULT_INSTALLED_URL_VALUE.into()"]
146    pub redirect_installed_url: String,
147    #[default = "SlackOAuthListenerConfig::DEFAULT_CANCELLED_URL_VALUE.into()"]
148    pub redirect_cancelled_url: String,
149    #[default = "SlackOAuthListenerConfig::DEFAULT_ERROR_URL_VALUE.into()"]
150    pub redirect_error_redirect_url: String,
151}
152
153impl SlackOAuthListenerConfig {
154    pub const DEFAULT_INSTALL_PATH_VALUE: &'static str = "/auth/install";
155    pub const DEFAULT_CALLBACK_PATH_VALUE: &'static str = "/auth/callback";
156    pub const DEFAULT_INSTALLED_URL_VALUE: &'static str = "/installed";
157    pub const DEFAULT_CANCELLED_URL_VALUE: &'static str = "/cancelled";
158    pub const DEFAULT_ERROR_URL_VALUE: &'static str = "/error";
159
160    pub const OAUTH_AUTHORIZE_URL_VALUE: &'static str = "https://slack.com/oauth/v2/authorize";
161
162    pub fn to_redirect_url(&self) -> ClientResult<Url> {
163        Url::parse(
164            format!(
165                "{}{}",
166                &self.redirect_callback_host, &self.redirect_callback_path
167            )
168            .as_str(),
169        )
170        .map_err(|e| e.into())
171    }
172}
173
174pub type UserCallbackFunction<E, IF, SCHC> =
175    fn(E, Arc<SlackClient<SCHC>>, SlackClientEventsUserState) -> IF;