mirror of
https://github.com/System-End/slack-morphism-rust.git
synced 2026-04-19 23:22:56 +00:00
axum framework support for Slack Events API (#144)
* Middleware for Slack Events API tests * Middleware for Slack Events API tests * Middleware for Slack Events API tests * Axum OAuth impl begin * Axum OAuth impl * Middleware for Slack Events API tests * Working version * Docs and cleanups * Clippy fixes * Slack interaction event responses support for Hyper
This commit is contained in:
parent
29d923e919
commit
5f8f3eb9a6
34 changed files with 1407 additions and 458 deletions
10
Cargo.toml
10
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "slack-morphism"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
authors = ["Abdulla Abdurakhmanov <me@abdolence.dev>"]
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -23,7 +23,7 @@ path = "src/lib.rs"
|
|||
[features]
|
||||
default = []
|
||||
hyper = ["dep:tokio","dep:hyper", "dep:hyper-rustls", "dep:tokio-stream","dep:tokio-tungstenite", "dep:tokio-tungstenite", "dep:signal-hook", "dep:signal-hook-tokio"]
|
||||
axum = ["hyper", "dep:axum"]
|
||||
axum = ["hyper", "dep:axum", "dep:tower"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
@ -55,6 +55,7 @@ tokio-tungstenite = { version = "0.17.2", features = ["rustls-tls-native-roots"]
|
|||
signal-hook = { version = "0.3.14", features = ["extended-siginfo"], optional = true}
|
||||
signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"], optional = true }
|
||||
axum = { version = "0.5", optional = true }
|
||||
tower = { version = "0.4", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
cargo-husky = { version = "1", default-features = false, features = ["run-for-all", "prepush-hook", "run-cargo-fmt"] }
|
||||
|
|
@ -100,3 +101,8 @@ required-features = ["hyper"]
|
|||
name = "webhook_message"
|
||||
path = "examples/webhook_message.rs"
|
||||
required-features = ["hyper"]
|
||||
|
||||
[[example]]
|
||||
name = "axum_events_api_server"
|
||||
path = "examples/axum_events_api_server.rs"
|
||||
required-features = ["axum"]
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ The examples require to work the following environment variables (from your Slac
|
|||
|
||||
To run example use with environment variables:
|
||||
```
|
||||
# SLACK_... cargo run --example <client|events_api_server|socket_mode>
|
||||
# SLACK_... cargo run --example <client|events_api_server|axum_events_api_server|socket_mode> --all-features
|
||||
```
|
||||
|
||||
Routes for this example are available on http://<your-host>:8080:
|
||||
|
|
@ -47,7 +47,7 @@ SLACK_CLIENT_SECRET=<your-client-secret> \
|
|||
SLACK_BOT_SCOPE=app_mentions:read,incoming-webhook \
|
||||
SLACK_REDIRECT_HOST=https://<your-ngrok-url>.ngrok.io \
|
||||
SLACK_SIGNING_SECRET=<your-signing-secret> \
|
||||
cargo run --example events_api_server
|
||||
cargo run --example events_api_server --all-features
|
||||
```
|
||||
|
||||
## Licence
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
- [Hyper connection types and proxy support](./hyper-connections-types.md)
|
||||
- [Rate control and retries](./rate-control-and-retries.md)
|
||||
- [Events API](./events-api.md)
|
||||
- [Hyper-based](./events-api-hyper.md)
|
||||
- [Axum-based](./events-api-axum.md)
|
||||
- [Socket Mode](./socket-mode.md)
|
||||
- [User state](./user-state-in-event-listener.md)
|
||||
- [Limitations](./limitations.md)
|
||||
|
|
|
|||
140
docs/src/events-api-axum.md
Normal file
140
docs/src/events-api-axum.md
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
# Events API and OAuth
|
||||
|
||||
The library provides route implementation in `SlackClientEventsListener` based on Hyper/Tokio for:
|
||||
- Push Events
|
||||
- Interaction Events
|
||||
- Command Events
|
||||
- OAuth v2 redirects and client functions
|
||||
|
||||
You can chain all of the routes using `chain_service_routes_fn` from the library.
|
||||
|
||||
## Hyper configuration
|
||||
In order to use Events API/OAuth you need to configure Hyper HTTP server.
|
||||
There is nothing special about how to do that, and you can use [the official hyper docs](https://hyper.rs/).
|
||||
This is just merely a quick example how to use it with Slack Morphism routes.
|
||||
|
||||
To create a server, you need hyper `make_service_fn` and `service_fn`.
|
||||
|
||||
## Example
|
||||
```rust,noplaypen
|
||||
|
||||
use slack_morphism::prelude::*;
|
||||
|
||||
use hyper::{Body, Response};
|
||||
use tracing::*;
|
||||
|
||||
use axum::Extension;
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn test_oauth_install_function(
|
||||
resp: SlackOAuthV2AccessTokenResponse,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState,
|
||||
) {
|
||||
println!("{:#?}", resp);
|
||||
}
|
||||
|
||||
async fn test_push_event(
|
||||
Extension(_environment): Extension<Arc<SlackHyperListenerEnvironment>>,
|
||||
Extension(event): Extension<SlackPushEvent>,
|
||||
) -> Response<Body> {
|
||||
println!("Received push event: {:?}", event);
|
||||
|
||||
match event {
|
||||
SlackPushEvent::UrlVerification(url_ver) => Response::new(Body::from(url_ver.challenge)),
|
||||
_ => Response::new(Body::empty()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_command_event(
|
||||
Extension(_environment): Extension<Arc<SlackHyperListenerEnvironment>>,
|
||||
Extension(event): Extension<SlackCommandEvent>,
|
||||
) -> axum::Json<SlackCommandEventResponse> {
|
||||
println!("Received command event: {:?}", event);
|
||||
axum::Json(SlackCommandEventResponse::new(
|
||||
SlackMessageContent::new().with_text("Working on it".into()),
|
||||
))
|
||||
}
|
||||
|
||||
async fn test_interaction_event(
|
||||
Extension(_environment): Extension<Arc<SlackHyperListenerEnvironment>>,
|
||||
Extension(event): Extension<SlackInteractionEvent>,
|
||||
) {
|
||||
println!("Received interaction event: {:?}", event);
|
||||
}
|
||||
|
||||
fn test_error_handler(
|
||||
err: Box<dyn std::error::Error + Send + Sync>,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState,
|
||||
) -> http::StatusCode {
|
||||
println!("{:#?}", err);
|
||||
|
||||
// Defines what we return Slack server
|
||||
http::StatusCode::BAD_REQUEST
|
||||
}
|
||||
|
||||
async fn test_server() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let client: Arc<SlackHyperClient> =
|
||||
Arc::new(SlackClient::new(SlackClientHyperConnector::new()));
|
||||
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
info!("Loading server: {}", addr);
|
||||
|
||||
let oauth_listener_config = SlackOAuthListenerConfig::new(
|
||||
config_env_var("SLACK_CLIENT_ID")?.into(),
|
||||
config_env_var("SLACK_CLIENT_SECRET")?.into(),
|
||||
config_env_var("SLACK_BOT_SCOPE")?,
|
||||
config_env_var("SLACK_REDIRECT_HOST")?,
|
||||
);
|
||||
|
||||
let listener_environment: Arc<SlackHyperListenerEnvironment> = Arc::new(
|
||||
SlackClientEventsListenerEnvironment::new(client.clone())
|
||||
.with_error_handler(test_error_handler),
|
||||
);
|
||||
let signing_secret: SlackSigningSecret = config_env_var("SLACK_SIGNING_SECRET")?.into();
|
||||
|
||||
let listener: SlackEventsAxumListener<SlackHyperHttpsConnector> =
|
||||
SlackEventsAxumListener::new(listener_environment.clone());
|
||||
|
||||
// build our application route with OAuth nested router and Push/Command/Interaction events
|
||||
let app = axum::routing::Router::new()
|
||||
.nest(
|
||||
"/auth",
|
||||
listener.oauth_router("/auth", &oauth_listener_config, test_oauth_install_function),
|
||||
)
|
||||
.route(
|
||||
"/push",
|
||||
axum::routing::post(test_push_event).layer(
|
||||
listener
|
||||
.events_layer(&signing_secret)
|
||||
.with_event_extractor(SlackEventsExtractors::push_event()),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/command",
|
||||
axum::routing::post(test_command_event).layer(
|
||||
listener
|
||||
.events_layer(&signing_secret)
|
||||
.with_event_extractor(SlackEventsExtractors::command_event()),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/interaction",
|
||||
axum::routing::post(test_interaction_event).layer(
|
||||
listener
|
||||
.events_layer(&signing_secret)
|
||||
.with_event_extractor(SlackEventsExtractors::interaction_event()),
|
||||
),
|
||||
);
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
```
|
||||
Complete example look at [github](https://github.com/abdolence/slack-morphism-rust/tree/master/examples)
|
||||
177
docs/src/events-api-hyper.md
Normal file
177
docs/src/events-api-hyper.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# Events API and OAuth
|
||||
|
||||
The library provides routes and middleware implementation in `SlackEventsAxumListener` for:
|
||||
- Push Events
|
||||
- Interaction Events
|
||||
- Command Events
|
||||
- OAuth v2 redirects and client functions nested router
|
||||
|
||||
## Example
|
||||
```rust,noplaypen
|
||||
|
||||
use slack_morphism::prelude::*;
|
||||
|
||||
// Hyper imports
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use hyper::{Body, Request, Response};
|
||||
|
||||
// For logging
|
||||
use log::*;
|
||||
|
||||
// For convinience there is an alias SlackHyperClient as SlackClient<SlackClientHyperConnector>
|
||||
|
||||
async fn create_slack_events_listener_server() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
info!("Loading server: {}", addr);
|
||||
|
||||
// This is our default HTTP route when Slack routes didn't handle incoming request (different/other path).
|
||||
async fn your_others_routes(
|
||||
_req: Request<Body>,
|
||||
) -> Result<Response<Body>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Response::builder()
|
||||
.body("Hey, this is a default users route handler".into())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
// Our error handler for Slack Events API
|
||||
fn slack_listener_error_handler(err: Box<dyn std::error::Error + Send + Sync>,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState) -> http::StatusCode {
|
||||
error!("Slack Events error: {:#?}", err);
|
||||
|
||||
// Defines what we return Slack server
|
||||
http::StatusCode::BAD_REQUEST
|
||||
}
|
||||
|
||||
// We need also a client instance. `Arc` used here because we would like
|
||||
// to share the the same client for all of the requests and all hyper threads
|
||||
|
||||
let client = Arc::new(SlackClient::new(SlackClientHyperConnector::new()));
|
||||
|
||||
|
||||
// In this example we're going to use all of the events handlers, but
|
||||
// you don't have to.
|
||||
|
||||
// Our Slack OAuth handler with a token response after installation
|
||||
async fn slack_oauth_install_function(
|
||||
resp: SlackOAuthV2AccessTokenResponse,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState
|
||||
) {
|
||||
println!("{:#?}", resp);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Push events handler
|
||||
async fn slack_push_events_function(event: SlackPushEvent,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
println!("{:#?}", event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Interaction events handler
|
||||
async fn slack_interaction_events_function(event: SlackInteractionEvent,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
println!("{:#?}", event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Commands events handler
|
||||
async fn slack_command_events_function(
|
||||
event: SlackCommandEvent,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState
|
||||
) -> Result<SlackCommandEventResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
println!("{:#?}", event);
|
||||
Ok(SlackCommandEventResponse::new(
|
||||
SlackMessageContent::new().with_text("Working on it".into()),
|
||||
))
|
||||
}
|
||||
|
||||
// Now we need some configuration for our Slack listener routes.
|
||||
// You can additionally configure HTTP route paths using theses configs,
|
||||
// but for simplicity we will skip that part here and configure only required parameters.
|
||||
let oauth_listener_config = Arc::new(SlackOAuthListenerConfig::new(
|
||||
config_env_var("SLACK_CLIENT_ID")?.into(),
|
||||
config_env_var("SLACK_CLIENT_SECRET")?.into(),
|
||||
config_env_var("SLACK_BOT_SCOPE")?,
|
||||
config_env_var("SLACK_REDIRECT_HOST")?,
|
||||
));
|
||||
|
||||
let push_events_config = Arc::new(SlackPushEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
let interactions_events_config = Arc::new(SlackInteractionEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
let command_events_config = Arc::new(SlackCommandEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
// Creating a shared listener environment with an ability to share client and user state
|
||||
let listener_environment = Arc::new(
|
||||
SlackClientEventsListenerEnvironment::new(client.clone())
|
||||
.with_error_handler(test_error_handler)
|
||||
);
|
||||
|
||||
|
||||
let make_svc = make_service_fn(move |_| {
|
||||
// Because of threading model you have to create copies of configs.
|
||||
let thread_oauth_config = oauth_listener_config.clone();
|
||||
let thread_push_events_config = push_events_config.clone();
|
||||
let thread_interaction_events_config = interactions_events_config.clone();
|
||||
let thread_command_events_config = command_events_config.clone();
|
||||
|
||||
// Creating listener
|
||||
let listener = SlackClientEventsHyperListener::new(listener_environment.clone());
|
||||
|
||||
// Chaining all of the possible routes for Slack.
|
||||
// `chain_service_routes_fn` is an auxiliary function from Slack Morphism.
|
||||
async move {
|
||||
let routes = chain_service_routes_fn(
|
||||
listener.oauth_service_fn(thread_oauth_config, test_oauth_install_function),
|
||||
chain_service_routes_fn(
|
||||
listener.push_events_service_fn(
|
||||
thread_push_events_config,
|
||||
slack_push_events_function,
|
||||
),
|
||||
chain_service_routes_fn(
|
||||
listener.interaction_events_service_fn(
|
||||
thread_interaction_events_config,
|
||||
slack_interaction_events_function,
|
||||
),
|
||||
chain_service_routes_fn(
|
||||
listener.command_events_service_fn(
|
||||
thread_command_events_config,
|
||||
slack_command_events_function,
|
||||
),
|
||||
your_others_routes,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(service_fn(routes))
|
||||
}
|
||||
|
||||
)};
|
||||
|
||||
// Starting a server with listener routes
|
||||
let server = hyper::server::Server::bind(&addr).serve(make_svc);
|
||||
server.await.map_err(|e| {
|
||||
error!("Server error: {}", e);
|
||||
e.into()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Complete example look at [github](https://github.com/abdolence/slack-morphism-rust/tree/master/examples)
|
||||
|
|
@ -1,187 +1,8 @@
|
|||
# Events API and OAuth
|
||||
|
||||
The library provides route implementation in `SlackClientEventsListener` based on Hyper/Tokio for:
|
||||
- Push Events
|
||||
- Interaction Events
|
||||
- Command Events
|
||||
- OAuth v2 redirects and client functions
|
||||
|
||||
You can chain all of the routes using `chain_service_routes_fn` from the library.
|
||||
|
||||
## Hyper configuration
|
||||
In order to use Events API/OAuth you need to configure Hyper HTTP server.
|
||||
There is nothing special about how to do that, and you can use [the official hyper docs](https://hyper.rs/).
|
||||
This is just merely a quick example how to use it with Slack Morphism routes.
|
||||
|
||||
To create a server, you need hyper `make_service_fn` and `service_fn`.
|
||||
|
||||
## Example
|
||||
```rust,noplaypen
|
||||
|
||||
use slack_morphism::prelude::*;
|
||||
|
||||
// Hyper imports
|
||||
use hyper::service::{make_service_fn, service_fn};
|
||||
use hyper::{Body, Request, Response};
|
||||
|
||||
// For logging
|
||||
use log::*;
|
||||
|
||||
// For convinience there is an alias SlackHyperClient as SlackClient<SlackClientHyperConnector>
|
||||
|
||||
async fn create_slack_events_listener_server() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
info!("Loading server: {}", addr);
|
||||
|
||||
// This is our default HTTP route when Slack routes didn't handle incoming request (different/other path).
|
||||
async fn your_others_routes(
|
||||
_req: Request<Body>,
|
||||
) -> Result<Response<Body>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Response::builder()
|
||||
.body("Hey, this is a default users route handler".into())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
// Our error handler for Slack Events API
|
||||
fn slack_listener_error_handler(err: Box<dyn std::error::Error + Send + Sync>,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState) -> http::StatusCode {
|
||||
error!("Slack Events error: {:#?}", err);
|
||||
|
||||
// Defines what we return Slack server
|
||||
http::StatusCode::BAD_REQUEST
|
||||
}
|
||||
|
||||
// We need also a client instance. `Arc` used here because we would like
|
||||
// to share the the same client for all of the requests and all hyper threads
|
||||
|
||||
let client = Arc::new(SlackClient::new(SlackClientHyperConnector::new()));
|
||||
|
||||
|
||||
// In this example we're going to use all of the events handlers, but
|
||||
// you don't have to.
|
||||
|
||||
// Our Slack OAuth handler with a token response after installation
|
||||
async fn slack_oauth_install_function(
|
||||
resp: SlackOAuthV2AccessTokenResponse,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState
|
||||
) {
|
||||
println!("{:#?}", resp);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Push events handler
|
||||
async fn slack_push_events_function(event: SlackPushEvent,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
println!("{:#?}", event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Interaction events handler
|
||||
async fn slack_interaction_events_function(event: SlackInteractionEvent,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
println!("{:#?}", event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Commands events handler
|
||||
async fn slack_command_events_function(
|
||||
event: SlackCommandEvent,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState
|
||||
) -> Result<SlackCommandEventResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
println!("{:#?}", event);
|
||||
Ok(SlackCommandEventResponse::new(
|
||||
SlackMessageContent::new().with_text("Working on it".into()),
|
||||
))
|
||||
}
|
||||
|
||||
// Now we need some configuration for our Slack listener routes.
|
||||
// You can additionally configure HTTP route paths using theses configs,
|
||||
// but for simplicity we will skip that part here and configure only required parameters.
|
||||
let oauth_listener_config = Arc::new(SlackOAuthListenerConfig::new(
|
||||
config_env_var("SLACK_CLIENT_ID")?.into(),
|
||||
config_env_var("SLACK_CLIENT_SECRET")?.into(),
|
||||
config_env_var("SLACK_BOT_SCOPE")?,
|
||||
config_env_var("SLACK_REDIRECT_HOST")?,
|
||||
));
|
||||
|
||||
let push_events_config = Arc::new(SlackPushEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
let interactions_events_config = Arc::new(SlackInteractionEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
let command_events_config = Arc::new(SlackCommandEventsListenerConfig::new(
|
||||
config_env_var("SLACK_SIGNING_SECRET")?.into(),
|
||||
));
|
||||
|
||||
// Creating a shared listener environment with an ability to share client and user state
|
||||
let listener_environment = Arc::new(
|
||||
SlackClientEventsListenerEnvironment::new(client.clone())
|
||||
.with_error_handler(test_error_handler)
|
||||
);
|
||||
|
||||
|
||||
let make_svc = make_service_fn(move |_| {
|
||||
// Because of threading model you have to create copies of configs.
|
||||
let thread_oauth_config = oauth_listener_config.clone();
|
||||
let thread_push_events_config = push_events_config.clone();
|
||||
let thread_interaction_events_config = interactions_events_config.clone();
|
||||
let thread_command_events_config = command_events_config.clone();
|
||||
|
||||
// Creating listener
|
||||
let listener = SlackClientEventsHyperListener::new(listener_environment.clone());
|
||||
|
||||
// Chaining all of the possible routes for Slack.
|
||||
// `chain_service_routes_fn` is an auxiliary function from Slack Morphism.
|
||||
async move {
|
||||
let routes = chain_service_routes_fn(
|
||||
listener.oauth_service_fn(thread_oauth_config, test_oauth_install_function),
|
||||
chain_service_routes_fn(
|
||||
listener.push_events_service_fn(
|
||||
thread_push_events_config,
|
||||
slack_push_events_function,
|
||||
),
|
||||
chain_service_routes_fn(
|
||||
listener.interaction_events_service_fn(
|
||||
thread_interaction_events_config,
|
||||
slack_interaction_events_function,
|
||||
),
|
||||
chain_service_routes_fn(
|
||||
listener.command_events_service_fn(
|
||||
thread_command_events_config,
|
||||
slack_command_events_function,
|
||||
),
|
||||
your_others_routes,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Ok::<_, Box<dyn std::error::Error + Send + Sync>>(service_fn(routes))
|
||||
}
|
||||
|
||||
)};
|
||||
|
||||
// Starting a server with listener routes
|
||||
let server = hyper::server::Server::bind(&addr).serve(make_svc);
|
||||
server.await.map_err(|e| {
|
||||
error!("Server error: {}", e);
|
||||
e.into()
|
||||
})
|
||||
}
|
||||
```
|
||||
The library provides two different ways to work with Slack Events API:
|
||||
- Using [pure Hyper-based solution](./events-api-hyper.md)
|
||||
- Using more [high-level solution for axum web framework](./events-api-axum.md)
|
||||
|
||||
## Testing with ngrok
|
||||
For development/testing purposes you can use [ngrok](https://ngrok.com/):
|
||||
|
|
@ -197,7 +18,7 @@ SLACK_CLIENT_SECRET=<your-client-secret> \
|
|||
SLACK_BOT_SCOPE=app_mentions:read,incoming-webhook \
|
||||
SLACK_REDIRECT_HOST=https://<your-ngrok-url>.ngrok.io \
|
||||
SLACK_SIGNING_SECRET=<your-signing-secret> \
|
||||
cargo run --example events_api_server
|
||||
cargo run --example events_api_server --all-features
|
||||
```
|
||||
|
||||
## Slack Signature Verifier
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Cargo.toml dependencies example:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
slack-morphism = { version = "1.1", features = ["hyper"] }
|
||||
slack-morphism = { version = "1.2", features = ["hyper", "axum"] }
|
||||
```
|
||||
|
||||
All imports you need:
|
||||
|
|
@ -15,7 +15,7 @@ use slack_morphism::prelude::*;
|
|||
|
||||
## Ready to use examples
|
||||
- Slack Web API client and Block kit example
|
||||
- Events API server example
|
||||
- Events API server example using either pure hyper solution or axum
|
||||
- Slack Web API client with Socket Mode
|
||||
|
||||
You can find them on [github](https://github.com/abdolence/slack-morphism-rust/tree/master/examples)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ All of the models, API and Block Kit support in Slack Morphism are well-typed.
|
|||
The library depends only on familiar for Rust developers principles and libraries like Serde, futures, hyper.
|
||||
|
||||
## Async
|
||||
Using latest Rust async/await language features and libraries, the library provides access to all of the functions
|
||||
Using the latest Rust async/await language features and libraries, the library provides access to all of the functions
|
||||
in asynchronous manner.
|
||||
|
||||
## Modular design
|
||||
|
|
@ -19,4 +19,4 @@ Includes also all type/models definitions that used for Slack Web/Events APIs.
|
|||
|
||||
This library provided the following features:
|
||||
- `hyper`: Slack client support/binding for Hyper/Tokio/Tungstenite.
|
||||
- `axum`: Slack client support/binding for [axum framework](https://github.com/tokio-rs/axum) support (WIP, will be available in next releases).
|
||||
- `axum`: Slack client support/binding for [axum framework](https://github.com/tokio-rs/axum) support.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
It is very common to have some user specific context and state in event handler functions.
|
||||
So, all listener handlers has access to it using `SlackClientEventsUserStateStorage`.
|
||||
|
||||
This needs for Hyper or Socket Mode. For Axum use its own support for user state management.
|
||||
|
||||
## Defining user state
|
||||
```rust,noplaypen
|
||||
|
||||
|
|
@ -18,7 +20,7 @@ let listener_environment = Arc::new(
|
|||
|
||||
```
|
||||
|
||||
## Reading user state in listeners
|
||||
## Reading user state in listeners for Hyper/Socket Mode
|
||||
|
||||
```rust,noplaypen
|
||||
async fn test_push_events_function(
|
||||
|
|
@ -36,7 +38,7 @@ async fn test_push_events_function(
|
|||
}
|
||||
```
|
||||
|
||||
## Updating user state in listeners
|
||||
## Updating user state in listeners for Hyper/Socket Mode
|
||||
|
||||
```rust,noplaypen
|
||||
async fn test_push_events_function(
|
||||
|
|
|
|||
148
examples/axum_events_api_server.rs
Normal file
148
examples/axum_events_api_server.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
use slack_morphism::prelude::*;
|
||||
|
||||
use hyper::{Body, Response};
|
||||
use tracing::*;
|
||||
|
||||
use axum::Extension;
|
||||
use std::sync::Arc;
|
||||
|
||||
async fn test_oauth_install_function(
|
||||
resp: SlackOAuthV2AccessTokenResponse,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState,
|
||||
) {
|
||||
println!("{:#?}", resp);
|
||||
}
|
||||
|
||||
async fn test_welcome_installed() -> String {
|
||||
"Welcome".to_string()
|
||||
}
|
||||
|
||||
async fn test_cancelled_install() -> String {
|
||||
"Cancelled".to_string()
|
||||
}
|
||||
|
||||
async fn test_error_install() -> String {
|
||||
"Error while installing".to_string()
|
||||
}
|
||||
|
||||
async fn test_push_event(
|
||||
Extension(_environment): Extension<Arc<SlackHyperListenerEnvironment>>,
|
||||
Extension(event): Extension<SlackPushEvent>,
|
||||
) -> Response<Body> {
|
||||
println!("Received push event: {:?}", event);
|
||||
|
||||
match event {
|
||||
SlackPushEvent::UrlVerification(url_ver) => Response::new(Body::from(url_ver.challenge)),
|
||||
_ => Response::new(Body::empty()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_command_event(
|
||||
Extension(_environment): Extension<Arc<SlackHyperListenerEnvironment>>,
|
||||
Extension(event): Extension<SlackCommandEvent>,
|
||||
) -> axum::Json<SlackCommandEventResponse> {
|
||||
println!("Received command event: {:?}", event);
|
||||
axum::Json(SlackCommandEventResponse::new(
|
||||
SlackMessageContent::new().with_text("Working on it".into()),
|
||||
))
|
||||
}
|
||||
|
||||
async fn test_interaction_event(
|
||||
Extension(_environment): Extension<Arc<SlackHyperListenerEnvironment>>,
|
||||
Extension(event): Extension<SlackInteractionEvent>,
|
||||
) {
|
||||
println!("Received interaction event: {:?}", event);
|
||||
}
|
||||
|
||||
fn test_error_handler(
|
||||
err: Box<dyn std::error::Error + Send + Sync>,
|
||||
_client: Arc<SlackHyperClient>,
|
||||
_states: SlackClientEventsUserState,
|
||||
) -> http::StatusCode {
|
||||
println!("{:#?}", err);
|
||||
|
||||
// Defines what we return Slack server
|
||||
http::StatusCode::BAD_REQUEST
|
||||
}
|
||||
|
||||
async fn test_server() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let client: Arc<SlackHyperClient> =
|
||||
Arc::new(SlackClient::new(SlackClientHyperConnector::new()));
|
||||
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
info!("Loading server: {}", addr);
|
||||
|
||||
let oauth_listener_config = SlackOAuthListenerConfig::new(
|
||||
config_env_var("SLACK_CLIENT_ID")?.into(),
|
||||
config_env_var("SLACK_CLIENT_SECRET")?.into(),
|
||||
config_env_var("SLACK_BOT_SCOPE")?,
|
||||
config_env_var("SLACK_REDIRECT_HOST")?,
|
||||
);
|
||||
|
||||
let listener_environment: Arc<SlackHyperListenerEnvironment> = Arc::new(
|
||||
SlackClientEventsListenerEnvironment::new(client.clone())
|
||||
.with_error_handler(test_error_handler),
|
||||
);
|
||||
let signing_secret: SlackSigningSecret = config_env_var("SLACK_SIGNING_SECRET")?.into();
|
||||
|
||||
let listener: SlackEventsAxumListener<SlackHyperHttpsConnector> =
|
||||
SlackEventsAxumListener::new(listener_environment.clone());
|
||||
|
||||
// build our application route with OAuth nested router and Push/Command/Interaction events
|
||||
let app = axum::routing::Router::new()
|
||||
.nest(
|
||||
"/auth",
|
||||
listener.oauth_router("/auth", &oauth_listener_config, test_oauth_install_function),
|
||||
)
|
||||
.route("/installed", axum::routing::get(test_welcome_installed))
|
||||
.route("/cancelled", axum::routing::get(test_cancelled_install))
|
||||
.route("/error", axum::routing::get(test_error_install))
|
||||
.route(
|
||||
"/push",
|
||||
axum::routing::post(test_push_event).layer(
|
||||
listener
|
||||
.events_layer(&signing_secret)
|
||||
.with_event_extractor(SlackEventsExtractors::push_event()),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/command",
|
||||
axum::routing::post(test_command_event).layer(
|
||||
listener
|
||||
.events_layer(&signing_secret)
|
||||
.with_event_extractor(SlackEventsExtractors::command_event()),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/interaction",
|
||||
axum::routing::post(test_interaction_event).layer(
|
||||
listener
|
||||
.events_layer(&signing_secret)
|
||||
.with_event_extractor(SlackEventsExtractors::interaction_event()),
|
||||
),
|
||||
);
|
||||
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn config_env_var(name: &str) -> Result<String, String> {
|
||||
std::env::var(name).map_err(|e| format!("{}: {}", name, e))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter("slack_morphism=debug")
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
test_server().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -152,7 +152,7 @@ pub fn config_env_var(name: &str) -> Result<String, String> {
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter("slack_morphism_hyper=debug,slack_morphism=debug")
|
||||
.with_env_filter("slack_morphism=debug")
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ pub fn config_env_var(name: &str) -> Result<String, String> {
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter("slack_morphism_hyper=debug,slack_morphism=debug")
|
||||
.with_env_filter("slack_morphism=debug")
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub fn config_env_var(name: &str) -> Result<String, String> {
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter("slack_morphism_hyper=debug,slack_morphism=debug")
|
||||
.with_env_filter("slack_morphism=debug")
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ pub fn config_env_var(name: &str) -> Result<String, String> {
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter("slack_morphism_hyper=debug,slack_morphism=debug")
|
||||
.with_env_filter("slack_morphism=debug")
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ pub fn config_env_var(name: &str) -> Result<String, String> {
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter("slack_morphism_hyper=debug,slack_morphism=debug")
|
||||
.with_env_filter("slack_morphism=debug")
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ pub fn config_env_var(name: &str) -> Result<String, String> {
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let subscriber = tracing_subscriber::fmt()
|
||||
.with_env_filter("slack_morphism_hyper=debug,slack_morphism=debug")
|
||||
.with_env_filter("slack_morphism=debug")
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
use crate::hyper_tokio::SlackClientHyperConnector;
|
||||
use crate::listener::SlackClientEventsListenerEnvironment;
|
||||
use hyper::client::connect::Connect;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod slack_events_middleware;
|
||||
pub use slack_events_middleware::SlackEventsApiMiddleware;
|
||||
|
||||
pub struct SlackEventsAxumListener<H: 'static + Send + Sync + Connect + Clone> {
|
||||
pub environment: Arc<SlackClientEventsListenerEnvironment<SlackClientHyperConnector<H>>>,
|
||||
}
|
||||
|
||||
impl<H: 'static + Send + Sync + Connect + Clone> SlackEventsAxumListener<H> {
|
||||
pub fn new(
|
||||
environment: Arc<SlackClientEventsListenerEnvironment<SlackClientHyperConnector<H>>>,
|
||||
) -> Self {
|
||||
Self { environment }
|
||||
}
|
||||
}
|
||||
|
||||
mod slack_oauth_routes;
|
||||
pub use slack_oauth_routes::*;
|
||||
|
||||
mod slack_events_extractors;
|
||||
pub use slack_events_extractors::SlackEventsExtractors;
|
||||
152
src/axum_support/slack_events_extractors.rs
Normal file
152
src/axum_support/slack_events_extractors.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
use crate::errors::*;
|
||||
use crate::events::{SlackCommandEvent, SlackInteractionEvent, SlackPushEvent};
|
||||
use crate::AnyStdResult;
|
||||
use http::Extensions;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub trait SlackEventsExtractor {
|
||||
fn extract(&self, verified_body: &str, extensions: &mut Extensions) -> AnyStdResult<()>;
|
||||
}
|
||||
|
||||
pub struct SlackEventsExtractors;
|
||||
|
||||
impl SlackEventsExtractors {
|
||||
pub fn empty() -> SlackEventsEmptyExtractor {
|
||||
SlackEventsEmptyExtractor::new()
|
||||
}
|
||||
|
||||
pub fn push_event() -> SlackPushEventsExtractor {
|
||||
SlackPushEventsExtractor::new()
|
||||
}
|
||||
|
||||
pub fn command_event() -> SlackCommandEventsExtractor {
|
||||
SlackCommandEventsExtractor::new()
|
||||
}
|
||||
|
||||
pub fn interaction_event() -> SlackInteractionEventsExtractor {
|
||||
SlackInteractionEventsExtractor::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlackEventsEmptyExtractor;
|
||||
|
||||
impl SlackEventsEmptyExtractor {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlackEventsExtractor for SlackEventsEmptyExtractor {
|
||||
fn extract(&self, _verified_body: &str, _extensions: &mut Extensions) -> AnyStdResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlackPushEventsExtractor;
|
||||
|
||||
impl SlackPushEventsExtractor {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlackEventsExtractor for SlackPushEventsExtractor {
|
||||
fn extract(&self, verified_body: &str, extensions: &mut Extensions) -> AnyStdResult<()> {
|
||||
let event = serde_json::from_str::<SlackPushEvent>(verified_body).map_err(|e| {
|
||||
SlackClientProtocolError::new(e).with_json_body(verified_body.to_string())
|
||||
})?;
|
||||
|
||||
extensions.insert(event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlackCommandEventsExtractor;
|
||||
|
||||
impl SlackCommandEventsExtractor {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlackEventsExtractor for SlackCommandEventsExtractor {
|
||||
fn extract(&self, verified_body: &str, extensions: &mut Extensions) -> AnyStdResult<()> {
|
||||
let body_params: HashMap<String, String> =
|
||||
url::form_urlencoded::parse(verified_body.as_bytes())
|
||||
.into_owned()
|
||||
.collect();
|
||||
|
||||
let event: SlackCommandEvent = match (
|
||||
body_params.get("team_id"),
|
||||
body_params.get("channel_id"),
|
||||
body_params.get("user_id"),
|
||||
body_params.get("command"),
|
||||
body_params.get("text"),
|
||||
body_params.get("response_url"),
|
||||
body_params.get("trigger_id"),
|
||||
) {
|
||||
(
|
||||
Some(team_id),
|
||||
Some(channel_id),
|
||||
Some(user_id),
|
||||
Some(command),
|
||||
text,
|
||||
Some(response_url),
|
||||
Some(trigger_id),
|
||||
) => Ok(SlackCommandEvent::new(
|
||||
team_id.into(),
|
||||
channel_id.into(),
|
||||
user_id.into(),
|
||||
command.into(),
|
||||
url::Url::parse(response_url)?.into(),
|
||||
trigger_id.into(),
|
||||
)
|
||||
.opt_text(text.cloned())),
|
||||
_ => Err(SlackClientError::SystemError(
|
||||
SlackClientSystemError::new()
|
||||
.with_message("Absent payload in the request from Slack".into()),
|
||||
)),
|
||||
}?;
|
||||
|
||||
extensions.insert(event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlackInteractionEventsExtractor;
|
||||
|
||||
impl SlackInteractionEventsExtractor {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlackEventsExtractor for SlackInteractionEventsExtractor {
|
||||
fn extract(&self, verified_body: &str, extensions: &mut Extensions) -> AnyStdResult<()> {
|
||||
let body_params: HashMap<String, String> =
|
||||
url::form_urlencoded::parse(verified_body.as_bytes())
|
||||
.into_owned()
|
||||
.collect();
|
||||
|
||||
let payload = body_params.get("payload").ok_or_else(|| {
|
||||
SlackClientError::SystemError(
|
||||
SlackClientSystemError::new()
|
||||
.with_message("Absent payload in the request from Slack".into()),
|
||||
)
|
||||
})?;
|
||||
|
||||
let event: SlackInteractionEvent =
|
||||
serde_json::from_str::<SlackInteractionEvent>(payload)
|
||||
.map_err(|e| SlackClientProtocolError::new(e).with_json_body(payload.clone()))?;
|
||||
|
||||
extensions.insert(event);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
226
src/axum_support/slack_events_middleware.rs
Normal file
226
src/axum_support/slack_events_middleware.rs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
use crate::axum_support::slack_events_extractors::{
|
||||
SlackEventsEmptyExtractor, SlackEventsExtractor,
|
||||
};
|
||||
use crate::axum_support::SlackEventsAxumListener;
|
||||
use crate::hyper_tokio::SlackClientHyperConnector;
|
||||
use crate::listener::SlackClientEventsListenerEnvironment;
|
||||
use crate::prelude::hyper_ext::HyperExtensions;
|
||||
use crate::signature_verifier::SlackEventSignatureVerifier;
|
||||
use crate::{SlackClientHttpConnector, SlackSigningSecret};
|
||||
use axum::body::BoxBody;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{body::Body, http::Request, response::Response};
|
||||
use futures_util::future::BoxFuture;
|
||||
use hyper::client::connect::Connect;
|
||||
use std::convert::Infallible;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
use tower::{Layer, Service};
|
||||
use tracing::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SlackEventsApiMiddlewareService<S, SCHC, SE>
|
||||
where
|
||||
SCHC: SlackClientHttpConnector + Send + Sync,
|
||||
SE: SlackEventsExtractor + Clone,
|
||||
{
|
||||
inner: Option<S>,
|
||||
environment: Arc<SlackClientEventsListenerEnvironment<SCHC>>,
|
||||
signature_verifier: SlackEventSignatureVerifier,
|
||||
extractor: SE,
|
||||
}
|
||||
|
||||
impl<S, SCHC, I, SE> SlackEventsApiMiddlewareService<S, SCHC, SE>
|
||||
where
|
||||
S: Service<Request<Body>, Response = I> + Send + 'static + Clone,
|
||||
S::Future: Send + 'static,
|
||||
S::Error: std::error::Error + 'static + Send + Sync,
|
||||
I: IntoResponse,
|
||||
SCHC: SlackClientHttpConnector + Send + Sync + 'static,
|
||||
SE: SlackEventsExtractor + Clone,
|
||||
{
|
||||
pub fn new(
|
||||
service: S,
|
||||
environment: Arc<SlackClientEventsListenerEnvironment<SCHC>>,
|
||||
secret: &SlackSigningSecret,
|
||||
extractor: SE,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Some(service),
|
||||
environment,
|
||||
signature_verifier: SlackEventSignatureVerifier::new(secret),
|
||||
extractor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, SCHC, SE> Service<Request<Body>> for SlackEventsApiMiddlewareService<S, SCHC, SE>
|
||||
where
|
||||
S: Service<Request<Body>, Response = Response, Error = Infallible> + Send + 'static + Clone,
|
||||
S::Future: Send + 'static,
|
||||
SCHC: SlackClientHttpConnector + Send + Sync + 'static,
|
||||
SE: SlackEventsExtractor + Clone + Send + Sync + 'static,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = Infallible;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
|
||||
if let Some(ref mut service) = self.inner.as_mut() {
|
||||
service.poll_ready(cx)
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request<Body>) -> Self::Future {
|
||||
let mut service = self.inner.take().unwrap();
|
||||
self.inner = Some(service.clone());
|
||||
let environment = self.environment.clone();
|
||||
|
||||
let signature_verifier = self.signature_verifier.clone();
|
||||
let extractor = self.extractor.clone();
|
||||
let request_uri = request.uri().clone();
|
||||
|
||||
debug!("Received Slack event: {}", &request_uri);
|
||||
|
||||
Box::pin(async move {
|
||||
match HyperExtensions::decode_signed_response(request, &signature_verifier).await {
|
||||
Ok(verified_body) => {
|
||||
let mut verified_request = Request::new(Body::empty());
|
||||
|
||||
verified_request
|
||||
.extensions_mut()
|
||||
.insert(environment.clone());
|
||||
|
||||
if let Err(err) =
|
||||
extractor.extract(verified_body.as_str(), verified_request.extensions_mut())
|
||||
{
|
||||
let http_status = (environment.error_handler)(
|
||||
err,
|
||||
environment.client.clone(),
|
||||
environment.user_state.clone(),
|
||||
);
|
||||
Ok(Response::builder()
|
||||
.status(http_status)
|
||||
.body(BoxBody::default())
|
||||
.unwrap())
|
||||
} else {
|
||||
*verified_request.body_mut() = Body::from(verified_body);
|
||||
|
||||
debug!("Calling a route service with Slack event: {}", &request_uri);
|
||||
|
||||
match service.call(verified_request).await {
|
||||
Ok(response) => {
|
||||
debug!("Route service finished successfully for: {}", &request_uri);
|
||||
Ok(response)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("A route service failed: {} with {}", &request_uri, err);
|
||||
|
||||
let http_status = (environment.error_handler)(
|
||||
Box::new(err),
|
||||
environment.client.clone(),
|
||||
environment.user_state.clone(),
|
||||
);
|
||||
Ok(Response::builder()
|
||||
.status(http_status)
|
||||
.body(BoxBody::default())
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("Slack event error: {}", err);
|
||||
let http_status = (environment.error_handler)(
|
||||
err,
|
||||
environment.client.clone(),
|
||||
environment.user_state.clone(),
|
||||
);
|
||||
Ok(Response::builder()
|
||||
.status(http_status)
|
||||
.body(BoxBody::default())
|
||||
.unwrap())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SlackEventsApiMiddleware<SCHC, S, SE>
|
||||
where
|
||||
SCHC: SlackClientHttpConnector + Send + Sync,
|
||||
SE: SlackEventsExtractor,
|
||||
{
|
||||
slack_signing_secret: SlackSigningSecret,
|
||||
environment: Arc<SlackClientEventsListenerEnvironment<SCHC>>,
|
||||
extractor: SE,
|
||||
_ph_s: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<SCHC, S> SlackEventsApiMiddleware<SCHC, S, SlackEventsEmptyExtractor>
|
||||
where
|
||||
SCHC: SlackClientHttpConnector + Send + Sync,
|
||||
{
|
||||
pub fn new(
|
||||
environment: Arc<SlackClientEventsListenerEnvironment<SCHC>>,
|
||||
slack_signing_secret: &SlackSigningSecret,
|
||||
) -> Self {
|
||||
Self {
|
||||
slack_signing_secret: slack_signing_secret.clone(),
|
||||
environment,
|
||||
extractor: SlackEventsEmptyExtractor::new(),
|
||||
_ph_s: PhantomData::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_event_extractor<SE>(self, extractor: SE) -> SlackEventsApiMiddleware<SCHC, S, SE>
|
||||
where
|
||||
SE: SlackEventsExtractor,
|
||||
{
|
||||
SlackEventsApiMiddleware {
|
||||
slack_signing_secret: self.slack_signing_secret,
|
||||
environment: self.environment,
|
||||
extractor,
|
||||
_ph_s: PhantomData::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, SCHC, I, SE> Layer<S> for SlackEventsApiMiddleware<SCHC, S, SE>
|
||||
where
|
||||
S: Service<Request<Body>, Response = I> + Send + 'static + Clone,
|
||||
S::Future: Send + 'static,
|
||||
S::Error: std::error::Error + 'static + Send + Sync,
|
||||
I: IntoResponse,
|
||||
SCHC: SlackClientHttpConnector + Send + Sync + 'static,
|
||||
SE: SlackEventsExtractor + Clone,
|
||||
{
|
||||
type Service = SlackEventsApiMiddlewareService<S, SCHC, SE>;
|
||||
|
||||
fn layer(&self, service: S) -> SlackEventsApiMiddlewareService<S, SCHC, SE> {
|
||||
SlackEventsApiMiddlewareService::new(
|
||||
service,
|
||||
self.environment.clone(),
|
||||
&self.slack_signing_secret,
|
||||
self.extractor.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: 'static + Send + Sync + Connect + Clone> SlackEventsAxumListener<H> {
|
||||
pub fn events_layer<S, ReqBody, I>(
|
||||
&self,
|
||||
slack_signing_secret: &SlackSigningSecret,
|
||||
) -> SlackEventsApiMiddleware<SlackClientHyperConnector<H>, S, SlackEventsEmptyExtractor>
|
||||
where
|
||||
S: Service<Request<ReqBody>, Response = I> + Send + 'static + Clone,
|
||||
S::Future: Send + 'static,
|
||||
S::Error: std::error::Error + 'static + Send + Sync,
|
||||
I: IntoResponse,
|
||||
{
|
||||
SlackEventsApiMiddleware::new(self.environment.clone(), slack_signing_secret)
|
||||
}
|
||||
}
|
||||
215
src/axum_support/slack_oauth_routes.rs
Normal file
215
src/axum_support/slack_oauth_routes.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
use crate::axum_support::SlackEventsAxumListener;
|
||||
use crate::hyper_tokio::hyper_ext::HyperExtensions;
|
||||
use crate::listener::{SlackClientEventsListenerEnvironment, UserCallbackFunction};
|
||||
use crate::prelude::SlackOAuthListenerConfig;
|
||||
use axum::response::Response;
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::FutureExt;
|
||||
use http::Request;
|
||||
use hyper::client::connect::Connect;
|
||||
use hyper::Body;
|
||||
use rvstruct::ValueStruct;
|
||||
use std::future::Future;
|
||||
use std::sync::Arc;
|
||||
use tracing::*;
|
||||
|
||||
use crate::api::*;
|
||||
use crate::errors::*;
|
||||
use crate::hyper_tokio::SlackClientHyperConnector;
|
||||
use crate::{AnyStdResult, SlackClientHttpApiUri};
|
||||
|
||||
impl<H: 'static + Send + Sync + Connect + Clone> SlackEventsAxumListener<H> {
|
||||
pub fn slack_oauth_install(
|
||||
&self,
|
||||
config: &SlackOAuthListenerConfig,
|
||||
) -> impl Fn(Request<Body>) -> BoxFuture<'static, Response<Body>> + 'static + Send + Clone {
|
||||
let environment = self.environment.clone();
|
||||
let config = config.clone();
|
||||
move |_| {
|
||||
let config = config.clone();
|
||||
let environment = environment.clone();
|
||||
async move {
|
||||
let full_uri = SlackClientHttpApiUri::create_url_with_params(
|
||||
SlackOAuthListenerConfig::OAUTH_AUTHORIZE_URL_VALUE,
|
||||
&vec![
|
||||
("client_id", Some(config.client_id.value())),
|
||||
("scope", Some(&config.bot_scope)),
|
||||
(
|
||||
"redirect_uri",
|
||||
Some(config.to_redirect_url()?.as_str().to_string()).as_ref(),
|
||||
),
|
||||
],
|
||||
);
|
||||
debug!("Redirecting to Slack OAuth authorize: {}", &full_uri);
|
||||
HyperExtensions::hyper_redirect_to(&full_uri.to_string())
|
||||
}
|
||||
.map(|res| Self::handle_error(environment, res))
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slack_oauth_callback(
|
||||
&self,
|
||||
config: &SlackOAuthListenerConfig,
|
||||
install_service_fn: UserCallbackFunction<
|
||||
SlackOAuthV2AccessTokenResponse,
|
||||
impl Future<Output = ()> + 'static + Send,
|
||||
SlackClientHyperConnector<H>,
|
||||
>,
|
||||
) -> impl Fn(Request<Body>) -> BoxFuture<'static, Response<Body>> + 'static + Send + Clone {
|
||||
let environment = self.environment.clone();
|
||||
let config = config.clone();
|
||||
move |req| {
|
||||
let config = config.clone();
|
||||
let environment = environment.clone();
|
||||
let err_environment = environment.clone();
|
||||
let err_config = config.clone();
|
||||
|
||||
async move {
|
||||
let params = HyperExtensions::parse_query_params(&req);
|
||||
debug!("Received Slack OAuth callback: {:?}", ¶ms);
|
||||
|
||||
match (params.get("code"), params.get("error")) {
|
||||
(Some(code), None) => {
|
||||
let oauth_access_resp = environment
|
||||
.client
|
||||
.oauth2_access(
|
||||
&SlackOAuthV2AccessTokenRequest::from(
|
||||
SlackOAuthV2AccessTokenRequestInit {
|
||||
client_id: config.client_id.clone(),
|
||||
client_secret: config.client_secret.clone(),
|
||||
code: code.into(),
|
||||
},
|
||||
)
|
||||
.with_redirect_uri(config.to_redirect_url()?),
|
||||
)
|
||||
.await;
|
||||
|
||||
match oauth_access_resp {
|
||||
Ok(oauth_resp) => {
|
||||
info!(
|
||||
"Received slack OAuth access resp for: {} / {} / {}",
|
||||
&oauth_resp.team.id,
|
||||
&oauth_resp
|
||||
.team
|
||||
.name
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "".into()),
|
||||
&oauth_resp.authed_user.id
|
||||
);
|
||||
install_service_fn(
|
||||
oauth_resp,
|
||||
environment.client.clone(),
|
||||
environment.user_state.clone(),
|
||||
)
|
||||
.await;
|
||||
HyperExtensions::hyper_redirect_to(&config.redirect_installed_url)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Slack OAuth error: {}", &err);
|
||||
(environment.clone().error_handler)(
|
||||
Box::new(err),
|
||||
environment.client.clone(),
|
||||
environment.user_state.clone(),
|
||||
);
|
||||
HyperExtensions::hyper_redirect_to(
|
||||
&config.redirect_error_redirect_url,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
(None, Some(err)) => {
|
||||
info!("Slack OAuth cancelled with the reason: {}", err);
|
||||
(environment.error_handler)(
|
||||
Box::new(SlackClientError::ApiError(SlackClientApiError::new(
|
||||
err.clone(),
|
||||
))),
|
||||
environment.client.clone(),
|
||||
environment.user_state.clone(),
|
||||
);
|
||||
let redirect_error_url = format!(
|
||||
"{}{}",
|
||||
&config.redirect_error_redirect_url,
|
||||
req.uri().query().map_or("".into(), |q| format!("?{}", &q))
|
||||
);
|
||||
HyperExtensions::hyper_redirect_to(&redirect_error_url)
|
||||
}
|
||||
_ => {
|
||||
error!("Slack OAuth cancelled with unknown reason");
|
||||
(environment.error_handler)(
|
||||
Box::new(SlackClientError::SystemError(
|
||||
SlackClientSystemError::new()
|
||||
.with_message("OAuth cancelled with unknown reason".into()),
|
||||
)),
|
||||
environment.client.clone(),
|
||||
environment.user_state.clone(),
|
||||
);
|
||||
HyperExtensions::hyper_redirect_to(&config.redirect_error_redirect_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
.map(move |res| match res {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
error!("Slack OAuth system error: {}", err);
|
||||
(err_environment.error_handler)(
|
||||
Box::new(SlackClientError::SystemError(
|
||||
SlackClientSystemError::new()
|
||||
.with_message(format!("OAuth cancelled system error: {}", err)),
|
||||
)),
|
||||
err_environment.client.clone(),
|
||||
err_environment.user_state.clone(),
|
||||
);
|
||||
HyperExtensions::hyper_redirect_to(&err_config.redirect_error_redirect_url)
|
||||
.unwrap()
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn oauth_router(
|
||||
&self,
|
||||
root_path: &str,
|
||||
config: &SlackOAuthListenerConfig,
|
||||
install_service_fn: UserCallbackFunction<
|
||||
SlackOAuthV2AccessTokenResponse,
|
||||
impl Future<Output = ()> + 'static + Send,
|
||||
SlackClientHyperConnector<H>,
|
||||
>,
|
||||
) -> axum::routing::Router {
|
||||
axum::routing::Router::new()
|
||||
.route(
|
||||
config.install_path.replace(root_path, "").as_str(),
|
||||
axum::routing::get(self.slack_oauth_install(config)),
|
||||
)
|
||||
.route(
|
||||
config
|
||||
.redirect_callback_path
|
||||
.replace(root_path, "")
|
||||
.as_str(),
|
||||
axum::routing::get(self.slack_oauth_callback(config, install_service_fn)),
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_error(
|
||||
environment: Arc<SlackClientEventsListenerEnvironment<SlackClientHyperConnector<H>>>,
|
||||
result: AnyStdResult<Response<hyper::Body>>,
|
||||
) -> Response<hyper::Body> {
|
||||
match result {
|
||||
Err(err) => {
|
||||
let http_status = (environment.error_handler)(
|
||||
err,
|
||||
environment.client.clone(),
|
||||
environment.user_state.clone(),
|
||||
);
|
||||
Response::builder()
|
||||
.status(http_status)
|
||||
.body(hyper::Body::empty())
|
||||
.unwrap()
|
||||
}
|
||||
Ok(result) => result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -199,3 +199,15 @@ impl From<url::ParseError> for SlackClientError {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<dyn std::error::Error + Sync + Send>> for SlackClientError {
|
||||
fn from(err: Box<dyn Error + Sync + Send>) -> Self {
|
||||
SlackClientError::SystemError(SlackClientSystemError::new().with_cause(err))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_serde_error(err: serde_json::Error, tried_to_parse: Option<&str>) -> SlackClientError {
|
||||
SlackClientError::ProtocolError(
|
||||
SlackClientProtocolError::new(err).opt_json_body(tried_to_parse.map(|s| s.to_string())),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,17 @@
|
|||
use crate::errors::*;
|
||||
use crate::hyper_tokio::ratectl::SlackTokioRateController;
|
||||
use crate::models::{SlackClientId, SlackClientSecret};
|
||||
use crate::signature_verifier::SlackEventAbsentSignatureError;
|
||||
use crate::signature_verifier::SlackEventSignatureVerifier;
|
||||
use crate::*;
|
||||
use async_recursion::async_recursion;
|
||||
use bytes::Buf;
|
||||
use futures::future::TryFutureExt;
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use hyper::body::HttpBody;
|
||||
use hyper::client::*;
|
||||
use hyper::http::StatusCode;
|
||||
use hyper::{Body, Request, Response, Uri};
|
||||
use hyper::{Body, Request};
|
||||
use hyper_rustls::HttpsConnector;
|
||||
use mime::Mime;
|
||||
use rvstruct::ValueStruct;
|
||||
|
||||
use crate::prelude::hyper_ext::HyperExtensions;
|
||||
use crate::ratectl::{SlackApiMethodRateControlConfig, SlackApiRateControlConfig};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::*;
|
||||
|
|
@ -68,83 +61,6 @@ impl<H: 'static + Send + Sync + Clone + connect::Connect> SlackClientHyperConnec
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_query_params(request: &Request<Body>) -> HashMap<String, String> {
|
||||
request
|
||||
.uri()
|
||||
.query()
|
||||
.map(|v| {
|
||||
url::form_urlencoded::parse(v.as_bytes())
|
||||
.into_owned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(HashMap::new)
|
||||
}
|
||||
|
||||
pub(crate) fn hyper_redirect_to(
|
||||
url: &str,
|
||||
) -> Result<Response<Body>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Response::builder()
|
||||
.status(hyper::http::StatusCode::FOUND)
|
||||
.header(hyper::header::LOCATION, url)
|
||||
.body(Body::empty())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn setup_token_auth_header(
|
||||
request_builder: hyper::http::request::Builder,
|
||||
token: Option<&SlackApiToken>,
|
||||
) -> hyper::http::request::Builder {
|
||||
if token.is_none() {
|
||||
request_builder
|
||||
} else {
|
||||
let token_header_value = format!("Bearer {}", token.unwrap().token_value.value());
|
||||
request_builder.header(hyper::header::AUTHORIZATION, token_header_value)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn setup_basic_auth_header(
|
||||
request_builder: hyper::http::request::Builder,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> hyper::http::request::Builder {
|
||||
let header_value = format!(
|
||||
"Basic {}",
|
||||
base64::encode(format!("{}:{}", username, password))
|
||||
);
|
||||
request_builder.header(hyper::header::AUTHORIZATION, header_value)
|
||||
}
|
||||
|
||||
pub(crate) fn create_http_request(
|
||||
url: Url,
|
||||
method: hyper::http::Method,
|
||||
) -> hyper::http::request::Builder {
|
||||
let uri: Uri = url.as_str().parse().unwrap();
|
||||
hyper::http::request::Builder::new()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.header("accept-charset", "utf-8")
|
||||
}
|
||||
|
||||
async fn http_body_to_string<T>(body: T) -> AnyStdResult<String>
|
||||
where
|
||||
T: HttpBody,
|
||||
T::Error: std::error::Error + Sync + Send + 'static,
|
||||
{
|
||||
let http_body = hyper::body::aggregate(body).await?;
|
||||
let mut http_reader = http_body.reader();
|
||||
let mut http_body_str = String::new();
|
||||
http_reader.read_to_string(&mut http_body_str)?;
|
||||
Ok(http_body_str)
|
||||
}
|
||||
|
||||
fn http_response_content_type<RS>(response: &Response<RS>) -> Option<Mime> {
|
||||
let http_headers = response.headers();
|
||||
http_headers.get(hyper::header::CONTENT_TYPE).map(|hv| {
|
||||
let hvs = hv.to_str().unwrap();
|
||||
hvs.parse::<Mime>().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
async fn send_http_request<RS>(&self, request: Request<Body>) -> ClientResult<RS>
|
||||
where
|
||||
RS: for<'de> serde::de::Deserialize<'de>,
|
||||
|
|
@ -156,17 +72,11 @@ impl<H: 'static + Send + Sync + Clone + connect::Connect> SlackClientHyperConnec
|
|||
request.uri()
|
||||
);
|
||||
|
||||
let http_res = self
|
||||
.hyper_connector
|
||||
.request(request)
|
||||
.await
|
||||
.map_err(Self::map_http_error)?;
|
||||
let http_res = self.hyper_connector.request(request).await?;
|
||||
let http_status = http_res.status();
|
||||
let http_headers = http_res.headers().clone();
|
||||
let http_content_type = Self::http_response_content_type(&http_res);
|
||||
let http_body_str = Self::http_body_to_string(http_res)
|
||||
.map_err(Self::map_system_error)
|
||||
.await?;
|
||||
let http_content_type = HyperExtensions::http_response_content_type(&http_res);
|
||||
let http_body_str = HyperExtensions::http_body_to_string(http_res).await?;
|
||||
let http_content_is_json = http_content_type.iter().all(|response_mime| {
|
||||
response_mime.type_() == mime::APPLICATION && response_mime.subtype() == mime::JSON
|
||||
});
|
||||
|
|
@ -182,13 +92,11 @@ impl<H: 'static + Send + Sync + Clone + connect::Connect> SlackClientHyperConnec
|
|||
StatusCode::OK if http_content_is_json => {
|
||||
let slack_message: SlackEnvelopeMessage =
|
||||
serde_json::from_str(http_body_str.as_str())
|
||||
.map_err(|err| Self::map_serde_error(err, Some(http_body_str.as_str())))?;
|
||||
.map_err(|err| map_serde_error(err, Some(http_body_str.as_str())))?;
|
||||
match slack_message.error {
|
||||
None => {
|
||||
let decoded_body =
|
||||
serde_json::from_str(http_body_str.as_str()).map_err(|err| {
|
||||
Self::map_serde_error(err, Some(http_body_str.as_str()))
|
||||
})?;
|
||||
let decoded_body = serde_json::from_str(http_body_str.as_str())
|
||||
.map_err(|err| map_serde_error(err, Some(http_body_str.as_str())))?;
|
||||
Ok(decoded_body)
|
||||
}
|
||||
Some(slack_error) => Err(SlackClientError::ApiError(
|
||||
|
|
@ -200,12 +108,12 @@ impl<H: 'static + Send + Sync + Clone + connect::Connect> SlackClientHyperConnec
|
|||
}
|
||||
}
|
||||
StatusCode::OK | StatusCode::NO_CONTENT => {
|
||||
serde_json::from_str("{}").map_err(|err| Self::map_serde_error(err, Some("{}")))
|
||||
serde_json::from_str("{}").map_err(|err| map_serde_error(err, Some("{}")))
|
||||
}
|
||||
StatusCode::TOO_MANY_REQUESTS if http_content_is_json => {
|
||||
let slack_message: SlackEnvelopeMessage =
|
||||
serde_json::from_str(http_body_str.as_str())
|
||||
.map_err(|err| Self::map_serde_error(err, Some(http_body_str.as_str())))?;
|
||||
.map_err(|err| map_serde_error(err, Some(http_body_str.as_str())))?;
|
||||
|
||||
Err(SlackClientError::RateLimitError(
|
||||
SlackRateLimitError::new()
|
||||
|
|
@ -313,61 +221,6 @@ impl<H: 'static + Send + Sync + Clone + connect::Connect> SlackClientHyperConnec
|
|||
Ok(result) => Ok(result),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn decode_signed_response(
|
||||
req: Request<Body>,
|
||||
signature_verifier: &SlackEventSignatureVerifier,
|
||||
) -> AnyStdResult<String> {
|
||||
let headers = &req.headers().clone();
|
||||
let req_body = req.into_body();
|
||||
match (
|
||||
headers.get(SlackEventSignatureVerifier::SLACK_SIGNED_HASH_HEADER),
|
||||
headers.get(SlackEventSignatureVerifier::SLACK_SIGNED_TIMESTAMP),
|
||||
) {
|
||||
(Some(received_hash), Some(received_ts)) => {
|
||||
Self::http_body_to_string(req_body)
|
||||
.and_then(|body| async {
|
||||
signature_verifier
|
||||
.verify(
|
||||
received_hash.to_str().unwrap(),
|
||||
&body,
|
||||
received_ts.to_str().unwrap(),
|
||||
)
|
||||
.map(|_| body)
|
||||
.map_err(|e| e.into())
|
||||
})
|
||||
.await
|
||||
}
|
||||
_ => Err(Box::new(SlackEventAbsentSignatureError::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_http_error(hyper_err: hyper::Error) -> SlackClientError {
|
||||
SlackClientError::HttpProtocolError(
|
||||
SlackClientHttpProtocolError::new().with_cause(Box::new(hyper_err)),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn map_hyper_http_error(hyper_err: hyper::http::Error) -> SlackClientError {
|
||||
SlackClientError::HttpProtocolError(
|
||||
SlackClientHttpProtocolError::new().with_cause(Box::new(hyper_err)),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn map_serde_error(
|
||||
err: serde_json::Error,
|
||||
tried_to_parse: Option<&str>,
|
||||
) -> SlackClientError {
|
||||
SlackClientError::ProtocolError(
|
||||
SlackClientProtocolError::new(err).opt_json_body(tried_to_parse.map(|s| s.to_string())),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn map_system_error(
|
||||
err: Box<dyn std::error::Error + Sync + Send>,
|
||||
) -> SlackClientError {
|
||||
SlackClientError::SystemError(SlackClientSystemError::new().with_cause(err))
|
||||
}
|
||||
}
|
||||
|
||||
impl<H: 'static + Send + Sync + Clone + connect::Connect> SlackClientHttpConnector
|
||||
|
|
@ -386,14 +239,15 @@ impl<H: 'static + Send + Sync + Clone + connect::Connect> SlackClientHttpConnect
|
|||
let body = self
|
||||
.send_rate_controlled_request(
|
||||
|| {
|
||||
let base_http_request =
|
||||
Self::create_http_request(full_uri.clone(), hyper::http::Method::GET);
|
||||
let base_http_request = HyperExtensions::create_http_request(
|
||||
full_uri.clone(),
|
||||
hyper::http::Method::GET,
|
||||
);
|
||||
|
||||
let http_request = Self::setup_token_auth_header(base_http_request, token);
|
||||
let http_request =
|
||||
HyperExtensions::setup_token_auth_header(base_http_request, token);
|
||||
|
||||
http_request
|
||||
.body(Body::empty())
|
||||
.map_err(Self::map_hyper_http_error)
|
||||
http_request.body(Body::empty()).map_err(|e| e.into())
|
||||
},
|
||||
token,
|
||||
rate_control_params,
|
||||
|
|
@ -419,13 +273,16 @@ impl<H: 'static + Send + Sync + Clone + connect::Connect> SlackClientHttpConnect
|
|||
async move {
|
||||
self.send_rate_controlled_request(
|
||||
|| {
|
||||
Self::setup_basic_auth_header(
|
||||
Self::create_http_request(full_uri.clone(), hyper::http::Method::GET),
|
||||
HyperExtensions::setup_basic_auth_header(
|
||||
HyperExtensions::create_http_request(
|
||||
full_uri.clone(),
|
||||
hyper::http::Method::GET,
|
||||
),
|
||||
client_id.value(),
|
||||
client_secret.value(),
|
||||
)
|
||||
.body(Body::empty())
|
||||
.map_err(Self::map_hyper_http_error)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
None,
|
||||
None,
|
||||
|
|
@ -449,21 +306,24 @@ impl<H: 'static + Send + Sync + Clone + connect::Connect> SlackClientHttpConnect
|
|||
RS: for<'de> serde::de::Deserialize<'de> + Send + 'a + Send + 'a,
|
||||
{
|
||||
async move {
|
||||
let post_json = serde_json::to_string(&request_body)
|
||||
.map_err(|err| Self::map_serde_error(err, None))?;
|
||||
let post_json =
|
||||
serde_json::to_string(&request_body).map_err(|err| map_serde_error(err, None))?;
|
||||
|
||||
let response_body = self
|
||||
.send_rate_controlled_request(
|
||||
|| {
|
||||
let base_http_request =
|
||||
Self::create_http_request(full_uri.clone(), hyper::http::Method::POST)
|
||||
.header("content-type", "application/json; charset=utf-8");
|
||||
let base_http_request = HyperExtensions::create_http_request(
|
||||
full_uri.clone(),
|
||||
hyper::http::Method::POST,
|
||||
)
|
||||
.header("content-type", "application/json; charset=utf-8");
|
||||
|
||||
let http_request = Self::setup_token_auth_header(base_http_request, token);
|
||||
let http_request =
|
||||
HyperExtensions::setup_token_auth_header(base_http_request, token);
|
||||
|
||||
http_request
|
||||
.body(post_json.clone().into())
|
||||
.map_err(Self::map_hyper_http_error)
|
||||
.map_err(|e| e.into())
|
||||
},
|
||||
token,
|
||||
rate_control_params,
|
||||
|
|
|
|||
17
src/hyper_tokio/hyper_errors.rs
Normal file
17
src/hyper_tokio/hyper_errors.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
use crate::errors::*;
|
||||
|
||||
impl From<hyper::Error> for SlackClientError {
|
||||
fn from(hyper_err: hyper::Error) -> Self {
|
||||
SlackClientError::HttpProtocolError(
|
||||
SlackClientHttpProtocolError::new().with_cause(Box::new(hyper_err)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hyper::http::Error> for SlackClientError {
|
||||
fn from(hyper_err: hyper::http::Error) -> Self {
|
||||
SlackClientError::HttpProtocolError(
|
||||
SlackClientHttpProtocolError::new().with_cause(Box::new(hyper_err)),
|
||||
)
|
||||
}
|
||||
}
|
||||
121
src/hyper_tokio/hyper_ext.rs
Normal file
121
src/hyper_tokio/hyper_ext.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
use crate::signature_verifier::*;
|
||||
use crate::{AnyStdResult, SlackApiToken};
|
||||
use bytes::Buf;
|
||||
use futures_util::TryFutureExt;
|
||||
use http::{Request, Response, Uri};
|
||||
use hyper::body::HttpBody;
|
||||
use hyper::Body;
|
||||
use mime::Mime;
|
||||
use rvstruct::ValueStruct;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use url::Url;
|
||||
|
||||
pub struct HyperExtensions;
|
||||
|
||||
impl HyperExtensions {
|
||||
pub fn parse_query_params(request: &Request<Body>) -> HashMap<String, String> {
|
||||
request
|
||||
.uri()
|
||||
.query()
|
||||
.map(|v| {
|
||||
url::form_urlencoded::parse(v.as_bytes())
|
||||
.into_owned()
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_else(HashMap::new)
|
||||
}
|
||||
|
||||
pub fn hyper_redirect_to(
|
||||
url: &str,
|
||||
) -> Result<Response<Body>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
Response::builder()
|
||||
.status(hyper::http::StatusCode::FOUND)
|
||||
.header(hyper::header::LOCATION, url)
|
||||
.body(Body::empty())
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn setup_token_auth_header(
|
||||
request_builder: hyper::http::request::Builder,
|
||||
token: Option<&SlackApiToken>,
|
||||
) -> hyper::http::request::Builder {
|
||||
if token.is_none() {
|
||||
request_builder
|
||||
} else {
|
||||
let token_header_value = format!("Bearer {}", token.unwrap().token_value.value());
|
||||
request_builder.header(hyper::header::AUTHORIZATION, token_header_value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_basic_auth_header(
|
||||
request_builder: hyper::http::request::Builder,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> hyper::http::request::Builder {
|
||||
let header_value = format!(
|
||||
"Basic {}",
|
||||
base64::encode(format!("{}:{}", username, password))
|
||||
);
|
||||
request_builder.header(hyper::header::AUTHORIZATION, header_value)
|
||||
}
|
||||
|
||||
pub fn create_http_request(
|
||||
url: Url,
|
||||
method: hyper::http::Method,
|
||||
) -> hyper::http::request::Builder {
|
||||
let uri: Uri = url.as_str().parse().unwrap();
|
||||
hyper::http::request::Builder::new()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.header("accept-charset", "utf-8")
|
||||
}
|
||||
|
||||
pub async fn http_body_to_string<T>(body: T) -> AnyStdResult<String>
|
||||
where
|
||||
T: HttpBody,
|
||||
T::Error: std::error::Error + Sync + Send + 'static,
|
||||
{
|
||||
let http_body = hyper::body::aggregate(body).await?;
|
||||
let mut http_reader = http_body.reader();
|
||||
let mut http_body_str = String::new();
|
||||
http_reader.read_to_string(&mut http_body_str)?;
|
||||
Ok(http_body_str)
|
||||
}
|
||||
|
||||
pub fn http_response_content_type<RS>(response: &Response<RS>) -> Option<Mime> {
|
||||
let http_headers = response.headers();
|
||||
http_headers.get(hyper::header::CONTENT_TYPE).map(|hv| {
|
||||
let hvs = hv.to_str().unwrap();
|
||||
hvs.parse::<Mime>().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn decode_signed_response(
|
||||
req: Request<Body>,
|
||||
signature_verifier: &SlackEventSignatureVerifier,
|
||||
) -> AnyStdResult<String> {
|
||||
let headers = &req.headers().clone();
|
||||
let req_body = req.into_body();
|
||||
match (
|
||||
headers.get(SlackEventSignatureVerifier::SLACK_SIGNED_HASH_HEADER),
|
||||
headers.get(SlackEventSignatureVerifier::SLACK_SIGNED_TIMESTAMP),
|
||||
) {
|
||||
(Some(received_hash), Some(received_ts)) => {
|
||||
Self::http_body_to_string(req_body)
|
||||
.and_then(|body| async {
|
||||
signature_verifier
|
||||
.verify(
|
||||
received_hash.to_str().unwrap(),
|
||||
&body,
|
||||
received_ts.to_str().unwrap(),
|
||||
)
|
||||
.map(|_| body)
|
||||
.map_err(|e| e.into())
|
||||
})
|
||||
.await
|
||||
}
|
||||
_ => Err(Box::new(SlackEventAbsentSignatureError::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ use crate::hyper_tokio::connector::SlackClientHyperConnector;
|
|||
use crate::listener::*;
|
||||
use crate::signature_verifier::SlackEventSignatureVerifier;
|
||||
|
||||
use crate::hyper_tokio::hyper_ext::HyperExtensions;
|
||||
use crate::hyper_tokio::*;
|
||||
pub use crate::models::events::*;
|
||||
pub use crate::models::SlackResponseUrl;
|
||||
|
|
@ -57,7 +58,7 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
async move {
|
||||
match (req.method(), req.uri().path()) {
|
||||
(&Method::POST, url) if url == cfg.events_path => {
|
||||
SlackClientHyperConnector::<H>::decode_signed_response(req, &sign_verifier)
|
||||
HyperExtensions::decode_signed_response(req, &sign_verifier)
|
||||
.map_ok(|body| {
|
||||
let body_params: HashMap<String, String> =
|
||||
url::form_urlencoded::parse(body.as_bytes())
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ use crate::listener::*;
|
|||
pub use crate::models::events::*;
|
||||
use crate::signature_verifier::SlackEventSignatureVerifier;
|
||||
|
||||
use crate::blocks::SlackViewSubmissionResponse;
|
||||
use crate::hyper_tokio::hyper_ext::HyperExtensions;
|
||||
use crate::hyper_tokio::*;
|
||||
use futures::future::{BoxFuture, FutureExt, TryFutureExt};
|
||||
use hyper::body::*;
|
||||
|
|
@ -14,12 +16,12 @@ use std::future::Future;
|
|||
use std::sync::Arc;
|
||||
|
||||
impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<H> {
|
||||
pub fn interaction_events_service_fn<'a, D, F>(
|
||||
pub fn interaction_events_service_fn<'a, D, F, R>(
|
||||
&self,
|
||||
config: Arc<SlackInteractionEventsListenerConfig>,
|
||||
interaction_service_fn: UserCallbackFunction<
|
||||
SlackInteractionEvent,
|
||||
impl Future<Output = UserCallbackResult<()>> + 'static + Send,
|
||||
impl Future<Output = UserCallbackResult<R>> + 'static + Send,
|
||||
SlackClientHyperConnector<H>,
|
||||
>,
|
||||
) -> impl Fn(
|
||||
|
|
@ -37,6 +39,7 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
F: Future<Output = Result<Response<Body>, Box<dyn std::error::Error + Send + Sync + 'a>>>
|
||||
+ 'a
|
||||
+ Send,
|
||||
R: SlackInteractionEventResponse,
|
||||
{
|
||||
let signature_verifier: Arc<SlackEventSignatureVerifier> = Arc::new(
|
||||
SlackEventSignatureVerifier::new(&config.events_signing_secret),
|
||||
|
|
@ -55,7 +58,7 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
async move {
|
||||
match (req.method(), req.uri().path()) {
|
||||
(&Method::POST, url) if url == cfg.events_path => {
|
||||
SlackClientHyperConnector::<H>::decode_signed_response(req, &sign_verifier)
|
||||
HyperExtensions::decode_signed_response(req, &sign_verifier)
|
||||
.map_ok(|body| {
|
||||
let body_params: HashMap<String, String> =
|
||||
url::form_urlencoded::parse(body.as_bytes())
|
||||
|
|
@ -79,12 +82,9 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
.and_then(|event| async move {
|
||||
match event {
|
||||
Ok(view_submission_event@SlackInteractionEvent::ViewSubmission(_)) => {
|
||||
match interaction_service_fn(view_submission_event, sc.clone(), thread_user_state_storage.clone()).await {
|
||||
Ok(_) => {
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body("".into())
|
||||
.map_err(|e| e.into())
|
||||
match interaction_service_fn(view_submission_event.clone(), sc.clone(), thread_user_state_storage.clone()).await {
|
||||
Ok(response) => {
|
||||
response.to_http_response(&view_submission_event)
|
||||
}
|
||||
Err(err) => {
|
||||
let status_code = thread_error_handler(err, sc, thread_user_state_storage);
|
||||
|
|
@ -97,11 +97,8 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
|
||||
}
|
||||
Ok(interaction_event) => {
|
||||
match interaction_service_fn(interaction_event, sc.clone(), thread_user_state_storage.clone()).await {
|
||||
Ok(_) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::empty())
|
||||
.map_err(|e| e.into()),
|
||||
match interaction_service_fn(interaction_event.clone(), sc.clone(), thread_user_state_storage.clone()).await {
|
||||
Ok(response) => response.to_http_response(&interaction_event),
|
||||
Err(err) => {
|
||||
let status_code = thread_error_handler(err, sc, thread_user_state_storage);
|
||||
Response::builder()
|
||||
|
|
@ -129,3 +126,32 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SlackInteractionEventResponse {
|
||||
fn to_http_response(&self, event: &SlackInteractionEvent) -> AnyStdResult<Response<Body>>;
|
||||
}
|
||||
|
||||
impl SlackInteractionEventResponse for () {
|
||||
fn to_http_response(&self, event: &SlackInteractionEvent) -> AnyStdResult<Response<Body>> {
|
||||
match event {
|
||||
SlackInteractionEvent::ViewSubmission(_) => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body("".into())
|
||||
.map_err(|e| e.into()),
|
||||
_ => Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(Body::empty())
|
||||
.map_err(|e| e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlackInteractionEventResponse for SlackViewSubmissionResponse {
|
||||
fn to_http_response(&self, _event: &SlackInteractionEvent) -> AnyStdResult<Response<Body>> {
|
||||
let json_str = serde_json::to_string(&self)?;
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("content-type", "application/json; charset=utf-8")
|
||||
.body(Body::from(json_str))?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::errors::*;
|
|||
use crate::listener::*;
|
||||
use crate::{SlackClient, SlackClientHttpApiUri};
|
||||
|
||||
use crate::hyper_tokio::hyper_ext::HyperExtensions;
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use hyper::body::*;
|
||||
use hyper::client::connect::Connect;
|
||||
|
|
@ -16,7 +17,7 @@ use std::sync::Arc;
|
|||
use tracing::*;
|
||||
|
||||
impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<H> {
|
||||
async fn slack_oauth_install_service(
|
||||
pub(crate) async fn slack_oauth_install_service(
|
||||
_: Request<Body>,
|
||||
config: &SlackOAuthListenerConfig,
|
||||
) -> Result<Response<Body>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
|
|
@ -32,10 +33,10 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
],
|
||||
);
|
||||
debug!("Redirecting to Slack OAuth authorize: {}", &full_uri);
|
||||
SlackClientHyperConnector::<H>::hyper_redirect_to(&full_uri.to_string())
|
||||
HyperExtensions::hyper_redirect_to(&full_uri.to_string())
|
||||
}
|
||||
|
||||
async fn slack_oauth_callback_service(
|
||||
pub(crate) async fn slack_oauth_callback_service(
|
||||
req: Request<Body>,
|
||||
config: &SlackOAuthListenerConfig,
|
||||
client: Arc<SlackClient<SlackClientHyperConnector<H>>>,
|
||||
|
|
@ -47,7 +48,7 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
>,
|
||||
error_handler: BoxedErrorHandler<SlackClientHyperConnector<H>>,
|
||||
) -> Result<Response<Body>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let params = SlackClientHyperConnector::<H>::parse_query_params(&req);
|
||||
let params = HyperExtensions::parse_query_params(&req);
|
||||
debug!("Received Slack OAuth callback: {:?}", ¶ms);
|
||||
|
||||
match (params.get("code"), params.get("error")) {
|
||||
|
|
@ -77,16 +78,12 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
&oauth_resp.authed_user.id
|
||||
);
|
||||
install_service_fn(oauth_resp, client, user_state_storage).await;
|
||||
SlackClientHyperConnector::<H>::hyper_redirect_to(
|
||||
&config.redirect_installed_url,
|
||||
)
|
||||
HyperExtensions::hyper_redirect_to(&config.redirect_installed_url)
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Slack OAuth error: {}", &err);
|
||||
error_handler(Box::new(err), client, user_state_storage);
|
||||
SlackClientHyperConnector::<H>::hyper_redirect_to(
|
||||
&config.redirect_error_redirect_url,
|
||||
)
|
||||
HyperExtensions::hyper_redirect_to(&config.redirect_error_redirect_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,7 +101,7 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
&config.redirect_error_redirect_url,
|
||||
req.uri().query().map_or("".into(), |q| format!("?{}", &q))
|
||||
);
|
||||
SlackClientHyperConnector::<H>::hyper_redirect_to(&redirect_error_url)
|
||||
HyperExtensions::hyper_redirect_to(&redirect_error_url)
|
||||
}
|
||||
_ => {
|
||||
error!("Slack OAuth cancelled with unknown reason");
|
||||
|
|
@ -116,9 +113,7 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
client,
|
||||
user_state_storage,
|
||||
);
|
||||
SlackClientHyperConnector::<H>::hyper_redirect_to(
|
||||
&config.redirect_error_redirect_url,
|
||||
)
|
||||
HyperExtensions::hyper_redirect_to(&config.redirect_error_redirect_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::errors::*;
|
||||
use crate::hyper_tokio::connector::SlackClientHyperConnector;
|
||||
use crate::hyper_tokio::hyper_ext::HyperExtensions;
|
||||
use crate::hyper_tokio::*;
|
||||
use crate::listener::*;
|
||||
pub use crate::models::events::*;
|
||||
|
|
@ -53,7 +54,7 @@ impl<H: 'static + Send + Sync + Connect + Clone> SlackClientEventsHyperListener<
|
|||
async move {
|
||||
match (req.method(), req.uri().path()) {
|
||||
(&Method::POST, url) if url == cfg.events_path => {
|
||||
SlackClientHyperConnector::<H>::decode_signed_response(req, &sign_verifier)
|
||||
HyperExtensions::decode_signed_response(req, &sign_verifier)
|
||||
.map_ok(|body| {
|
||||
serde_json::from_str::<SlackPushEvent>(body.as_str()).map_err(|e| {
|
||||
SlackClientProtocolError::new(e)
|
||||
|
|
|
|||
|
|
@ -11,14 +11,22 @@ use crate::SlackClient;
|
|||
use crate::*;
|
||||
|
||||
pub mod connector;
|
||||
pub mod hyper_errors;
|
||||
pub(crate) mod hyper_ext;
|
||||
pub mod listener;
|
||||
mod ratectl;
|
||||
pub mod scroller_ext;
|
||||
mod socket_mode;
|
||||
|
||||
use crate::listener::SlackClientEventsListenerEnvironment;
|
||||
pub use listener::chain_service_routes_fn;
|
||||
pub use listener::SlackClientEventsHyperListener;
|
||||
pub use scroller_ext::SlackApiResponseScrollerExt;
|
||||
pub use socket_mode::*;
|
||||
|
||||
pub type SlackHyperClient = SlackClient<SlackClientHyperHttpsConnector>;
|
||||
|
||||
pub type SlackHyperListenerEnvironment =
|
||||
SlackClientEventsListenerEnvironment<SlackClientHyperHttpsConnector>;
|
||||
|
||||
pub type SlackHyperHttpsConnector = hyper_rustls::HttpsConnector<hyper::client::HttpConnector>;
|
||||
|
|
|
|||
|
|
@ -105,8 +105,7 @@ mod token;
|
|||
#[cfg(feature = "hyper")]
|
||||
pub mod hyper_tokio;
|
||||
|
||||
// In next releases
|
||||
// #[cfg(feature = "axum")]
|
||||
// pub mod axum_support;
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum_support;
|
||||
|
||||
pub mod prelude;
|
||||
|
|
|
|||
|
|
@ -97,3 +97,48 @@ pub struct SlackViewStateValueSelectedOption {
|
|||
pub text: SlackBlockPlainText,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "response_action")]
|
||||
pub enum SlackViewSubmissionResponse {
|
||||
#[serde(rename = "clear")]
|
||||
Clear(SlackViewSubmissionClearResponse),
|
||||
#[serde(rename = "update")]
|
||||
Update(SlackViewSubmissionUpdateResponse),
|
||||
#[serde(rename = "push")]
|
||||
Push(SlackViewSubmissionPushResponse),
|
||||
#[serde(rename = "errors")]
|
||||
Errors(SlackViewSubmissionErrorsResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct SlackViewSubmissionClearResponse {}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct SlackViewSubmissionUpdateResponse {
|
||||
pub view: SlackView,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct SlackViewSubmissionPushResponse {
|
||||
pub view: SlackView,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct SlackViewSubmissionErrorsResponse {
|
||||
pub errors: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_slack_view_submission_clear_response_serialization() {
|
||||
let output = serde_json::to_string(&SlackViewSubmissionResponse::Clear(
|
||||
SlackViewSubmissionClearResponse::new(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(output, r#"{"response_action":"clear"}"#);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,11 @@ mod authorization;
|
|||
mod command;
|
||||
mod interaction;
|
||||
mod push;
|
||||
mod view;
|
||||
|
||||
pub use authorization::*;
|
||||
pub use command::*;
|
||||
pub use interaction::*;
|
||||
pub use push::*;
|
||||
pub use view::*;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, ValueStruct)]
|
||||
pub struct SlackEventId(pub String);
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use rsb_derive::Builder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::blocks::SlackView;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "response_action")]
|
||||
pub enum SlackViewSubmissionResponse {
|
||||
#[serde(rename = "clear")]
|
||||
Clear(SlackViewSubmissionClearResponse),
|
||||
#[serde(rename = "update")]
|
||||
Update(SlackViewSubmissionUpdateResponse),
|
||||
#[serde(rename = "push")]
|
||||
Push(SlackViewSubmissionPushResponse),
|
||||
#[serde(rename = "errors")]
|
||||
Errors(SlackViewSubmissionErrorsResponse),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct SlackViewSubmissionClearResponse {}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct SlackViewSubmissionUpdateResponse {
|
||||
pub view: SlackView,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct SlackViewSubmissionPushResponse {
|
||||
pub view: SlackView,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
|
||||
pub struct SlackViewSubmissionErrorsResponse {
|
||||
pub errors: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_slack_view_submission_clear_response_serialization() {
|
||||
let output = serde_json::to_string(&SlackViewSubmissionResponse::Clear(
|
||||
SlackViewSubmissionClearResponse::new(),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(output, r#"{"response_action":"clear"}"#);
|
||||
}
|
||||
}
|
||||
|
|
@ -9,3 +9,6 @@ pub use crate::models::*; // common Slack models like SlackUser, etc and macros
|
|||
|
||||
#[cfg(feature = "hyper")]
|
||||
pub use crate::hyper_tokio::*;
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub use crate::axum_support::*;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue