Skip to content

Instantly share code, notes, and snippets.

@chrisbloom7
Last active July 2, 2024 03:57
Show Gist options
  • Save chrisbloom7/52bc954b4df09b9cb829a73cfd5466c0 to your computer and use it in GitHub Desktop.
Save chrisbloom7/52bc954b4df09b9cb829a73cfd5466c0 to your computer and use it in GitHub Desktop.
Basic implementation of a websocket connector for Slack using slack-ruby-client and async-websockets gems
# frozen_string_literal: true
# Basic implementation of a websocket connector for Slack using the slack-ruby-client gem
#
# Run this rake task in a virtual container that is set to automatically restart on failure; this
# will help deal with disconnects from Slack since socket URLs are temporary.
# https://api.slack.com/apis/connections/socket-implement#disconnect
#
# This rake task can be called in multiple virtual containers to help with resilliancy and rolling restarts
# https://api.slack.com/apis/connections/socket-implement#connections
require "async"
require "slack_ruby_client"
require "async/io/stream"
require "async/http/endpoint"
require "async/websocket/client"
namespace :slack do
namespace :socket_mode do
desc "Start the socket mode client"
task :start, [:debug] => [:environment] do |_task, args|
# Get a websocket URL
# https://api.slack.com/apis/connections/socket-implement#call
client = Slack::Web::Client.new
url = client.apps_connections_open.url
url += "&debug_reconnects=true" if args[:debug].present?
endpoint = Async::HTTP::Endpoint.parse(url)
# Start the async client and open a connection to the websocket URL
# https://api.slack.com/apis/connections/socket-implement#connect
Async do
Async::WebSocket::Client.connect(endpoint) do |connection|
while (message = connection.read)
if message[:envelope_id]
# Acknowledge receipt of message
# https://api.slack.com/apis/connections/socket-implement#acknowledge
connection.write({ envelope_id: message[:envelope_id] })
connection.flush
end
# Handle your events here
# https://api.slack.com/apis/connections/socket-implement#events
# ...
end
end
end
end
end
end
@ioquatix
Copy link

Cool! As of the latest version of async-websocket, you don't need to put Async::WebSocket::Client.connect(endpoint) in an async block, it's implicit now: https://github.com/socketry/async-websocket/blob/b6dedb57a7f8597c6935e9cc62934ad330e12f29/lib/async/websocket/client.rb#L57

@chrisbloom7
Copy link
Author

Thanks for the tip @ioquatix! In this example, I take it the only change would be unwrapping Async::WebSocket::Client.connect from the Async block above it, correct?

@ioquatix
Copy link

ioquatix commented Jul 1, 2024

Yes that should be it. It's just a small quality of life improvement in the latest release, as I imagine it will be a common use case and the overhead is pretty much close to zero in terms of the internal Sync{} block.

namespace :slack do
  namespace :socket_mode do
    desc "Start the socket mode client"
    task :start, [:debug] => [:environment] do |_task, args|
      # Get a websocket URL
      # https://api.slack.com/apis/connections/socket-implement#call
      client = Slack::Web::Client.new      
      url = client.apps_connections_open.url
      url += "&debug_reconnects=true" if args[:debug].present?
      endpoint = Async::HTTP::Endpoint.parse(url)
      
      # Start the async client and open a connection to the websocket URL
      # https://api.slack.com/apis/connections/socket-implement#connect
      Async::WebSocket::Client.connect(endpoint) do |connection|
        while (message = connection.read)
          if message[:envelope_id]
            # Acknowledge receipt of message
            # https://api.slack.com/apis/connections/socket-implement#acknowledge
            connection.write({ envelope_id: message[:envelope_id] })
            connection.flush
          end

          # Handle your events here
          # https://api.slack.com/apis/connections/socket-implement#events
          # ...
        end
      end
    end
  end
end

@ioquatix
Copy link

ioquatix commented Jul 2, 2024

Also, if you'd like to pass arguments in more easily, you might like to try bake, create a file bake/slack/socket_mode.rb with the following:

# Start the socket mode client.
# @parameter [Boolean] Whether to enable debug reconnects.
def start(debug_reconnects: false)
  client = Slack::Web::Client.new

  # Get a websocket URL
  # https://api.slack.com/apis/connections/socket-implement#call
  url = client.apps_connections_open.url

  if debug_reconnects
    url += "&debug_reconnects=true"
  end

  endpoint = Async::HTTP::Endpoint.parse(url)

  # Start the async client and open a connection to the websocket URL
  # https://api.slack.com/apis/connections/socket-implement#connect
  Async::WebSocket::Client.connect(endpoint) do |connection|
    while (message = connection.read)
      if message[:envelope_id]
        # Acknowledge receipt of message
        # https://api.slack.com/apis/connections/socket-implement#acknowledge
        connection.write({ envelope_id: message[:envelope_id] })
        connection.flush
      end

      # Handle your events here
      # https://api.slack.com/apis/connections/socket-implement#events
      # ...
    end
  end
end

Then launch it like so:

bundle exec bake slack:socket_mode:start --debug-reconnects true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment