chat_sdk-x 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 263d6f652c3e638bf81b76b31d42cbcd629cd545fa905aa12d703b359ad7fd16
4
- data.tar.gz: 00c9a4d7394037ec77d1cdbd3fc6f9462009a61c01c8f15acc349fb872b63682
3
+ metadata.gz: b3d2500b4890fc426c2f4bf0ae98c4d20309818ddfe00c66518013481a6aa436
4
+ data.tar.gz: 51c114854c79621bdd093b06bc0d1f7a040533ea38f943ce7f6b9bfae056b51c
5
5
  SHA512:
6
- metadata.gz: b0b4b384141b90f9a2870d7e2f3d16a78ae0a5f02093615053a981d0bb3a8d8d724b7803967992e386c46dff2711f5762ddc867cb1d2e640bd4ce9df87ce5ab5
7
- data.tar.gz: e3954e97efc0e99e52787e9934f26d4f0e36df7827352b2a960da04cbfe37346b6a6647c4e8bd5d11cd43692149f3fafe8ea2881b92cd4665cec6c676cefaabc
6
+ metadata.gz: 4f096900b3e142d8aba30b804c8ad49309755392876dd3741bb7824d92879bd09fc5b5aad4f888b6185556d34c001664eedc3af3fcdc83a1adaad6d3d2b19493
7
+ data.tar.gz: 7665fbb3b51567a21a4abf0007853845ed13a902f3cf1483e4b08eba2c5f23408e31001ebbb16f870f09af5d5de99ae8d01e113932bd977bd634802acd8809e2
@@ -7,7 +7,9 @@ require "rack/utils"
7
7
  module ChatSDK
8
8
  module X
9
9
  class Adapter < ChatSDK::Adapter::Base
10
- capabilities :direct_messages, :reactions
10
+ include ChatSDK::Adapter::MediaTypes
11
+
12
+ capabilities :direct_messages, :reactions, :delete_messages, :message_history, :file_uploads
11
13
 
12
14
  attr_reader :client
13
15
 
@@ -83,20 +85,53 @@ module ChatSDK
83
85
  raw: result
84
86
  )
85
87
  else
86
- payload = {text: text}
87
- payload[:reply] = {in_reply_to_tweet_id: channel_id} if channel_id && thread_id
88
- result = @client.create_tweet(**payload)
89
- tweet_id = result.dig("data", "id") || result["id"]
88
+ reply_to = (channel_id if channel_id && thread_id)
89
+ result = @client.create_tweet(text: text, reply_to: reply_to)
90
+ parse_tweet_message(result, channel_id, thread_id: thread_id)
91
+ end
92
+ end
90
93
 
91
- ChatSDK::Message.new(
92
- id: tweet_id,
93
- text: text,
94
- author: ChatSDK::Author.new(id: @user_id || "bot", name: "bot", platform: :x, bot: true),
95
- thread_id: thread_id || "x:post:#{tweet_id}",
96
- channel_id: channel_id,
97
- platform: :x,
98
- raw: result
99
- )
94
+ def upload_file(channel_id:, io:, filename:, thread_id: nil, comment: nil) # rubocop:disable Lint/UnusedMethodArgument
95
+ content_type = detect_content_type(filename)
96
+ bytes = io.respond_to?(:size) ? io.size : io.read.bytesize.tap { io.rewind }
97
+
98
+ media_id = @client.upload_media(io: io, content_type: content_type, total_bytes: bytes)
99
+
100
+ text = comment || ""
101
+ result = @client.create_tweet(text: text, media_ids: [media_id])
102
+ parse_tweet_message(result, channel_id)
103
+ end
104
+
105
+ def delete_message(channel_id:, message_id:) # rubocop:disable Lint/UnusedMethodArgument
106
+ @client.delete_tweet(tweet_id: message_id)
107
+ end
108
+
109
+ def fetch_messages(channel_id:, thread_id: nil, cursor: nil, limit: 50) # rubocop:disable Lint/UnusedMethodArgument
110
+ if thread_id&.start_with?("x:dm:")
111
+ participant_id = thread_id.delete_prefix("x:dm:")
112
+ result = @client.get_dm_events(participant_id: participant_id, cursor: cursor, limit: limit)
113
+ data = result["data"] || []
114
+ messages = data.map do |dm|
115
+ ChatSDK::Message.new(
116
+ id: dm["id"]&.to_s,
117
+ text: dm["text"] || "",
118
+ author: ChatSDK::Author.new(
119
+ id: dm["sender_id"] || "unknown",
120
+ name: dm["sender_id"] || "unknown",
121
+ platform: :x,
122
+ bot: false
123
+ ),
124
+ thread_id: thread_id,
125
+ channel_id: channel_id,
126
+ platform: :x,
127
+ raw: dm
128
+ )
129
+ end
130
+ next_cursor = result.dig("meta", "next_token")
131
+ [messages, next_cursor]
132
+ else
133
+ # X does not provide a robust thread-fetching API for tweets
134
+ [[], nil]
100
135
  end
101
136
  end
102
137
 
@@ -108,6 +143,19 @@ module ChatSDK
108
143
  @client.unlike_tweet(user_id: @user_id, tweet_id: message_id)
109
144
  end
110
145
 
146
+ def get_user(user_id)
147
+ data = @client.get_user(user_id)
148
+ return nil unless data&.dig("data", "id")
149
+
150
+ ChatSDK::Author.new(
151
+ id: data.dig("data", "id"),
152
+ name: data.dig("data", "username"),
153
+ platform: :x,
154
+ bot: false,
155
+ raw: data
156
+ )
157
+ end
158
+
111
159
  def open_dm(user_id)
112
160
  user_id
113
161
  end
@@ -119,6 +167,22 @@ module ChatSDK
119
167
  def render(postable_message)
120
168
  postable_message.text
121
169
  end
170
+
171
+ private
172
+
173
+ def parse_tweet_message(result, channel_id, thread_id: nil)
174
+ tweet_id = result.dig("data", "id") || result["id"]
175
+
176
+ ChatSDK::Message.new(
177
+ id: tweet_id,
178
+ text: result.dig("data", "text") || "",
179
+ author: ChatSDK::Author.new(id: @user_id || "bot", name: "bot", platform: :x, bot: true),
180
+ thread_id: thread_id || "x:post:#{tweet_id}",
181
+ channel_id: channel_id,
182
+ platform: :x,
183
+ raw: result
184
+ )
185
+ end
122
186
  end
123
187
  end
124
188
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
4
+
3
5
  module ChatSDK
4
6
  module X
5
7
  class ApiClient < ChatSDK::ApiClient::Base
@@ -9,12 +11,42 @@ module ChatSDK
9
11
  @access_token = access_token
10
12
  end
11
13
 
12
- def create_tweet(text:, reply: nil)
14
+ def create_tweet(text:, reply_to: nil, media_ids: nil)
13
15
  body = {"text" => text}
14
- body["reply"] = reply if reply
16
+ body["reply"] = {"in_reply_to_tweet_id" => reply_to} if reply_to
17
+ body["media"] = {"media_ids" => media_ids} if media_ids&.any?
15
18
  request(:post, "/2/tweets", body)
16
19
  end
17
20
 
21
+ def upload_media(io:, content_type:, total_bytes:)
22
+ # INIT
23
+ init_result = request(:post, "/2/media/upload", {
24
+ command: "INIT",
25
+ total_bytes: total_bytes,
26
+ media_type: content_type
27
+ })
28
+ media_id = init_result.dig("data", "id") || init_result["media_id_string"]
29
+
30
+ # APPEND (single chunk, works for images <5MB)
31
+ response = upload_connection.post("/2/media/upload") do |req|
32
+ req.body = {
33
+ command: "APPEND",
34
+ media_id: media_id,
35
+ segment_index: 0,
36
+ media_data: Base64.strict_encode64(io.read)
37
+ }
38
+ end
39
+ handle_response(response)
40
+
41
+ # FINALIZE
42
+ request(:post, "/2/media/upload", {
43
+ command: "FINALIZE",
44
+ media_id: media_id
45
+ })
46
+
47
+ media_id
48
+ end
49
+
18
50
  def send_dm(participant_id:, text:)
19
51
  request(:post, "/2/dm_conversations/with/#{participant_id}/messages", {"text" => text})
20
52
  end
@@ -27,6 +59,20 @@ module ChatSDK
27
59
  request(:delete, "/2/users/#{user_id}/likes/#{tweet_id}")
28
60
  end
29
61
 
62
+ def delete_tweet(tweet_id:)
63
+ request(:delete, "/2/tweets/#{tweet_id}")
64
+ end
65
+
66
+ def get_user(user_id)
67
+ request(:get, "/2/users/#{user_id}?user.fields=name,username")
68
+ end
69
+
70
+ def get_dm_events(participant_id:, cursor: nil, limit: 50)
71
+ query = "max_results=#{[limit, 100].min}&dm_event.fields=id,text,sender_id,created_at"
72
+ query += "&pagination_token=#{cursor}" if cursor
73
+ request(:get, "/2/dm_conversations/with/#{participant_id}/dm_events?#{query}")
74
+ end
75
+
30
76
  private
31
77
 
32
78
  def base_url
@@ -10,6 +10,7 @@ module ChatSDK
10
10
  events = []
11
11
  events.concat(parse_mentions(payload, bot_user_id))
12
12
  events.concat(parse_direct_messages(payload, bot_user_id))
13
+ events.concat(parse_favorites(payload, bot_user_id))
13
14
  events
14
15
  end
15
16
 
@@ -81,6 +82,32 @@ module ChatSDK
81
82
  )
82
83
  end
83
84
  end
85
+
86
+ def parse_favorites(payload, bot_user_id)
87
+ fav_events = payload["favorite_events"] || []
88
+ return [] unless fav_events.is_a?(Array)
89
+
90
+ fav_events.filter_map do |data|
91
+ user = data["user"] || {}
92
+ user_id = user["id_str"] || user["id"]&.to_s
93
+ next if bot_user_id && user_id == bot_user_id
94
+
95
+ tweet = data["favorited_status"] || {}
96
+ tweet_id = tweet["id_str"] || tweet["id"]&.to_s
97
+
98
+ ChatSDK::Events::Reaction.new(
99
+ emoji: "heart",
100
+ user_id: user_id || "unknown",
101
+ message_id: tweet_id || "unknown",
102
+ thread_id: "x:post:#{tweet_id}",
103
+ channel_id: user_id,
104
+ added: true,
105
+ platform: :x,
106
+ adapter_name: :x,
107
+ raw: data
108
+ )
109
+ end
110
+ end
84
111
  end
85
112
  end
86
113
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chat_sdk-x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Quentin Rousseau