x-resources 1.0.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 +7 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +254 -0
- data/LICENSE.txt +21 -0
- data/README.md +148 -0
- data/lib/x/resources/abstract_class.rb +29 -0
- data/lib/x/resources/actions/direct_messages.rb +99 -0
- data/lib/x/resources/actions/engagement.rb +95 -0
- data/lib/x/resources/actions/lists.rb +126 -0
- data/lib/x/resources/actions/posts.rb +77 -0
- data/lib/x/resources/actions/relationships.rb +91 -0
- data/lib/x/resources/actions.rb +22 -0
- data/lib/x/resources/api.rb +40 -0
- data/lib/x/resources/attributes.rb +203 -0
- data/lib/x/resources/batch.rb +43 -0
- data/lib/x/resources/batch_finders.rb +185 -0
- data/lib/x/resources/bookmark_folder.rb +23 -0
- data/lib/x/resources/community.rb +137 -0
- data/lib/x/resources/cursor.rb +493 -0
- data/lib/x/resources/direct_message.rb +325 -0
- data/lib/x/resources/direct_message_conversations.rb +147 -0
- data/lib/x/resources/errors.rb +104 -0
- data/lib/x/resources/finders.rb +255 -0
- data/lib/x/resources/identity.rb +65 -0
- data/lib/x/resources/includes.rb +216 -0
- data/lib/x/resources/list.rb +336 -0
- data/lib/x/resources/lookups/communities.rb +56 -0
- data/lib/x/resources/lookups/direct_messages.rb +86 -0
- data/lib/x/resources/lookups/lists.rb +44 -0
- data/lib/x/resources/lookups/media.rb +72 -0
- data/lib/x/resources/lookups/posts.rb +198 -0
- data/lib/x/resources/lookups/spaces.rb +87 -0
- data/lib/x/resources/lookups/trends.rb +38 -0
- data/lib/x/resources/lookups/users.rb +221 -0
- data/lib/x/resources/lookups.rb +25 -0
- data/lib/x/resources/marshalling.rb +93 -0
- data/lib/x/resources/matching_rule.rb +107 -0
- data/lib/x/resources/media.rb +278 -0
- data/lib/x/resources/media_ids.rb +74 -0
- data/lib/x/resources/memo.rb +54 -0
- data/lib/x/resources/page.rb +394 -0
- data/lib/x/resources/page_limit.rb +80 -0
- data/lib/x/resources/pages.rb +270 -0
- data/lib/x/resources/parallel.rb +82 -0
- data/lib/x/resources/personalized_trend.rb +124 -0
- data/lib/x/resources/place.rb +107 -0
- data/lib/x/resources/poll.rb +75 -0
- data/lib/x/resources/post.rb +615 -0
- data/lib/x/resources/post_collections.rb +67 -0
- data/lib/x/resources/post_counts.rb +215 -0
- data/lib/x/resources/post_search.rb +86 -0
- data/lib/x/resources/post_usage.rb +203 -0
- data/lib/x/resources/post_writes.rb +140 -0
- data/lib/x/resources/published_count.rb +31 -0
- data/lib/x/resources/references.rb +121 -0
- data/lib/x/resources/relation_writes.rb +54 -0
- data/lib/x/resources/relationships.rb +77 -0
- data/lib/x/resources/resource.rb +535 -0
- data/lib/x/resources/serialization.rb +58 -0
- data/lib/x/resources/shape.rb +167 -0
- data/lib/x/resources/space.rb +332 -0
- data/lib/x/resources/topic.rb +59 -0
- data/lib/x/resources/trend.rb +130 -0
- data/lib/x/resources/user.rb +502 -0
- data/lib/x/resources/user_collections.rb +213 -0
- data/lib/x/resources/user_finders.rb +282 -0
- data/lib/x/resources/utils.rb +358 -0
- data/lib/x/resources/value_equality.rb +38 -0
- data/lib/x/resources/value_marshalling.rb +89 -0
- data/lib/x/resources/version.rb +25 -0
- data/lib/x/resources.rb +22 -0
- data/sig/manifest.yaml +7 -0
- data/sig/x-resources.rbs +813 -0
- metadata +140 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cursor"
|
|
4
|
+
require_relative "direct_message_conversations"
|
|
5
|
+
require_relative "finders"
|
|
6
|
+
require_relative "resource"
|
|
7
|
+
|
|
8
|
+
module X
|
|
9
|
+
module Resources
|
|
10
|
+
# A direct message event
|
|
11
|
+
# @api public
|
|
12
|
+
class ::X::DirectMessage < Resource
|
|
13
|
+
extend Finders
|
|
14
|
+
extend DirectMessageConversations
|
|
15
|
+
|
|
16
|
+
# The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X
|
|
17
|
+
#
|
|
18
|
+
# @!parse
|
|
19
|
+
# extend X::Resources::Finders
|
|
20
|
+
# extend X::Resources::DirectMessageConversations
|
|
21
|
+
|
|
22
|
+
# The direct message event fields the object layer requests; the sender, the participants, and the posts a
|
|
23
|
+
# message refers to come with their expansions
|
|
24
|
+
#
|
|
25
|
+
# A minor release may add to it the fields the API adds, so that a lookup asks for them too; see
|
|
26
|
+
# {Resource#hydrated?} for what that means for a resource looked up with a list of fields of its own.
|
|
27
|
+
FIELDS = %w[attachments created_at dm_conversation_id entities event_type id text].freeze
|
|
28
|
+
# Every expansion available on direct message endpoints
|
|
29
|
+
#
|
|
30
|
+
# A minor release may add to it the expansions the API adds, so that a lookup asks for them too; see
|
|
31
|
+
# {Resource#hydrated?} for what that means for a resource looked up with a list of expansions of its own.
|
|
32
|
+
EXPANSIONS = %w[attachments.media_keys participant_ids referenced_posts sender_id].freeze
|
|
33
|
+
# Maximum number of events per page
|
|
34
|
+
# @api private
|
|
35
|
+
MAX_RESULTS = 100
|
|
36
|
+
private_constant :MAX_RESULTS
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# The API endpoint used to look up direct message events by identifier
|
|
40
|
+
#
|
|
41
|
+
# @api private
|
|
42
|
+
# @return [String] the endpoint
|
|
43
|
+
# @example Get the endpoint
|
|
44
|
+
# X::DirectMessage.__send__(:endpoint) # => "dm_events"
|
|
45
|
+
def endpoint
|
|
46
|
+
"dm_events"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The query parameter that selects direct message event fields
|
|
50
|
+
#
|
|
51
|
+
# @api private
|
|
52
|
+
# @return [String] the fields parameter
|
|
53
|
+
# @example Get the fields parameter
|
|
54
|
+
# X::DirectMessage.__send__(:fields_key) # => "dm_event.fields"
|
|
55
|
+
def fields_key = "dm_event.fields"
|
|
56
|
+
|
|
57
|
+
private :endpoint, :fields_key
|
|
58
|
+
|
|
59
|
+
# The default query parameters requesting every direct message field and expansion
|
|
60
|
+
#
|
|
61
|
+
# @api public
|
|
62
|
+
# @return [Hash{String => Array<String>}] the default query parameters
|
|
63
|
+
# @example Get the default parameters
|
|
64
|
+
# X::DirectMessage.default_params["dm_event.fields"]
|
|
65
|
+
def default_params
|
|
66
|
+
{"dm_event.fields" => FIELDS, "user.fields" => User::FIELDS, "post.fields" => Post::FIELDS,
|
|
67
|
+
"media.fields" => Media::FIELDS, "expansions" => EXPANSIONS}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The most recent direct message events across every conversation
|
|
71
|
+
#
|
|
72
|
+
# @api public
|
|
73
|
+
# @param client [Object] the client used to make the requests
|
|
74
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
75
|
+
# @return [Cursor] a cursor over the events
|
|
76
|
+
# @example Print the most recent direct messages
|
|
77
|
+
# X::DirectMessage.all(client: client).first(10).each { |message| puts message.text }
|
|
78
|
+
def all(client:, **params)
|
|
79
|
+
Cursor.__send__(:build, self, "dm_events", client:, params: {max_results: MAX_RESULTS}.merge(params))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The direct message events in the one-to-one conversation with a user
|
|
83
|
+
#
|
|
84
|
+
# @api public
|
|
85
|
+
# @param user [User, String, Integer] the other participant or their identifier
|
|
86
|
+
# @param client [Object] the client used to make the requests
|
|
87
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
88
|
+
# @return [Cursor] a cursor over the events
|
|
89
|
+
# @example Print the conversation with a user
|
|
90
|
+
# X::DirectMessage.with(user, client: client).each { |message| puts message.text }
|
|
91
|
+
def with(user, client:, **params)
|
|
92
|
+
path = "dm_conversations/with/#{Utils.id_of(user, User)}/dm_events"
|
|
93
|
+
Cursor.__send__(:build, self, path, client:, params: {max_results: MAX_RESULTS}.merge(params))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Send a direct message to a user as the authenticated user
|
|
97
|
+
#
|
|
98
|
+
# @api public
|
|
99
|
+
# @param user [User, String, Integer] the recipient or their identifier
|
|
100
|
+
# @param text [String, nil] the text of the message, or nil for a message of attachments alone
|
|
101
|
+
# @param client [Object] the client used to make the request
|
|
102
|
+
# @param media_ids [Array<String, Integer, #fetch, Media>, String, Integer, #fetch, Media, nil] the identifiers or
|
|
103
|
+
# media keys of uploaded media to attach, what the uploads returned, or media, such as that of a post, one or
|
|
104
|
+
# many
|
|
105
|
+
# @param params [Hash] additional request body fields, such as attachments
|
|
106
|
+
# @return [DirectMessage] the sent message, holding only its identifiers
|
|
107
|
+
# @raise [ArgumentError] if the message has neither text nor any other field, or has both media_ids and
|
|
108
|
+
# attachments
|
|
109
|
+
# @raise [MissingResource] if the API answers without the message
|
|
110
|
+
# @example Send a direct message
|
|
111
|
+
# X::DirectMessage.create(user, "Hello!", client: client)
|
|
112
|
+
# @example Send an image without text
|
|
113
|
+
# X::DirectMessage.create(user, client: client, media_ids: media)
|
|
114
|
+
def create(user, text = nil, client:, media_ids: nil, **params)
|
|
115
|
+
path = "dm_conversations/with/#{Utils.id_of(user, User)}/messages"
|
|
116
|
+
sent(client.post(path, message(text, params, media_ids), **Utils::JSON_CLASSES), path, client:)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Delete a direct message event as the authenticated user
|
|
120
|
+
#
|
|
121
|
+
# @api public
|
|
122
|
+
# @param message [DirectMessage, String, Integer] the event or its identifier
|
|
123
|
+
# @param client [Object] the client used to make the request
|
|
124
|
+
# @return [Boolean] true if the event was deleted
|
|
125
|
+
# @example Delete a direct message
|
|
126
|
+
# X::DirectMessage.delete("1234567890", client: client)
|
|
127
|
+
def delete(message, client:)
|
|
128
|
+
body = client.delete("dm_events/#{Utils.id_of(message, self)}", **Utils::JSON_CLASSES)
|
|
129
|
+
Utils.written(body, "deleted").eql?(true)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @!attribute [r] text
|
|
134
|
+
# The text
|
|
135
|
+
#
|
|
136
|
+
# It is the text as the API sends it, which escapes &, <, and > as &, <, and >, and it stays so
|
|
137
|
+
# throughout 1.x, so unescape it to display it.
|
|
138
|
+
#
|
|
139
|
+
# @api public
|
|
140
|
+
# @return [String, nil] the text, HTML-escaped as the API sends it
|
|
141
|
+
# @example Get the text
|
|
142
|
+
# message.text # => "Ruby & Rails"
|
|
143
|
+
# @example Display the text
|
|
144
|
+
# CGI.unescapeHTML(message.text) # => "Ruby & Rails"
|
|
145
|
+
attribute :text
|
|
146
|
+
|
|
147
|
+
# @!attribute [r] event_type
|
|
148
|
+
# The event type: MessageCreate, ParticipantsJoin, or ParticipantsLeave
|
|
149
|
+
# @api public
|
|
150
|
+
# @return [String, nil] the event type
|
|
151
|
+
# @example Get the event type
|
|
152
|
+
# message.event_type
|
|
153
|
+
attribute :event_type
|
|
154
|
+
|
|
155
|
+
# @!attribute [r] created_at
|
|
156
|
+
# The time when the event occurred
|
|
157
|
+
# @api public
|
|
158
|
+
# @return [Time, nil] the event time
|
|
159
|
+
# @example Get the event time
|
|
160
|
+
# message.created_at
|
|
161
|
+
attribute :created_at, :time
|
|
162
|
+
|
|
163
|
+
# @!attribute [r] sender_id
|
|
164
|
+
# The identifier of the sender
|
|
165
|
+
# @api public
|
|
166
|
+
# @return [Integer, nil] the sender identifier
|
|
167
|
+
# @example Get the sender identifier
|
|
168
|
+
# message.sender_id
|
|
169
|
+
attribute :sender_id, :integer
|
|
170
|
+
|
|
171
|
+
# @!attribute [r] dm_conversation_id
|
|
172
|
+
# The identifier of the conversation
|
|
173
|
+
# @api public
|
|
174
|
+
# @return [String, nil] the conversation identifier: the identifiers of the two users of a one-to-one
|
|
175
|
+
# conversation joined with a hyphen, or the number of a group conversation
|
|
176
|
+
# @raise [InvalidAttribute] if the response holds an identifier that is neither
|
|
177
|
+
# @example Get the conversation identifier
|
|
178
|
+
# message.dm_conversation_id
|
|
179
|
+
attribute :dm_conversation_id, :conversation_id
|
|
180
|
+
|
|
181
|
+
# @!attribute [r] participant_ids
|
|
182
|
+
# The identifiers of the participants who joined or left
|
|
183
|
+
# @api public
|
|
184
|
+
# @return [Array<Integer>] the participant identifiers, empty if there are none
|
|
185
|
+
# @example Get the participant identifiers
|
|
186
|
+
# message.participant_ids
|
|
187
|
+
attribute :participant_ids, :integers
|
|
188
|
+
|
|
189
|
+
# @!attribute [r] referenced_posts
|
|
190
|
+
# The referenced posts with their identifiers
|
|
191
|
+
# @api public
|
|
192
|
+
# @return [Array<Hash>] the referenced posts, empty if there are none
|
|
193
|
+
# @example Get the referenced posts
|
|
194
|
+
# message.referenced_posts
|
|
195
|
+
attribute :referenced_posts, :objects, tweet_key: %w[referenced_tweets]
|
|
196
|
+
reference_keys.push(%w[referenced_posts], %w[referenced_tweets])
|
|
197
|
+
|
|
198
|
+
# @!attribute [r] attachments
|
|
199
|
+
# The attachment keys
|
|
200
|
+
# @api public
|
|
201
|
+
# @return [Hash, nil] the attachments
|
|
202
|
+
# @example Get the attachments
|
|
203
|
+
# message.attachments
|
|
204
|
+
attribute :attachments, :object
|
|
205
|
+
|
|
206
|
+
# @!attribute [r] entities
|
|
207
|
+
# The entities found in the text: its URLs, hashtags, mentions, and cashtags
|
|
208
|
+
# @api public
|
|
209
|
+
# @return [Hash, nil] the entities
|
|
210
|
+
# @example Get the URLs of a message
|
|
211
|
+
# message.entities&.dig("urls")
|
|
212
|
+
attribute :entities, :object
|
|
213
|
+
|
|
214
|
+
# @!method sender
|
|
215
|
+
# The sender, resolved from the includes or as a stub holding only its identifier
|
|
216
|
+
# @api public
|
|
217
|
+
# @return [User, nil] the sender
|
|
218
|
+
# @example Get the sender's username
|
|
219
|
+
# message.sender.username
|
|
220
|
+
reference :sender, :User, key: %w[sender_id]
|
|
221
|
+
|
|
222
|
+
# @!method participants
|
|
223
|
+
# The participants who joined or left, from the includes or as stubs
|
|
224
|
+
# @api public
|
|
225
|
+
# @return [Array<User>] the participants
|
|
226
|
+
# @example Get the participants
|
|
227
|
+
# message.participants
|
|
228
|
+
references :participants, :User, key: %w[participant_ids]
|
|
229
|
+
|
|
230
|
+
# @!method media
|
|
231
|
+
# The attached media, from the includes or as stubs holding only their keys
|
|
232
|
+
# @api public
|
|
233
|
+
# @return [Array<Media>] the media
|
|
234
|
+
# @example Get the media URLs
|
|
235
|
+
# message.media.map(&:url)
|
|
236
|
+
references :media, :Media, key: %w[attachments media_keys]
|
|
237
|
+
|
|
238
|
+
# The referenced posts, resolved from the includes or built as stubs
|
|
239
|
+
#
|
|
240
|
+
# @api public
|
|
241
|
+
# @return [Array<Post>] the referenced posts
|
|
242
|
+
# @raise [InvalidAttribute] if the response holds a referenced post that is not an object
|
|
243
|
+
# @example Get the referenced posts
|
|
244
|
+
# message.references
|
|
245
|
+
def references
|
|
246
|
+
referenced_posts.filter_map do |reference|
|
|
247
|
+
resolve(Post, reference["id"]) #: Post?
|
|
248
|
+
end.freeze
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Check whether a user sent this message
|
|
252
|
+
#
|
|
253
|
+
# A message that does not name its sender, as the message a new direct message returns does not, or one fetched
|
|
254
|
+
# with dm_event.fields that leave sender_id out, cannot say the user sent it, so it answers false; its sender_id
|
|
255
|
+
# is nil.
|
|
256
|
+
#
|
|
257
|
+
# @api public
|
|
258
|
+
# @param user [User, String, Integer] the user or their identifier
|
|
259
|
+
# @return [Boolean] true if the user sent the message, false if another user did, or the message does not name
|
|
260
|
+
# its sender
|
|
261
|
+
# @example Split messages into sent and received
|
|
262
|
+
# me = client.current_user_id
|
|
263
|
+
# messages.partition { |message| message.from?(me) }
|
|
264
|
+
def from?(user) = sender_id.to_s.eql?(Utils.id_of(user, User))
|
|
265
|
+
|
|
266
|
+
# Check whether the message belongs to a group conversation
|
|
267
|
+
#
|
|
268
|
+
# The identifier of a one-to-one conversation joins the identifiers of its two participants with a hyphen, and
|
|
269
|
+
# the identifier of a group conversation is a number of its own.
|
|
270
|
+
#
|
|
271
|
+
# @api public
|
|
272
|
+
# @return [Boolean] true if the message belongs to a group conversation, false if to a one-to-one conversation or
|
|
273
|
+
# the message does not say
|
|
274
|
+
# @raise [InvalidAttribute] if the response holds a conversation identifier that is not one
|
|
275
|
+
# @example Leave out the messages of group conversations
|
|
276
|
+
# client.direct_messages.reject(&:group?)
|
|
277
|
+
def group?
|
|
278
|
+
conversation_id = dm_conversation_id
|
|
279
|
+
!conversation_id.nil? && !conversation_id.include?("-")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# The other participant of a one-to-one conversation, as seen by a user
|
|
283
|
+
#
|
|
284
|
+
# The sender, when the user did not send the message, and otherwise the other member of the
|
|
285
|
+
# conversation, from the includes or as a stub holding only its identifier. A message that does not name its
|
|
286
|
+
# sender, as the message a new direct message returns does not, is read by its conversation alone, whose other
|
|
287
|
+
# member is the peer of a user who is one of its two. A message whose conversation the user is not one of the
|
|
288
|
+
# two members of has no peer for that user, even one another user sent, so neither has a message of a group
|
|
289
|
+
# conversation, whose identifier names none of its members.
|
|
290
|
+
#
|
|
291
|
+
# @api public
|
|
292
|
+
# @param user [User, String, Integer] the user, usually the authenticated user, or their identifier
|
|
293
|
+
# @return [User, nil] the other participant, or nil for a group conversation, one without another participant,
|
|
294
|
+
# or one the user is not a member of
|
|
295
|
+
# @raise [InvalidAttribute] if the response holds a conversation identifier that is not one
|
|
296
|
+
# @example Print the identifier of the user each message was exchanged with
|
|
297
|
+
# me = client.current_user_id
|
|
298
|
+
# client.direct_messages.reject(&:group?).each { |message| puts message.peer(me)&.id }
|
|
299
|
+
def peer(user)
|
|
300
|
+
user_id = Utils.id_of(user, User)
|
|
301
|
+
members = dm_conversation_id.to_s.split("-")
|
|
302
|
+
return unless members.empty? || members.include?(user_id)
|
|
303
|
+
return sender unless sender_id.nil? || from?(user_id)
|
|
304
|
+
|
|
305
|
+
resolve(User, members.find { |id| !id.eql?(user_id) }) #: User?
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Delete this direct message event as the authenticated user
|
|
309
|
+
#
|
|
310
|
+
# @api public
|
|
311
|
+
# @return [Boolean] true if the event was deleted
|
|
312
|
+
# @example Delete a direct message
|
|
313
|
+
# message.delete
|
|
314
|
+
def delete
|
|
315
|
+
self.class.delete(self, client: client!)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# The other names of attributes, as the aliases YARD reads them as
|
|
319
|
+
#
|
|
320
|
+
# @!parse
|
|
321
|
+
# alias_method :referenced_tweets, :referenced_posts
|
|
322
|
+
attribute_alias :referenced_tweets, :referenced_posts
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cursor"
|
|
4
|
+
require_relative "media_ids"
|
|
5
|
+
require_relative "shape"
|
|
6
|
+
require_relative "utils"
|
|
7
|
+
|
|
8
|
+
module X
|
|
9
|
+
module Resources
|
|
10
|
+
# Group conversations of direct messages: starting one, sending to one, and reading one, extended by DirectMessage
|
|
11
|
+
#
|
|
12
|
+
# Internal to x-resources: the methods it gives DirectMessage, such as X::DirectMessage.create_group, are public API,
|
|
13
|
+
# but the module is only how they are shared, and which classes extend or include it can change within 1.x.
|
|
14
|
+
#
|
|
15
|
+
# @api semipublic
|
|
16
|
+
module DirectMessageConversations
|
|
17
|
+
# Maximum number of events per page of a conversation
|
|
18
|
+
# @api private
|
|
19
|
+
MAX_RESULTS = 100
|
|
20
|
+
private_constant :MAX_RESULTS
|
|
21
|
+
|
|
22
|
+
# Start a group conversation, sending its first message as the authenticated user
|
|
23
|
+
#
|
|
24
|
+
# @api public
|
|
25
|
+
# @param users [Array<User, String, Integer>] the other participants or their identifiers
|
|
26
|
+
# @param text [String, nil] the text of the first message, or nil for a message of attachments alone
|
|
27
|
+
# @param client [Object] the client used to make the request
|
|
28
|
+
# @param media_ids [Array<String, Integer, #fetch, Media>, String, Integer, #fetch, Media, nil] the identifiers or
|
|
29
|
+
# media keys of uploaded media to attach, what the uploads returned, or media, such as that of a post, one or
|
|
30
|
+
# many
|
|
31
|
+
# @param params [Hash] additional fields of the message, such as attachments
|
|
32
|
+
# @return [DirectMessage] the sent message, holding only its identifiers, among them the new conversation's
|
|
33
|
+
# @raise [ArgumentError] if the message has neither text nor any other field, or has both media_ids and
|
|
34
|
+
# attachments
|
|
35
|
+
# @raise [MissingResource] if the API answers without the message
|
|
36
|
+
# @example Start a group conversation
|
|
37
|
+
# X::DirectMessage.create_group([alice, bob], "Hello, both of you!", client: client)
|
|
38
|
+
# @example Start a group conversation with an image
|
|
39
|
+
# X::DirectMessage.create_group([alice, bob], client: client, media_ids: media)
|
|
40
|
+
def create_group(users, text = nil, client:, media_ids: nil, **params)
|
|
41
|
+
body = {conversation_type: "Group", participant_ids: users.map { |user| Utils.id_of(user, User) }, message: message(text, params, media_ids)}
|
|
42
|
+
sent(client.post("dm_conversations", body, **Utils::JSON_CLASSES), "dm_conversations", client:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Send a direct message to a conversation as the authenticated user
|
|
46
|
+
#
|
|
47
|
+
# @api public
|
|
48
|
+
# @param conversation [DirectMessage, String, Integer] a message of the conversation, or the conversation's identifier
|
|
49
|
+
# @param text [String, nil] the text of the message, or nil for a message of attachments alone
|
|
50
|
+
# @param client [Object] the client used to make the request
|
|
51
|
+
# @param media_ids [Array<String, Integer, #fetch, Media>, String, Integer, #fetch, Media, nil] the identifiers or
|
|
52
|
+
# media keys of uploaded media to attach, what the uploads returned, or media, such as that of a post, one or
|
|
53
|
+
# many
|
|
54
|
+
# @param params [Hash] additional request body fields, such as attachments
|
|
55
|
+
# @return [DirectMessage] the sent message, holding only its identifiers
|
|
56
|
+
# @raise [ArgumentError] if the conversation identifier is not one, the message has neither text nor any other
|
|
57
|
+
# field, or it has both media_ids and attachments
|
|
58
|
+
# @raise [MissingResource] if the API answers without the message
|
|
59
|
+
# @example Reply to the conversation of a message
|
|
60
|
+
# X::DirectMessage.create_in(message, "Sounds good", client: client)
|
|
61
|
+
# @example Reply with an image
|
|
62
|
+
# X::DirectMessage.create_in(message, client: client, media_ids: media)
|
|
63
|
+
def create_in(conversation, text = nil, client:, media_ids: nil, **params)
|
|
64
|
+
path = "dm_conversations/#{conversation_id_of(conversation)}/messages"
|
|
65
|
+
sent(client.post(path, message(text, params, media_ids), **Utils::JSON_CLASSES), path, client:)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The direct message events of a conversation, one-to-one or group
|
|
69
|
+
#
|
|
70
|
+
# @api public
|
|
71
|
+
# @param conversation [DirectMessage, String, Integer] a message of the conversation, or the conversation's identifier
|
|
72
|
+
# @param client [Object] the client used to make the requests
|
|
73
|
+
# @param params [Hash] query parameters merged over the default parameters, such as event_types
|
|
74
|
+
# @return [Cursor] a cursor over the events
|
|
75
|
+
# @raise [ArgumentError] if the conversation identifier is not one
|
|
76
|
+
# @example Print the conversation a message belongs to
|
|
77
|
+
# X::DirectMessage.in(message, client: client).each { |event| puts event.text }
|
|
78
|
+
def in(conversation, client:, **params)
|
|
79
|
+
path = "dm_conversations/#{conversation_id_of(conversation)}/dm_events"
|
|
80
|
+
Cursor.__send__(:build, DirectMessage, path, client:, params: {max_results: MAX_RESULTS}.merge(params))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# The identifier of a conversation, from a message of it or as given
|
|
86
|
+
# @api private
|
|
87
|
+
# @param conversation [DirectMessage, String, Integer] a message of the conversation, or the conversation's identifier
|
|
88
|
+
# @return [String] the conversation identifier
|
|
89
|
+
# @raise [ArgumentError] if the identifier is not one
|
|
90
|
+
def conversation_id_of(conversation)
|
|
91
|
+
id = case conversation
|
|
92
|
+
when DirectMessage then conversation.dm_conversation_id.to_s
|
|
93
|
+
else conversation.to_s
|
|
94
|
+
end
|
|
95
|
+
return id if id.match?(Shape::CONVERSATION_ID)
|
|
96
|
+
|
|
97
|
+
raise ArgumentError, "#{conversation.inspect} is not a conversation: pass a direct message or a conversation identifier"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The fields of a message to send, which needs text or attachments
|
|
101
|
+
#
|
|
102
|
+
# The API takes media as attachments, each an object holding the identifier of one upload as a String, so
|
|
103
|
+
# media_ids builds them, and a caller who builds them itself passes attachments instead, by a String or a Symbol.
|
|
104
|
+
#
|
|
105
|
+
# @api private
|
|
106
|
+
# @param text [String, nil] the text of the message
|
|
107
|
+
# @param params [Hash] additional fields of the message, such as attachments
|
|
108
|
+
# @param media_ids [Array, #fetch, Media, String, Integer, nil] the identifiers of uploaded media to attach, what
|
|
109
|
+
# the uploads returned, or media, one or many; an empty list attaches nothing, as nil does
|
|
110
|
+
# @return [Hash{Symbol => Object}] the fields, without the text when there is none
|
|
111
|
+
# @raise [ArgumentError] if the message has neither text nor any other field, or attaches media by both
|
|
112
|
+
# media_ids and attachments
|
|
113
|
+
def message(text, params, media_ids)
|
|
114
|
+
fields = {text:, **Utils.fields(params)}.compact
|
|
115
|
+
attachments = MediaIds.media_ids_of(media_ids).map { |media_id| {media_id:} }
|
|
116
|
+
unless attachments.empty?
|
|
117
|
+
raise ArgumentError, "pass media_ids or attachments, not both" if fields.key?(:attachments)
|
|
118
|
+
|
|
119
|
+
fields[:attachments] = attachments
|
|
120
|
+
end
|
|
121
|
+
raise ArgumentError, "a direct message needs text, or something else to show, such as media_ids" if fields.empty?
|
|
122
|
+
|
|
123
|
+
fields
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The message a send created, from the identifiers the API returned
|
|
127
|
+
#
|
|
128
|
+
# It is built as the resources of any response are, so a response without the message, or without its event
|
|
129
|
+
# identifier, raises, as any request that creates a resource does.
|
|
130
|
+
#
|
|
131
|
+
# @api private
|
|
132
|
+
# @param body [Hash, nil] the response body
|
|
133
|
+
# @param path [String] the path the message was sent to
|
|
134
|
+
# @param client [Object] the client used to make the request
|
|
135
|
+
# @return [DirectMessage] the message
|
|
136
|
+
# @raise [MissingResource] if the response holds no data, or no event identifier
|
|
137
|
+
# @raise [InvalidAttribute] if the response holds an event identifier that is not one
|
|
138
|
+
def sent(body, path, client:)
|
|
139
|
+
body = body.to_h
|
|
140
|
+
data = body["data"]
|
|
141
|
+
data = {"id" => data["dm_event_id"], "dm_conversation_id" => data["dm_conversation_id"]} if data.is_a?(Hash)
|
|
142
|
+
created_from_response(body.merge("data" => data), "POST #{path}", client:)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
private_constant :DirectMessageConversations
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "x/core"
|
|
4
|
+
|
|
5
|
+
module X
|
|
6
|
+
module Resources
|
|
7
|
+
# Base error class for the failures of the object layer, which every error x-resources raises of its own descends from
|
|
8
|
+
#
|
|
9
|
+
# It descends from X::Error, so that rescuing the errors of the X API catches one of them as well. The errors
|
|
10
|
+
# that descend from it are named directly under X, as the errors of x-core are, so that this is the one name
|
|
11
|
+
# under X::Resources a rescue reaches for: it catches the failure of the object layer alone.
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
class Error < X::Error; end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Raised when a resource that was asked for by identifier or name does not exist
|
|
18
|
+
#
|
|
19
|
+
# A lookup of one resource that is not there raises it whether the API answers 200 OK with no data or a 404 that
|
|
20
|
+
# reports the resource as not found, so code that rescues it need not know which. It is not
|
|
21
|
+
# the NotFound of x-core, which holds the response, and which every other 404 raises, such as one from a client
|
|
22
|
+
# pointed at the wrong host or API version; the NotFound of a 404 that reports the resource is the cause of this.
|
|
23
|
+
#
|
|
24
|
+
# It is raised as well when a request that creates a resource, such as X::Post.create, succeeds without returning
|
|
25
|
+
# it, as X::User.current! raises it when users/me returns no user, holding the problems the response reported.
|
|
26
|
+
#
|
|
27
|
+
# @api public
|
|
28
|
+
class MissingResource < Resources::Error
|
|
29
|
+
# The problems the API reported about the resource
|
|
30
|
+
# @api public
|
|
31
|
+
# @return [Array<Problem>] the problems, empty if the API reported none
|
|
32
|
+
# @example Read why a user was not found
|
|
33
|
+
# error.problems.first&.detail # => "Could not find user with username: [nobody]."
|
|
34
|
+
attr_reader :problems
|
|
35
|
+
|
|
36
|
+
# Initialize the error, adding the detail of the first problem to the message
|
|
37
|
+
#
|
|
38
|
+
# @api public
|
|
39
|
+
# @param message [String, nil] the message
|
|
40
|
+
# @param problems [Array<Problem>] the problems the API reported
|
|
41
|
+
# @return [MissingResource] a new error
|
|
42
|
+
# @example Raise the error
|
|
43
|
+
# raise X::MissingResource.new("Could not find X::User nobody", problems: problems)
|
|
44
|
+
def initialize(message = nil, problems: [])
|
|
45
|
+
explanation = problems.first&.then { |problem| problem.detail || problem.title }
|
|
46
|
+
super(([message, explanation].compact.join(": ") if message || explanation))
|
|
47
|
+
@problems = problems.dup.freeze
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Raised when a successful response holds what the object layer cannot read as what the API documents
|
|
52
|
+
#
|
|
53
|
+
# It is the base of InvalidAttribute, for one value of a response, and is raised itself for what a response says
|
|
54
|
+
# beside its values, such as a page that names the token of a page before it as the next, which would have the
|
|
55
|
+
# pages requested again for good. It is not the InvalidResponse of x-core, which a body that is not JSON raises,
|
|
56
|
+
# and which holds the response.
|
|
57
|
+
#
|
|
58
|
+
# @api public
|
|
59
|
+
# @example Rescue what the object layer cannot read of a response
|
|
60
|
+
# begin
|
|
61
|
+
# user.followers.to_a
|
|
62
|
+
# rescue X::UnreadableResponse => e
|
|
63
|
+
# logger.warn(e.message)
|
|
64
|
+
# end
|
|
65
|
+
class UnreadableResponse < Resources::Error; end
|
|
66
|
+
|
|
67
|
+
# Raised when a response holds a value that cannot be read as what the API documents it to be
|
|
68
|
+
#
|
|
69
|
+
# A timestamp that is not ISO 8601, or an identifier that is not one, is read when the attribute or the reference
|
|
70
|
+
# that holds it is read, and the identifier of a resource when the resource is built from the response, so that one
|
|
71
|
+
# value the object layer cannot read raises where it is read, as this error, which descends from X::Error, rather
|
|
72
|
+
# than as the ArgumentError the same value raises when a caller passes it. The cause is the error that refused it.
|
|
73
|
+
#
|
|
74
|
+
# @api public
|
|
75
|
+
class InvalidAttribute < UnreadableResponse; end
|
|
76
|
+
|
|
77
|
+
# Raised when a resource that holds no client is asked for what only a request can answer
|
|
78
|
+
#
|
|
79
|
+
# A resource built without a client, such as one Marshal read back, or one built with from_id and no client, holds
|
|
80
|
+
# its attributes, which it reads as any resource does, but cannot hydrate, refresh, page a collection, or act, since
|
|
81
|
+
# each of them is a request, and the client is what makes it. Build the resource with the client: of from_id, or
|
|
82
|
+
# look it up again with a client.
|
|
83
|
+
#
|
|
84
|
+
# @api public
|
|
85
|
+
# @example Hydrate a resource that has a client
|
|
86
|
+
# X::User.from_id(7_505_382, client: client).hydrate
|
|
87
|
+
class MissingClient < Resources::Error; end
|
|
88
|
+
|
|
89
|
+
# Raised when a scan or a count reads the pages its max_pages allows, and the API names a page after them
|
|
90
|
+
#
|
|
91
|
+
# A check that pages through a collection, such as List#member? or User#follows?, and a count of posts, such as
|
|
92
|
+
# X::Post.count_all, read as many pages as the answer takes, and the API bills each one. Given max_pages,
|
|
93
|
+
# each reads no more pages than that, and raises this rather than answer from the pages it read, which would be
|
|
94
|
+
# wrong: a member on a page it did not read, or posts counted on one, would go unseen.
|
|
95
|
+
#
|
|
96
|
+
# @api public
|
|
97
|
+
# @example Give up on a check that would read more than ten pages
|
|
98
|
+
# begin
|
|
99
|
+
# list.member?(user, max_pages: 10)
|
|
100
|
+
# rescue X::PageLimitReached
|
|
101
|
+
# nil
|
|
102
|
+
# end
|
|
103
|
+
class PageLimitReached < Resources::Error; end
|
|
104
|
+
end
|