💎 ChatSDK Ruby

X Adapter

X (Twitter) adapter with HMAC-SHA256 webhook signature verification, tweets, direct messages, and likes via the chat_sdk-x gem.

The X adapter (chat_sdk-x) integrates with the X (Twitter) API v2 for tweets, direct messages, likes, and HMAC-SHA256 webhook signature verification.

Installation

# Gemfile
gem "chat_sdk"
gem "chat_sdk-x"

Configuration

require "chat_sdk"
require "chat_sdk/x"

x = ChatSDK::X::Adapter.new(
  access_token: ENV["X_ACCESS_TOKEN"],         # Bearer token for API v2
  consumer_secret: ENV["X_CONSUMER_SECRET"],   # Consumer secret for webhook verification
  user_id: ENV["X_USER_ID"]                    # Bot user ID (for filtering own messages)
)

access_token and consumer_secret are required. user_id is recommended to filter out the bot's own messages from incoming events.

Environment Variables

VariableDescription
X_ACCESS_TOKENBearer token from the X Developer Portal
X_CONSUMER_SECRETConsumer secret from your X App settings (for HMAC-SHA256 webhook verification)
X_USER_IDThe bot's X user ID (used to skip own messages in event parsing)

X App Setup

  1. Create an app in the X Developer Portal.
  2. Enable OAuth 2.0 and generate a Bearer Token under Keys and Tokens.
  3. Note your Consumer Secret from Consumer Keys.
  4. Set up an Account Activity API webhook endpoint for real-time events.
  5. Note your bot's user ID (available from the GET /2/users/me endpoint).

Webhook URL

https://your-domain.com/webhooks/x
map "/webhooks/x" do
  run bot.webhooks[:x]
end

The adapter handles CRC challenge-response (GET with crc_token) and POST webhook events.

Capabilities

CapabilitySupportedNotes
direct_messagesYesDMs via X API v2
reactionsYesLike/unlike tweets
edit_messagesNoRaises NotSupportedError
delete_messagesYesDeletes tweets
ephemeral_messagesNoRaises NotSupportedError
typing_indicatorNoRaises NotSupportedError
modalsNoRaises NotSupportedError
streaming_editNoRaises NotSupportedError
threadsNoRaises NotSupportedError
message_historyYesDM history via API v2
file_uploadsYesChunked media upload

Events

The adapter parses two types of webhook events:

  • Mentions -- When someone mentions your bot in a tweet, parsed as Events::Mention with thread_id: "x:post:{conversation_id}".
  • Direct Messages -- Incoming DMs parsed as Events::DirectMessage with thread_id: "x:dm:{sender_id}".

Messages from the bot itself (matching user_id) are automatically skipped.

Direct Client Access

x_adapter = bot.adapter(:x)
client = x_adapter.client  # ChatSDK::X::ApiClient

# Create a tweet
client.create_tweet(text: "Hello from ChatSDK!")

# Send a DM
client.send_dm(participant_id: "user-id", text: "Hello via DM")

# Like a tweet
client.like_tweet(user_id: "my-user-id", tweet_id: "tweet-id")

# Unlike a tweet
client.unlike_tweet(user_id: "my-user-id", tweet_id: "tweet-id")

Mention Formatting

X mentions use the @username format. Pass the username (not user ID) to mention:

x_adapter.mention("rootly")  # => "@rootly"

Message IDs

  • Tweets: Numeric tweet IDs (e.g., "1234567890")
  • Thread IDs: Prefixed strings -- "x:post:{conversation_id}" for tweets, "x:dm:{sender_id}" for DMs
  • DM routing: When thread_id starts with "x:dm:", post_message sends a DM instead of a tweet

OAuth Token Refresh

Instead of a static bearer token, you can use managed OAuth2 with automatic token refresh. Provide client_id and refresh_token instead of access_token:

x = ChatSDK::X::Adapter.new(
  client_id: ENV["X_CLIENT_ID"],
  client_secret: ENV["X_CLIENT_SECRET"],
  refresh_token: ENV["X_REFRESH_TOKEN"],
  consumer_secret: ENV["X_CONSUMER_SECRET"],
  user_id: ENV["X_USER_ID"]
)

# Wire up a state store so refreshed tokens survive restarts
x.set_state(my_state_store)

The adapter automatically refreshes the access token before it expires (with a 60-second margin) and persists the rotated refresh token to the state store. Token refresh is thread-safe with mutex-guarded double-check locking.

Media Uploads

Upload images and media to attach to tweets using a three-phase chunked upload (INIT/APPEND/FINALIZE):

x.upload_file(
  channel_id: nil,
  io: File.open("photo.jpg"),
  filename: "photo.jpg",
  comment: "Check this out!"
)

The adapter handles the chunked upload protocol automatically -- it initializes the upload with the file size and content type, appends the file data, and finalizes to get a media_id that is attached to the tweet.

Message History

Fetch DM conversation history using cursor-based pagination:

messages, next_cursor = x.fetch_messages(
  channel_id: "user-id",
  thread_id: "x:dm:user-id"
)

# Page through results
more_messages, _ = x.fetch_messages(
  channel_id: "user-id",
  thread_id: "x:dm:user-id",
  cursor: next_cursor
)

Message history is available for DM threads only (thread_id must start with "x:dm:"). Tweet thread history is not supported by the X API.

Get User

Look up an X user by ID:

author = x.get_user("123456789")
author.name   # => "rootly"
author.raw    # => full X API v2 response

Returns a ChatSDK::Author or nil if the user is not found.

On this page