Class: ChatSDK::X::Adapter

Inherits:
Adapter::Base
  • Object
show all
Defined in:
lib/chat_sdk/x/adapter.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(access_token: nil, consumer_secret: nil, user_id: nil) ⇒ Adapter

Returns a new instance of Adapter.

Raises:

  • (ChatSDK::ConfigurationError)


14
15
16
17
18
19
20
21
22
23
# File 'lib/chat_sdk/x/adapter.rb', line 14

def initialize(access_token: nil, consumer_secret: nil, user_id: nil)
  @access_token = access_token || ENV["X_ACCESS_TOKEN"]
  @consumer_secret = consumer_secret || ENV["X_CONSUMER_SECRET"]
  @user_id = user_id || ENV["X_USER_ID"]

  raise ChatSDK::ConfigurationError, "X access_token required" unless @access_token
  raise ChatSDK::ConfigurationError, "X consumer_secret required" unless @consumer_secret

  @client = ApiClient.new(@access_token)
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



12
13
14
# File 'lib/chat_sdk/x/adapter.rb', line 12

def client
  @client
end

Instance Method Details

#ack_response(rack_request) ⇒ Object



49
50
51
52
53
54
55
56
57
58
# File 'lib/chat_sdk/x/adapter.rb', line 49

def ack_response(rack_request)
  return nil unless rack_request.get?

  crc_token = rack_request.params["crc_token"]
  return nil unless crc_token

  response_token = "sha256=#{Base64.strict_encode64(OpenSSL::HMAC.digest("SHA256", @consumer_secret, crc_token))}"

  [200, {"content-type" => "application/json"}, [JSON.generate({"response_token" => response_token})]]
end

#add_reaction(channel_id:, message_id:, emoji:) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument



136
137
138
# File 'lib/chat_sdk/x/adapter.rb', line 136

def add_reaction(channel_id:, message_id:, emoji:) # rubocop:disable Lint/UnusedMethodArgument
  @client.like_tweet(user_id: @user_id, tweet_id: message_id)
end

#delete_message(channel_id:, message_id:) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument



103
104
105
# File 'lib/chat_sdk/x/adapter.rb', line 103

def delete_message(channel_id:, message_id:) # rubocop:disable Lint/UnusedMethodArgument
  @client.delete_tweet(tweet_id: message_id)
end

#fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/chat_sdk/x/adapter.rb', line 107

def fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50) # rubocop:disable Lint/UnusedMethodArgument
  if thread_id&.start_with?("x:dm:")
    participant_id = thread_id.delete_prefix("x:dm:")
    result = @client.get_dm_events(participant_id: participant_id, cursor: cursor, limit: limit)
    data = result["data"] || []
    messages = data.map do |dm|
      ChatSDK::Message.new(
        id: dm["id"]&.to_s,
        text: dm["text"] || "",
        author: ChatSDK::Author.new(
          id: dm["sender_id"] || "unknown",
          name: dm["sender_id"] || "unknown",
          platform: :x,
          bot: false
        ),
        thread_id: thread_id,
        channel_id: channel_id,
        platform: :x,
        raw: dm
      )
    end
    next_cursor = result.dig("meta", "next_token")
    [messages, next_cursor]
  else
    # X does not provide a robust thread-fetching API for tweets
    [[], nil]
  end
end

#mention(user_id) ⇒ Object



148
149
150
# File 'lib/chat_sdk/x/adapter.rb', line 148

def mention(user_id)
  "@#{user_id}"
end

#nameObject



25
26
27
# File 'lib/chat_sdk/x/adapter.rb', line 25

def name
  :x
end

#open_dm(user_id) ⇒ Object



144
145
146
# File 'lib/chat_sdk/x/adapter.rb', line 144

def open_dm(user_id)
  user_id
end

#parse_events(rack_request) ⇒ Object



60
61
62
63
64
65
# File 'lib/chat_sdk/x/adapter.rb', line 60

def parse_events(rack_request)
  payload = read_json_body(rack_request)
  EventParser.parse(payload, bot_user_id: @user_id)
rescue JSON::ParserError
  []
end

#post_message(channel_id:, message:, thread_id: nil) ⇒ Object

Outbound



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/chat_sdk/x/adapter.rb', line 68

def post_message(channel_id:, message:, thread_id: nil)
  msg = ChatSDK::PostableMessage.from(message)
  text = msg.text || msg.card&.fallback_text || ""

  if thread_id&.start_with?("x:dm:")
    result = @client.send_dm(participant_id: channel_id, text: text)
    message_id = result.dig("dm_event", "id") || result["id"]

    ChatSDK::Message.new(
      id: message_id,
      text: text,
      author: ChatSDK::Author.new(id: @user_id || "bot", name: "bot", platform: :x, bot: true),
      thread_id: thread_id,
      channel_id: channel_id,
      platform: :x,
      raw: result
    )
  else
    payload = {text: text}
    payload[:reply] = {in_reply_to_tweet_id: channel_id} if channel_id && thread_id
    result = @client.create_tweet(**payload)
    tweet_id = result.dig("data", "id") || result["id"]

    ChatSDK::Message.new(
      id: tweet_id,
      text: text,
      author: ChatSDK::Author.new(id: @user_id || "bot", name: "bot", platform: :x, bot: true),
      thread_id: thread_id || "x:post:#{tweet_id}",
      channel_id: channel_id,
      platform: :x,
      raw: result
    )
  end
end

#remove_reaction(channel_id:, message_id:, emoji:) ⇒ Object

rubocop:disable Lint/UnusedMethodArgument



140
141
142
# File 'lib/chat_sdk/x/adapter.rb', line 140

def remove_reaction(channel_id:, message_id:, emoji:) # rubocop:disable Lint/UnusedMethodArgument
  @client.unlike_tweet(user_id: @user_id, tweet_id: message_id)
end

#render(postable_message) ⇒ Object



152
153
154
# File 'lib/chat_sdk/x/adapter.rb', line 152

def render(postable_message)
  postable_message.text
end

#verify_request!(rack_request) ⇒ Object

Inbound



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/chat_sdk/x/adapter.rb', line 30

def verify_request!(rack_request)
  body = rack_request.body.read
  rack_request.body.rewind

  signature = rack_request.get_header("HTTP_X_TWITTER_WEBHOOKS_SIGNATURE")

  unless signature
    raise ChatSDK::SignatureVerificationError, "Missing X signature header"
  end

  expected = "sha256=#{Base64.strict_encode64(OpenSSL::HMAC.digest("SHA256", @consumer_secret, body))}"

  unless Rack::Utils.secure_compare(signature, expected)
    raise ChatSDK::SignatureVerificationError, "Invalid X signature"
  end

  true
end