Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 the latest Rust async/await language features and libraries, the library provides access to all of the functions in asynchronous manner.

Modular design

Base crate to support frameworks-agnostic client and that doesn’t have any dependency to any HTTP/async library itself, and you can implement binding to any library you want. 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 support.
  • obsolete-chrono: deprecated. Keeps the date/time types backed by chrono instead of jiff to ease migration.

By default, hyper and axum features use rustls-native-certs and hyper-rustls/ring setup for TLS configuration, but you can switch off default-features and use hyper-base and axum-base features and have your own TLS configuration.

Getting Started

Getting Started

Cargo.toml dependencies example:

[dependencies]
slack-morphism = { version = "2", features = ["hyper", "axum"] }

All imports you need:

use slack_morphism::prelude::*;

Ready to use examples

  • Slack Web API client and Block kit 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

Slack Web API client

Create a client instance

use slack_morphism::prelude::*;

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

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

Note that session is just an auxiliary lightweight structure that stores references to the token and the client to make easier to have series of calls for the same token. It doesn’t make any network calls. There is no need to store it.

Another option is to use session is to use function run_in_session:

    // Sessions are lightweight and basically just a reference to client and token
    client
        .run_in_session(&token, |session| async move {
            let test: SlackApiTestResponse = session
                .api_test(&SlackApiTestRequest::new().with_foo("Test".into()))
                .await?;

            println!("{:#?}", test);

            let auth_test = session.auth_test().await?;
            println!("{:#?}", auth_test);

            Ok(())
        })
        .await?;

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

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_util::stream::BoxStream;
    use futures_util::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

Slack Block Kit messages and views are JSON documents built from a fixed set of block and element types. This library models each of those types as a Rust struct or enum, and provides a small set of macros — slack_blocks!, md!, pt! — to build the Vec of blocks or elements each block-holding type expects, without writing .into() on every item by hand.

Everything below is real, compiling code. Run it yourself from examples/blocks_showcase.rs, or copy any snippet directly.

From JSON to Rust

Take a very simple block, as Slack’s Block Kit Builder shows it:

{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "A message *with some bold text* and _some italicized text_."
      }
    }
  ]
}

The same block in type-safe Rust is one struct per JSON object, with the type tag implied by the struct, and md! standing in for the mrkdwn text object:

use slack_morphism::prelude::*;

let blocks: Vec<SlackBlock> = slack_blocks![
    SlackSectionBlock::new()
        .with_text(md!("A message *with some bold text* and _some italicized text_.")),
];

Every block, element and composition object follows this pattern: a JSON object becomes SlackXxx::new(required fields), each optional key becomes a .with_key(..) call, and a JSON array of blocks or elements becomes a slack_blocks![..] list.

Quick start

The smallest possible message is a single section block with some markdown text, wrapped in a SlackMessageContent and posted with SlackApiChatPostMessageRequest:

use slack_morphism::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = SlackClient::new(SlackClientHyperConnector::new()?);
let token: SlackApiToken = SlackApiToken::new("xoxb-your-token".into());
let session = client.open_session(&token);

let blocks: Vec<SlackBlock> = slack_blocks![
    SlackSectionBlock::new().with_text(md!("A message *with some bold text*.")),
];

let content = SlackMessageContent::new().with_blocks(blocks);
let request = SlackApiChatPostMessageRequest::new("#general".into(), content);

session.chat_post_message(&request).await?;
Ok(())
}

The rest of this page builds up from here: how text objects work, how slack_blocks! builds lists, and how each block and element family is constructed.

Text objects

Block Kit has two flavors of text: mrkdwn (Slack’s own markdown) and plain_text. The library models them as SlackBlockMarkDownText and SlackBlockPlainText, and the md! and pt! macros build them:

use slack_morphism::prelude::*;

let markdown: SlackBlockMarkDownText = md!("A message *with some bold text*.");
let plain: SlackBlockPlainText = pt!("Plain text");

// Both macros format like `format!` when given more than one argument.
let user_id = SlackUserId("U1234".into());
let greeting: SlackBlockMarkDownText = md!("Hey {}, welcome!", user_id.to_slack_format());

md! and pt! convert into whatever type the call site expects via Into, so the same macro call can produce a SlackBlockText, a SlackContextBlockElement, or the raw SlackBlockMarkDownText/ SlackBlockPlainText, depending on where it is used.

Some fields require plain text specifically and reject markdown at the API level: block headers, button labels, and input labels are typed as SlackBlockPlainTextOnly, not SlackBlockText. Passing markdown there does not fail to compile — a plain string still converts — but Slack rejects the request at runtime with invalid_blocks if the string contains markdown syntax Slack cannot render as plain text. Use pt!, or a bare string literal, for these fields.

Building block lists

slack_blocks! builds a Vec<T> from a comma-separated list, converting each item with .into(). Any block or element type that has a From (or Into) conversion to the target item type can appear as a bare item:

use slack_morphism::prelude::*;

let show_divider = true;

let blocks: Vec<SlackBlock> = slack_blocks![
    SlackHeaderBlock::new(pt!("Weekly report")),
    optionally(show_divider => SlackDividerBlock::new()),
    ..(1..=3).map(|n| SlackSectionBlock::new().with_text(md!("Item {}", n))),
];

assert_eq!(blocks.len(), 5);
  • A bare expression is pushed via .into().
  • optionally(pred => item) converts and pushes item the same way a bare item does, only when pred is true; neither pred nor item is evaluated when it is false.
  • ..iter splices every element of an iterator or Vec in, each converted via .into() — useful for building a run of blocks from data instead of writing them out by hand, or for spreading in blocks assembled elsewhere, as in Templates below.

These forms mix freely in one list. An older some(item) / some_into(item) / optionally_into(pred => item) syntax still works and appears throughout older code and examples; prefer the bare form above in new code.

One limit worth knowing up front: a bare md!(..) or pt!(..) call cannot be a list item directly, because its expansion already ends in an untyped .into() and the compiler has nothing left to infer the item type from (E0283). This only matters for a list whose items are text objects themselves, such as a context block’s elements or a section’s fields — give the list an explicit element type instead, as in SlackContextBlock::new(vec![md!("hi")]) or .with_fields(vec![md!("hi")]), both used below.

Sections

A section block carries a text object, optional fields (short text/value pairs laid out in a grid), and an optional accessory element:

use slack_morphism::prelude::*;

let section = SlackSectionBlock::new()
    .with_text(md!("*Deploy status*: all systems green"))
    .with_fields(vec![md!("*Region:*\nus-east-1"), md!("*Duration:*\n42s")])
    .with_accessory(
        SlackBlockButtonElement::new("view-details".into(), pt!("Details")).into(),
    );

let block: SlackBlock = section.into();

Actions and inputs

Action blocks hold interactive elements — buttons, selects, pickers. Input blocks pair one element with a required label, for use in modals.

use slack_morphism::prelude::*;

let approve = SlackBlockButtonElement::new("approve".into(), pt!("Approve"))
    .with_style(SlackBlockButtonStyle::Primary);
let deny = SlackBlockButtonElement::new("deny".into(), pt!("Deny"))
    .with_style(SlackBlockButtonStyle::Danger);

let actions: SlackBlock = SlackActionsBlock::new(slack_blocks![approve, deny]).into();

let region_select = SlackBlockStaticSelectElement::new("region".into())
    .with_placeholder(pt!("Choose a region"))
    .with_options(vec![
        SlackBlockChoiceItem::new("US East".into(), "us-east-1".into()),
        SlackBlockChoiceItem::new("EU West".into(), "eu-west-1".into()),
    ]);

let date_picker = SlackBlockDatePickerElement::new("deploy-date".into())
    .with_placeholder(pt!("Pick a date"));

let date_input: SlackBlock =
    SlackInputBlock::new("Deploy date".into(), date_picker.into()).into();

let _ = (actions, region_select, date_input);

Context

Context blocks hold a small run of text and image elements, usually shown in a muted style below other content. As noted above, its element list needs an explicit element type, so bare md!/pt! calls work directly inside vec![...] without going through slack_blocks!:

use slack_morphism::prelude::*;

let context: SlackBlock =
    SlackContextBlock::new(vec![md!("Posted by "), pt!("the release bot")]).into();

Rich text

Rich text blocks are Slack’s structured formatting model: sections made of inline runs (text, links, users, emoji, …), lists, quotes, and preformatted code. A bare string converts into an unstyled text run wherever an inline element is expected, and SlackRichTextText has .bold(), .italic(), .strike(), and .code() helpers for styled runs:

use slack_morphism::prelude::*;

let section: SlackRichTextElement = SlackRichTextSection::new(vec![
    "Build ".into(),
    SlackRichTextText::new("passed".to_string()).bold().into(),
    " ".into(),
    SlackRichTextEmoji::new(SlackEmojiName("white_check_mark".into())).into(),
])
.into();

let list: SlackRichTextElement = SlackRichTextList::new(
    SlackRichTextListStyle::Bullet,
    vec!["Unit tests".into(), "Integration tests".into()],
)
.into();

let quote: SlackRichTextElement =
    SlackRichTextQuote::new(vec!["Ship it.".into()]).into();

let preformatted: SlackRichTextElement =
    SlackRichTextPreformatted::new(vec!["cargo test --doc".into()]).into();

let rich_text: SlackBlock =
    SlackRichTextBlock::new(vec![section, list, quote, preformatted]).into();

The same bare-string conversion applies inside a SlackRichTextList’s items and a rich text table cell, below.

Tables

Table blocks are a Vec<Vec<SlackTableCell>>. A bare string becomes a raw-text cell; a SlackTableRichTextCell holds rich text elements the same way a rich text block does:

use slack_morphism::prelude::*;

let table: SlackBlock = SlackTableBlock::new(vec![
    vec!["Service".into(), "Status".into()],
    vec![
        "api".into(),
        SlackTableRichTextCell::new(vec![SlackRichTextSection::new(vec![
            SlackRichTextText::new("healthy".to_string()).bold().into(),
        ])
        .into()])
        .into(),
    ],
])
.into();

Templates

SlackMessageTemplate renders a full SlackMessageContent; SlackBlocksTemplate renders just a Vec<SlackBlock> meant to be spread into a larger message with ... A typical template takes its parameters as a struct built with rsb_derive::Builder:

use slack_morphism::prelude::*;
use rsb_derive::Builder;

#[derive(Debug, Clone, Builder)]
struct DeploySummaryParams {
    service: String,
    ok: bool,
}

impl SlackBlocksTemplate for DeploySummaryParams {
    fn render_template(&self) -> Vec<SlackBlock> {
        slack_blocks![
            SlackDividerBlock::new(),
            SlackContextBlock::new(vec![md!("Deployed by the release bot")]),
        ]
    }
}

#[derive(Debug, Clone, Builder)]
struct WelcomeMessageParams {
    user_id: SlackUserId,
}

impl SlackMessageTemplate for WelcomeMessageParams {
    fn render_template(&self) -> SlackMessageContent {
        let footer = DeploySummaryParams::new("api".into(), true);

        SlackMessageContent::new()
            .with_text(format!("Hey {}", self.user_id.to_slack_format()))
            .with_blocks(slack_blocks![
                SlackSectionBlock::new()
                    .with_text(md!("Hey {}, welcome!", self.user_id.to_slack_format())),
                ..footer.render_template(),
            ])
    }
}

let message = WelcomeMessageParams::new("U1234".into());
let content = message.render_template();
assert_eq!(content.blocks.map(|b| b.len()), Some(3));

Views and modals

A modal is a SlackModalView: a title, a list of blocks (typically inputs), and optional submit/close button text. Opening one requires a trigger_id from the interaction that triggered it — a button click or a shortcut invocation, not something you can obtain outside of a live request. Publishing a home tab view uses views.publish and a user id instead:

use slack_morphism::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let client = SlackClient::new(SlackClientHyperConnector::new()?);
let token: SlackApiToken = SlackApiToken::new("xoxb-your-token".into());
let session = client.open_session(&token);

let name_input = SlackBlockPlainTextInputElement::new("name".into());

let modal = SlackModalView::new(
    "Deploy".into(),
    vec![SlackInputBlock::new("Service name".into(), name_input.into()).into()],
)
.with_submit(pt!("Deploy"));

let open_request = SlackApiViewsOpenRequest::new(
    SlackTriggerId("123.456.abcdef".into()),
    SlackView::Modal(modal),
);
session.views_open(&open_request).await?;

let home = SlackHomeView::new(vec![SlackSectionBlock::new()
    .with_text(md!("Welcome to your home tab."))
    .into()]);

let publish_request =
    SlackApiViewsPublishRequest::new(SlackUserId("U1234".into()), SlackView::Home(home));
session.views_publish(&publish_request).await?;
Ok(())
}

Troubleshooting invalid_blocks

Slack validates blocks server-side and returns invalid_blocks with little detail beyond that. The usual causes:

  • Markdown where plain text is required. Header text, button labels, input labels, and similar SlackBlockPlainTextOnly fields do not render mrkdwn syntax; passing text with *bold* or a link in one of them either shows the literal asterisks or is rejected outright, depending on the field.
  • Text over 3000 characters. Section and context text objects have a 3000-character limit; button and option text has a much smaller one (75 characters for a button label).
  • More than 50 blocks in one message, or more than 100 in a modal or home tab.
  • A missing or duplicate action_id. Every interactive element needs an action_id unique within its view or message; the button, select, and input constructors above all take it as their first argument for this reason.

When none of the above explains it, the fastest way to isolate a bad block is to paste the JSON from Block Kit Builder straight into serde_json::from_str::<Vec<SlackBlock>>, confirm it deserializes and matches what you expected, then compare it field by field against the blocks you built:

use slack_morphism::prelude::*;

let payload = r#"[
    {
        "type": "section",
        "text": { "type": "mrkdwn", "text": "A message *with some bold text*." }
    }
]"#;

let blocks: Vec<SlackBlock> = serde_json::from_str(payload)?;
assert_eq!(blocks.len(), 1);
Ok::<(), serde_json::Error>(())

Convert Block Kit Builder JSON

Paste the JSON from Block Kit Builder below and press Convert (or Ctrl+Enter) to get the equivalent builder code. The converter runs entirely in your browser: nothing is uploaded anywhere. It accepts a Builder message ({"blocks": [...]}), a bare block array, a single block, or a modal or home view; anything the crate cannot model is reported as an error rather than guessed at.

The converter runs on the documentation site; open this page there to use it.

The same conversion is available as a command-line tool:

cargo run --manifest-path tools/blockkit-to-rust/Cargo.toml --bin bk2rs < message.json

More examples

examples/blocks_showcase.rs posts one message per block family covered on this page and prints a modal view. examples/client.rs shows a complete, runnable client setup including authentication. examples/socket_mode.rs shows handling interactions (button clicks, view submissions) that come back from the blocks you post.

Send Slack Webhook Messages

You can use client..post_webhook_message to post Slack Incoming Webhook messages:


use slack_morphism::prelude::*;
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)
    );
    

Please note that this configuration available only for Slack Client, and doesn’t work for Socket Mode (WS) mode.

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

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

Observability and tracing

The library uses popular tracing crate for logs and distributed traces (spans). To improve observability for your specific cases, additionally to the fields provided by library, you can inject your own trace fields:

use slack_morphism::prelude::*;
use tracing::*;

// While Team ID is optional but still useful for tracing and rate control purposes
let token: SlackApiToken =
    SlackApiToken::new(token_value).with_team_id(config_env_var("SLACK_TEST_TEAM_ID")?.into());

// Let's create our own user specific span first
let my_custom_span = span!(Level::DEBUG, "My scope", my_scope_attr = "my-scope-value");
debug!("Testing tracing abilities");

// Sessions are lightweight and basically just a reference to client and token
client
    .run_in_session(&token, |session| async move {
        let test: SlackApiTestResponse = session
            .api_test(&SlackApiTestRequest::new().with_foo("Test".into()))
            .await?;
        println!("{:#?}", test);

        let auth_test = session.auth_test().await?;
        println!("{:#?}", auth_test);

        Ok(())
    })
    .instrument(my_custom_span.or_current())
    .await

Events API and OAuth

The library provides two different ways to work with Slack Events API:

Testing with ngrok

For development/testing purposes you can use ngrok:

ngrok http 8080

and copy the URL it gives for you to the example parameters for SLACK_REDIRECT_HOST.

Example testing with ngrok:

SLACK_CLIENT_ID=<your-client-id> \
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 --all-features

Slack Signature Verifier

The library provides Slack events signature verifier (SlackEventSignatureVerifier), which is already integrated in the OAuth 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.

In case you’re embedding the library into your own Web/routes-framework, you can use it separately.

Events API and OAuth for Hyper

The library provides routes and middleware implementation in SlackClientEventsListener for:

  • Push Events
  • Interaction Events
  • Command Events
  • OAuth v2 redirects and client functions nested router

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.

Example


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.
    // Returning `SlackInteractionResponse` lets one handler answer every kind of
    // interaction: options for `block_suggestion`, a `response_action` for
    // `view_submission`, and a plain acknowledgement for everything else.
    async fn slack_interaction_events_function(event: SlackInteractionEvent, 
        _client: Arc<SlackHyperClient>,
        _states: SlackClientEventsUserState
    ) -> Result<SlackInteractionResponse, Box<dyn std::error::Error + Send + Sync>> {
        println!("{:#?}", event);

        Ok(SlackInteractionResponse::Empty)
    }

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

Options Load URL / block_suggestion

When a user types into an external_select or multi_external_select menu, Slack sends a block_suggestion payload to the app’s Options Load URL. Point that URL at the same /interaction route as the Interactivity Request URL and answer the event with SlackBlockSuggestionResponse:

async fn slack_interaction_events_function(
    event: SlackInteractionEvent,
    _client: Arc<SlackHyperClient>,
    _states: SlackClientEventsUserState,
) -> Result<SlackInteractionResponse, Box<dyn std::error::Error + Send + Sync>> {
    match event {
        SlackInteractionEvent::BlockSuggestion(suggestion_event) => {
            // `suggestion_event.value` is what the user has typed so far
            Ok(SlackBlockSuggestionResponse::Options(SlackBlockSuggestionOptions::new(vec![
                SlackBlockChoiceItem::new(pt!("Unexpected sentience"), "AI-2323".to_string())
                    .with_description(pt!("Issue AI-2323")),
            ]))
            .into())
        }
        _ => Ok(SlackInteractionResponse::Empty),
    }
}

Slack limits the reply to 100 options, or to 100 option groups (SlackBlockSuggestionResponse::OptionGroups) of 100 options each, and only plain_text is allowed in the option text and description.

Complete example look at github

Events API and OAuth for Axum

The library provides route implementation in SlackEventsAxumListener based on Hyper/Tokio for:

  • Push Events
  • Interaction Events
  • Command Events
  • OAuth v2 redirects and client functions

Example


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

// `SlackInteractionResponse` implements `axum::response::IntoResponse`, so one handler
// can answer every kind of interaction: options for `block_suggestion`, a
// `response_action` for `view_submission`, and an empty 200 for everything else.
async fn test_interaction_event(
    Extension(_environment): Extension<Arc<SlackHyperListenerEnvironment>>,
    Extension(event): Extension<SlackInteractionEvent>,
) -> SlackInteractionResponse {
    println!("Received interaction event: {:?}", event);

    match event {
        SlackInteractionEvent::BlockSuggestion(suggestion_event) => {
            // `suggestion_event.value` is what the user has typed so far.
            // Slack allows at most 100 plain text options here.
            SlackBlockSuggestionResponse::Options(SlackBlockSuggestionOptions::new(vec![
                SlackBlockChoiceItem::new(pt!("Unexpected sentience"), "AI-2323".to_string()),
            ]))
            .into()
        }
        _ => SlackInteractionResponse::Empty,
    }
}

fn test_error_handler(
    err: Box<dyn std::error::Error + Send + Sync>,
    _client: Arc<SlackHyperClient>,
    _states: SlackClientEventsUserState,
) -> HttpStatusCode {
    println!("{:#?}", err);

    // Defines what we return Slack server
    HttpStatusCode::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

To serve the Options Load URL for external_select menus, point it at the same /interaction route: the block_suggestion payload arrives through the same extractor. See Events API for Hyper for the limits Slack applies to the reply.

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

// Returning `SlackInteractionResponse` lets one handler answer every kind of
// interaction: options for `block_suggestion`, a `response_action` for
// `view_submission`, and a bare acknowledgement for everything else.
// Handlers returning `Result<(), _>` keep working as before.
async fn test_interaction_events_function(
    event: SlackInteractionEvent,
    _client: Arc<SlackHyperClient>,
    _states: SlackClientEventsUserState,
) -> Result<SlackInteractionResponse, Box<dyn std::error::Error + Send + Sync>> {
    println!("{:#?}", event);
    Ok(SlackInteractionResponse::Empty)
}

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;

Options Load URL / block_suggestion

Socket Mode apps don’t configure an Options Load URL: when a user types into an external_select or multi_external_select menu, the block_suggestion payload arrives on the same socket as any other interaction. Answer it by returning SlackBlockSuggestionResponse from the interaction callback:

async fn test_interaction_events_function(
    event: SlackInteractionEvent,
    _client: Arc<SlackHyperClient>,
    _states: SlackClientEventsUserState,
) -> Result<SlackInteractionResponse, Box<dyn std::error::Error + Send + Sync>> {
    match event {
        SlackInteractionEvent::BlockSuggestion(suggestion_event) => {
            // `suggestion_event.value` is what the user has typed so far
            Ok(SlackBlockSuggestionResponse::Options(SlackBlockSuggestionOptions::new(vec![
                SlackBlockChoiceItem::new(pt!("Unexpected sentience"), "AI-2323".to_string())
                    .with_description(pt!("Issue AI-2323")),
            ]))
            .into())
        }
        _ => Ok(SlackInteractionResponse::Empty),
    }
}

The response travels back in the socket acknowledgement, so it is only sent when Slack marked the envelope with accepts_response_payload. Otherwise the library logs a warning and sends a bare acknowledgement.

Slack limits the reply to 100 options, or to 100 option groups (SlackBlockSuggestionResponse::OptionGroups) of 100 options each, and only plain_text is allowed in the option text and description.

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.

A frame whose body the library cannot parse (an unrecognised event type, or a known type with an unexpected payload) is passed to this error handler and then acknowledged on your behalf, so Slack does not keep redelivering it.

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.

This needs for Hyper or Socket Mode. For Axum use its own support for user state management.

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 for Hyper/Socket Mode

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 for Hyper/Socket Mode

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)