Intro and motivation
Slack Morphism is a modern client library for Slack Web/Events API/Sockets Mode and Block Kit.
Type-safety
All of the models, API and Block Kit support in Slack Morphism are well-typed.
Easy to use
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 in asynchronous manner.
Modular design
This library provided as multiple modules:
slack-morphism-models
, gives you access to all type/models definitions that used for Slack Web/Events APIs.slack-morphism
, base module to support frameworks-agnostic client, that doesn't have any dependency to any HTTP/async library itself, and you can implement binding to any library you want.slack-morphism-hyper
, Slack client support/binding for Hyper/Tokio/Tungstenite.
Getting Started
Cargo.toml dependencies example:
[dependencies]
slack-morphism="0.37"
slack-morphism-models="0.37"
slack-morphism-hyper="0.37"
All imports you need:
use slack_morphism::prelude::*;
use slack_morphism_hyper::*;
or more granularly:
use slack_morphism::*; // access to network/client functions
use slack_morphism::api::*; // Slack Web API methods (chat, users, views, etc)
use slack_morphism::listener::*; // Slack Events API listener (routes) implementation
use slack_morphism_models::*; // common Slack models like SlackUser, etc and macros
use slack_morphism_models::blocks::*; // Slack Block Kit models
use slack_morphism_models::events::*; // Slack Events Models
use slack_morphism_hyper::*; // Hyper/Tokio client implementation
Ready to use examples
- Slack Web API client and Block kit example
- Events API server example
- Slack Web API client with Socket Mode
You can find them on github
Slack Web API client
Create a client instance
use slack_morphism::*;
// Slack Morphism Hyper/Tokio support
use slack_morphism_hyper::*;
let client = SlackClient::new( SlackClientHyperConnector::new() );
Make Web API methods calls
For most of Slack Web API methods (except for OAuth methods, Incoming Webhooks and event replies) you need a Slack token to make a call. For simple bots you can have it in your config files, or you can obtain workspace tokens using Slack OAuth.
In the example below, we’re using a hardcoded Slack token, but don’t do that for your production bots and apps. You should securely and properly store all of Slack tokens.
use slack_morphism::*;
use slack_morphism::api::*;
use slack_morphism_models::*;
// Slack Morphism Hyper/Tokio support
use slack_morphism_hyper::*;
async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = SlackClient::new(SlackClientHyperConnector::new());
// Create our Slack API token
let token_value: SlackApiTokenValue = "xoxb-89.....".into();
let token: SlackApiToken = SlackApiToken::new(token_value);
// Create a Slack session with this token
// A session is just a lightweight wrapper around your token
// not to specify it all the time for series of calls.
let session = client.open_session(&token);
// Make your first API call (which is `api.test` here)
let test: SlackApiTestResponse = session
.api_test(&SlackApiTestRequest::new().with_foo("Test".into()))
.await?;
// Send a simple text message
let post_chat_req =
SlackApiChatPostMessageRequest::new("#general".into(),
SlackMessageContent::new().with_text("Hey there!".into())
);
let post_chat_resp = session.chat_post_message(&post_chat_req).await?;
Ok(())
}
Pagination support
Some Web API methods defines cursors and pagination, to give you an ability to load a lot of data continually (using batching and continually making many requests).
Examples: conversations.history
, conversations.list
, users.list
, ...
To help with those methods Slack Morphism provides additional a “scroller” implementation, which deal with all scrolling/batching requests for you.
For example for users.list
:
use slack_morphism::*;
use slack_morphism::api::*;
use slack_morphism_models::*;
use slack_morphism_hyper::*;
use std::time::Duration;
async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let hyper_connector = SlackClientHyperConnector::new();
let client = SlackClient::new(hyper_connector);
let token_value: SlackApiTokenValue = "xoxb-89.....".into();
let token: SlackApiToken = SlackApiToken::new(token_value);
let session = client.open_session(&token);
// Create a first request and specify a batch limit:
let scroller_req: SlackApiUsersListRequest = SlackApiUsersListRequest::new().with_limit(5);
// Create a scroller from this request
let scroller = scroller_req.scroller();
// Option 1: Create a Rust Futures Stream from this scroller and use it
use futures::stream::BoxStream;
use futures::TryStreamExt;
let mut items_stream = scroller.to_items_stream(&session);
while let Some(items) = items_stream.try_next().await? {
println!("users batch: {:#?}", items);
}
// Option 2: Collect all of the data in a vector (which internally uses the same approach above)
// Only for Tokio/Hyper for now
let collected_members: Vec<SlackUser> = scroller
.collect_items_stream(&session, Duration::from_millis(1000))
.await?;
// Option 3: Throttling scrolling with Tokio/Hyper:
let mut items_throttled_stream =
scroller.to_items_throttled_stream(&session, Duration::from_millis(500));
while let Some(items) = items_throttled_stream.try_next().await? {
println!("res: {:#?}", items);
}
Ok(())
}
Block Kit support
To support Slack Block Kit rich messages and views, the library provides:
- Well-typed models
- Macros to help build blocks, block elements
Let’s take some very simple block example:
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "A message *with some bold text* and _some italicized text_."
}
}
]
}
Now, let’s look at how it looks with type-safe code using Slack Morphism Blocks macro support:
use slack_morphism_models::*;
use slack_morphism_models::blocks::*;
let blocks : Vec<SlackBlock> = slack_blocks![
some_into(
SlackSectionBlock::new()
.with_text(md!("A message *with some bold text* and _some italicized text_."))
)
];
Let’s look at another more complex example for welcoming message:
use slack_morphism::*;
use slack_morphism::api::*;
use slack_morphism_models::*;
use slack_morphism_models::blocks::*;
use slack_morphism_hyper::*;
async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use std::time::Duration;
use rsb_derive::Builder;
let hyper_connector = SlackClientHyperConnector::new();
let client = SlackClient::new(hyper_connector);
let token_value: SlackApiTokenValue = "xoxb-89.....".into();
let token: SlackApiToken = SlackApiToken::new(token_value);
let session = client.open_session(&token);
// Our welcome message params as a struct
#[derive(Debug, Clone, Builder)]
pub struct WelcomeMessageTemplateParams {
pub user_id: SlackUserId,
}
// Define our welcome message template using block macros, a trait and models from the library
impl SlackMessageTemplate for WelcomeMessageTemplateParams {
fn render_template(&self) -> SlackMessageContent {
SlackMessageContent::new()
.with_text(format!("Hey {}", self.user_id.to_slack_format()))
.with_blocks(slack_blocks![
some_into(
SlackSectionBlock::new()
.with_text(md!("Hey {}", self.user_id.to_slack_format()))
),
some_into(SlackDividerBlock::new()),
some_into(SlackImageBlock::new(
"https://www.gstatic.com/webp/gallery3/2_webp_ll.png".into(),
"Test Image".into()
)),
some_into(SlackHeaderBlock::new(
pt!("Simple header")
)),
some_into(SlackActionsBlock::new(slack_blocks![some_into(
SlackBlockButtonElement::new(
"simple-message-button".into(),
pt!("Simple button text")
)
)]))
])
}
}
// Use it
let message = WelcomeMessageTemplateParams::new("some-slack-uid".into());
let post_chat_req =
SlackApiChatPostMessageRequest::new("#general".into(), message.render_template());
Ok(())
}
Look other examples in examples/templates.rs.
Send Slack Webhook Messages
You can use client..post_webhook_message
to post Slack Incoming Webhook messages:
use slack_morphism::prelude::*;
use slack_morphism_hyper::*;
use url::Url;
let client = SlackClient::new(SlackClientHyperConnector::new());
// Your incoming webhook url from config or OAuth/events (ResponseURL)
let webhook_url: Url = Url::parse("https://hooks.slack.com/services/...")?;
client
.post_webhook_message(
&webhook_url,
&SlackApiPostWebhookMessageRequest::new(
SlackMessageContent::new()
.with_text(format!("Hey")),
),
)
.await?;
Different hyper connection types and proxy support
In some cases you may need to configure hyper connection types.
Common examples:
- Need to use a proxy server
- Have different initialisation for certs/TLS
To do that there is additional initialisation method in SlackClientHyperConnector
.
For example for proxy server config it might be used as:
let proxy = {
let https_connector = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.https_only()
.enable_http1()
.build();
let proxy_uri = "http://proxy.unfortunate.world.example.net:3128"
.parse()
.unwrap();
let proxy = Proxy::new(Intercept::Https, proxy_uri);
ProxyConnector::from_proxy(https_connector, proxy).unwrap()
};
let _client = SlackClient::new(
SlackClientHyperConnector::with_connector(proxy)
);
Rate control, throttling and retrying Slack API method requests
Enable rate control
Slack API defines rate limits to which all of your applications must follow.
By default, throttler isn't enabled, so you should enable it explicitly:
use slack_morphism::prelude::*;
use slack_morphism_hyper::*;
let client = SlackClient::new(
SlackClientHyperConnector::new()
.with_rate_control(
SlackApiRateControlConfig::new()
)
);
The example above creates a Slack API Client that follows the official rate limits from Slack. Because the Slack rate limits apply per workspaces (separately), to use throttling and limits properly you have to specify team id in tokens:
let token_value: SlackApiTokenValue = config_env_var("SLACK_TEST_TOKEN")?.into();
let team_id: SlackTeamId = config_env_var("SLACK_TEST_TEAM_ID")?.into();
let token: SlackApiToken = SlackApiToken::new(token_value).with_team_id(team_id);
let session = client.open_session(&token);
Rate control params
You can also customise rate control params using SlackApiRateControlConfig
:
- To global rate limit all APIs and for all teams use:
SlackApiRateControlConfig.global_max_rate_limit
. Default is not limited. - To rate limit all APIs and each team separately:
SlackApiRateControlConfig.team_max_rate_limit
. Default is not limited. - To change default tiers limits use
SlackApiRateControlConfig.tiers_limits
. Defaults are following the Slack recommendations (almost, there are slight differences to optimize bursting for Tier1).
Enable automatic retry for rate exceeded requests
To enable automatic retry of Slack Web API method requests,
you need to specify max_retries
in rate control params (default value is 0
):
let client = SlackClient::new(
SlackClientHyperConnector::new()
.with_rate_control(
SlackApiRateControlConfig::new().with_max_retries(5)
),
);
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. 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
use slack_morphism::api::*;
use slack_morphism::listener::*;
use slack_morphism::*;
use slack_morphism_models::*;
// Slack Morphism Hyper/Tokio support
use slack_morphism_hyper::*;
// 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(
std::env::var("SLACK_CLIENT_ID")?,
std::env::var("SLACK_CLIENT_SECRET")?,
std::env::var("SLACK_BOT_SCOPE")?,
std::env::var("SLACK_REDIRECT_HOST")?,
));
let push_events_config = Arc::new(SlackPushEventsListenerConfig::new(std::env::var(
"SLACK_SIGNING_SECRET",
)?));
let interactions_events_config = Arc::new(SlackInteractionEventsListenerConfig::new(
std::env::var("SLACK_SIGNING_SECRET")?,
));
let command_events_config = Arc::new(SlackCommandEventsListenerConfig::new(std::env::var(
"SLACK_SIGNING_SECRET",
)?));
// 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()
})
}
Also the library provides Slack events signature verifier (SlackEventSignatureVerifier
),
which is already integrated in the routes implementation for you and you don't need to use
it directly. All you need is provide your client id and secret configuration
to route implementation.
Look at the complete example here.
Slack Socket Mode support
Slack Morphism supports Slack Socket Mode starting with 0.10.x. Socket Mode allows your app to use the Events API and interactive components without exposing a public HTTP endpoint.
The mode is useful if you want to create an app that works with few workspaces and don't want to work with HTTP endpoints yourself.
Register your event callback functions
use slack_morphism::prelude::*;
use slack_morphism_hyper::*;
async fn test_interaction_events_function(
event: SlackInteractionEvent,
_client: Arc<SlackHyperClient>,
_states: SlackClientEventsUserState,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("{:#?}", event);
Ok(())
}
async fn test_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()),
))
}
async fn test_push_events_sm_function(
event: SlackPushEventCallback,
_client: Arc<SlackHyperClient>,
_states: SlackClientEventsUserState,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("{:#?}", event);
Ok(())
}
let client = Arc::new(SlackClient::new(SlackClientHyperConnector::new()));
let socket_mode_callbacks = SlackSocketModeListenerCallbacks::new()
.with_command_events(test_command_events_function)
.with_interaction_events(test_interaction_events_function)
.with_push_events(test_push_events_sm_function);
let listener_environment = Arc::new(
SlackClientEventsListenerEnvironment::new(client.clone())
);
let socket_mode_listener = SlackClientSocketModeListener::new(
&SlackClientSocketModeConfig::new(),
listener_environment.clone(),
socket_mode_callbacks,
);
Connect using socket mode to Slack
The following code initiates Web-sockets based connections to Slack endpoints using Slack Web methods and provided user token.
Slack Morphism supports multiple web-socket connections per one token to gracefully handle disconnects.
By default it uses 2 connections to one token. To configure it see SlackClientSocketModeConfig
;
// Need to specify App token for Socket Mode:
let app_token_value: SlackApiTokenValue =
config_env_var("SLACK_TEST_APP_TOKEN")?.into();
let app_token: SlackApiToken = SlackApiToken::new(app_token_value);
// Register an app token to listen for events,
socket_mode_listener.listen_for(&app_token).await?;
// Start WS connections calling Slack API to get WS url for the token,
// and wait for Ctrl-C to shutdown
// There are also `.start()`/`.shutdown()` available to manage manually
socket_mode_listener.serve().await;
Important caveats
The time blocking of the SM listener callbacks is important
If your app blocks callbacks more than 2-3 seconds Slack server may decide to repeat requests again and also to inform users with errors and timeouts. So, if you have something complex and time-consuming in your callbacks you should spawn your own future, e.g:
async fn test_push_events_sm_function(
event: SlackPushEventCallback,
_client: Arc<SlackHyperClient>,
_states: SlackClientEventsUserState,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
tokio::spawn(async move { process_message(client, event).await; });
Ok(())
}
Error handling function
It is highly recommended implementing your own error handling function:
fn test_error_handler(
err: Box<dyn std::error::Error + Send + Sync>,
_client: Arc<SlackHyperClient>,
_states: SlackClientEventsUserState,
) -> http::StatusCode {
println!("{:#?}", err);
// This return value should be OK if we want to return successful ack
// to the Slack server using Web-sockets
// https://api.slack.com/apis/connections/socket-implement#acknowledge
// so that Slack knows whether to retry
http::StatusCode::OK
}
// Register it:
let listener_environment = Arc::new(
SlackClientEventsListenerEnvironment::new(client.clone())
.with_error_handler(test_error_handler),
);
The implementation allows you:
- Return positive ack using http::StatusCode result / implement complex logic related to it. https://api.slack.com/apis/connections/socket-implement#acknowledge
- Increase visibility and observability in general when errors happen in your app and from Slack/library.
User state propagation for event listeners and callback functions
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
.
Defining user state
// Defining your state as a struct
struct UserStateExample(u64);
// Initializing it in listener environment:
let listener_environment = Arc::new(
SlackClientEventsListenerEnvironment::new(client.clone())
.with_error_handler(test_error_handler)
.with_user_state(UserStateExample(555)),
);
Reading user state in listeners
async fn test_push_events_function(
event: SlackPushEvent,
client: Arc<SlackHyperClient>,
user_state_storage: SlackClientEventsUserState,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let states = user_state_storage.read().await;
let user_state: Option<&UserStateExample> =
states.get_user_state::<UserStateExample>();
Ok(())
}
Updating user state in listeners
async fn test_push_events_function(
event: SlackPushEvent,
client: Arc<SlackHyperClient>,
user_state_storage: SlackClientEventsUserState,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let states = user_state_storage.write().await;
states.set_user_state(UserStateExample(555));
Ok(())
}
Limitations
Slack Morphism doesn't provide:
- RTM API (the usage of which is slowly declining in favour of Events API)
- Legacy Web/Events API methods and models (like Slack Message attachments, which should be replaced with Slack Blocks)