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:
Abdulla Abdurakhmanov 2022-08-06 15:23:38 +02:00 committed by GitHub
parent 29d923e919
commit 5f8f3eb9a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1407 additions and 458 deletions

View file

@ -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"]

View file

@ -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

View file

@ -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
View 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)

View 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)

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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(

View 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(())
}

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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)?;

View file

@ -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;

View 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(())
}
}

View 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)
}
}

View 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: {:?}", &params);
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,
}
}
}

View file

@ -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())),
)
}

View file

@ -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,

View 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)),
)
}
}

View 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())),
}
}
}

View file

@ -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())

View file

@ -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))?)
}
}

View file

@ -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: {:?}", &params);
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)
}
}
}

View file

@ -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)

View file

@ -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>;

View file

@ -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;

View file

@ -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"}"#);
}
}

View file

@ -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);

View file

@ -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"}"#);
}
}

View file

@ -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::*;