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
| Variable | Description |
|---|---|
X_ACCESS_TOKEN | Bearer token from the X Developer Portal |
X_CONSUMER_SECRET | Consumer secret from your X App settings (for HMAC-SHA256 webhook verification) |
X_USER_ID | The bot's X user ID (used to skip own messages in event parsing) |
X App Setup
- Create an app in the X Developer Portal.
- Enable OAuth 2.0 and generate a Bearer Token under Keys and Tokens.
- Note your Consumer Secret from Consumer Keys.
- Set up an Account Activity API webhook endpoint for real-time events.
- Note your bot's user ID (available from the
GET /2/users/meendpoint).
Webhook URL
https://your-domain.com/webhooks/xmap "/webhooks/x" do
run bot.webhooks[:x]
endThe adapter handles CRC challenge-response (GET with crc_token) and POST webhook events.
Capabilities
| Capability | Supported | Notes |
|---|---|---|
direct_messages | Yes | DMs via X API v2 |
reactions | Yes | Like/unlike tweets |
edit_messages | No | Raises NotSupportedError |
delete_messages | Yes | Deletes tweets |
ephemeral_messages | No | Raises NotSupportedError |
typing_indicator | No | Raises NotSupportedError |
modals | No | Raises NotSupportedError |
streaming_edit | No | Raises NotSupportedError |
threads | No | Raises NotSupportedError |
message_history | Yes | DM history via API v2 |
file_uploads | Yes | Chunked media upload |
Events
The adapter parses two types of webhook events:
- Mentions -- When someone mentions your bot in a tweet, parsed as
Events::Mentionwiththread_id: "x:post:{conversation_id}". - Direct Messages -- Incoming DMs parsed as
Events::DirectMessagewiththread_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_idstarts with"x:dm:",post_messagesends 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 responseReturns a ChatSDK::Author or nil if the user is not found.