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 +4 -4
- data/lib/chat_sdk/x/adapter.rb +78 -14
- data/lib/chat_sdk/x/api_client.rb +48 -2
- data/lib/chat_sdk/x/event_parser.rb +27 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b3d2500b4890fc426c2f4bf0ae98c4d20309818ddfe00c66518013481a6aa436
|
|
4
|
+
data.tar.gz: 51c114854c79621bdd093b06bc0d1f7a040533ea38f943ce7f6b9bfae056b51c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f096900b3e142d8aba30b804c8ad49309755392876dd3741bb7824d92879bd09fc5b5aad4f888b6185556d34c001664eedc3af3fcdc83a1adaad6d3d2b19493
|
|
7
|
+
data.tar.gz: 7665fbb3b51567a21a4abf0007853845ed13a902f3cf1483e4b08eba2c5f23408e31001ebbb16f870f09af5d5de99ae8d01e113932bd977bd634802acd8809e2
|
data/lib/chat_sdk/x/adapter.rb
CHANGED
|
@@ -7,7 +7,9 @@ require "rack/utils"
|
|
|
7
7
|
module ChatSDK
|
|
8
8
|
module X
|
|
9
9
|
class Adapter < ChatSDK::Adapter::Base
|
|
10
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
result
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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:,
|
|
14
|
+
def create_tweet(text:, reply_to: nil, media_ids: nil)
|
|
13
15
|
body = {"text" => text}
|
|
14
|
-
body["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
|