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,615 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi/escape"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require_relative "batch_finders"
|
|
7
|
+
require_relative "community"
|
|
8
|
+
require_relative "matching_rule"
|
|
9
|
+
require_relative "cursor"
|
|
10
|
+
require_relative "post_collections"
|
|
11
|
+
require_relative "post_counts"
|
|
12
|
+
require_relative "post_search"
|
|
13
|
+
require_relative "post_writes"
|
|
14
|
+
require_relative "references"
|
|
15
|
+
require_relative "resource"
|
|
16
|
+
|
|
17
|
+
module X
|
|
18
|
+
module Resources
|
|
19
|
+
# A post, also known as a tweet
|
|
20
|
+
# @api public
|
|
21
|
+
class ::X::Post < Resource
|
|
22
|
+
# The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X; they
|
|
23
|
+
# stand apart from the mixins, since YARD reads a comment that code follows as the documentation of that code
|
|
24
|
+
#
|
|
25
|
+
# @!parse
|
|
26
|
+
# include X::Resources::References
|
|
27
|
+
# include X::Resources::PostCollections
|
|
28
|
+
# extend X::Resources::BatchFinders
|
|
29
|
+
# extend X::Resources::PostCounts
|
|
30
|
+
# extend X::Resources::PostSearch
|
|
31
|
+
# extend X::Resources::PostWrites
|
|
32
|
+
|
|
33
|
+
# Every public post field; the identifiers of referenced resources come with their expansions
|
|
34
|
+
#
|
|
35
|
+
# The metrics that only the author, or an advertiser, may read, non_public_metrics, organic_metrics, and
|
|
36
|
+
# promoted_metrics, are left out, since a field that depends on who is authenticated would make every request
|
|
37
|
+
# fail for a client that cannot read it, as are the fields of Community Notes and of suggested sources, which the
|
|
38
|
+
# API gives to the programs they belong to. So is source, which the API has deprecated: a field it stops taking
|
|
39
|
+
# would make every request that asks for it fail. Nor does a post read it; a request that names it in its
|
|
40
|
+
# post.fields finds it in {Resource#attrs}.
|
|
41
|
+
#
|
|
42
|
+
# A minor release may add to it the fields the API adds, so that a lookup asks for them too; see
|
|
43
|
+
# {Resource#hydrated?} for what that means for a resource looked up with a list of fields of its own.
|
|
44
|
+
FIELDS = %w[article article_title attachments card_uri community_id context_annotations conversation_id
|
|
45
|
+
created_at display_text_range edit_controls entities geo id lang media_metadata note_post paid_partnership
|
|
46
|
+
possibly_sensitive public_metrics reply_settings scopes text withheld].freeze
|
|
47
|
+
# The expansions of the resources a post refers to that the object layer resolves
|
|
48
|
+
#
|
|
49
|
+
# The identifiers of a post's edit history come with every post, so the edit_history_post_ids expansion, which
|
|
50
|
+
# would include each version of the post again, including the post itself, is left out, as is
|
|
51
|
+
# entities.mentions.username, which would include each user the post mentions, since nothing reads them.
|
|
52
|
+
#
|
|
53
|
+
# A minor release may add to it the expansions the API adds, so that a lookup asks for them too; see
|
|
54
|
+
# {Resource#hydrated?} for what that means for a resource looked up with a list of expansions of its own.
|
|
55
|
+
EXPANSIONS = %w[attachments.media_keys attachments.media_source_tweet attachments.poll_ids author_id geo.place_id
|
|
56
|
+
in_reply_to_user_id referenced_posts].freeze
|
|
57
|
+
|
|
58
|
+
include References
|
|
59
|
+
include PostCollections
|
|
60
|
+
extend BatchFinders
|
|
61
|
+
extend PostCounts
|
|
62
|
+
extend PostSearch
|
|
63
|
+
extend PostWrites
|
|
64
|
+
|
|
65
|
+
class << self
|
|
66
|
+
# The API endpoint used to look up posts by identifier
|
|
67
|
+
#
|
|
68
|
+
# @api private
|
|
69
|
+
# @return [String] the endpoint
|
|
70
|
+
# @example Get the endpoint
|
|
71
|
+
# X::Post.__send__(:endpoint) # => "tweets"
|
|
72
|
+
def endpoint = "tweets"
|
|
73
|
+
|
|
74
|
+
# The key under which posts appear in the includes of a response
|
|
75
|
+
#
|
|
76
|
+
# @api private
|
|
77
|
+
# @return [String] the includes key
|
|
78
|
+
# @example Get the includes key
|
|
79
|
+
# X::Post.__send__(:includes_key) # => "posts"
|
|
80
|
+
def includes_key = "posts"
|
|
81
|
+
|
|
82
|
+
# The query parameter that selects post fields
|
|
83
|
+
#
|
|
84
|
+
# @api private
|
|
85
|
+
# @return [String] the fields parameter
|
|
86
|
+
# @example Get the fields parameter
|
|
87
|
+
# X::Post.__send__(:fields_key) # => "post.fields"
|
|
88
|
+
def fields_key = "post.fields"
|
|
89
|
+
|
|
90
|
+
private :endpoint, :includes_key, :fields_key
|
|
91
|
+
|
|
92
|
+
# The default query parameters requesting every post field and expansion
|
|
93
|
+
#
|
|
94
|
+
# @api public
|
|
95
|
+
# @return [Hash{String => Array<String>}] the default query parameters
|
|
96
|
+
# @example Get the default parameters
|
|
97
|
+
# X::Post.default_params["post.fields"]
|
|
98
|
+
def default_params
|
|
99
|
+
{"post.fields" => FIELDS, "user.fields" => User::FIELDS, "media.fields" => Media::FIELDS,
|
|
100
|
+
"poll.fields" => Poll::FIELDS, "place.fields" => Place::FIELDS, "expansions" => EXPANSIONS}
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The full text
|
|
105
|
+
#
|
|
106
|
+
# A long post, of more than 280 characters, holds its full text in note_post, and a text cut short with an
|
|
107
|
+
# ellipsis and a link to the post; this reads the full text.
|
|
108
|
+
#
|
|
109
|
+
# It is the text as the API sends it, which escapes &, <, and > as &, <, and >, and it stays so
|
|
110
|
+
# throughout 1.x, so unescape it to display it.
|
|
111
|
+
#
|
|
112
|
+
# @api public
|
|
113
|
+
# @return [String, nil] the text, HTML-escaped as the API sends it
|
|
114
|
+
# @raise [InvalidAttribute] if the response holds a note_post that is not an object
|
|
115
|
+
# @example Get the text
|
|
116
|
+
# post.text # => "Ruby & Rails"
|
|
117
|
+
# @example Display the text
|
|
118
|
+
# CGI.unescapeHTML(post.text) # => "Ruby & Rails"
|
|
119
|
+
def text = full["text"]
|
|
120
|
+
|
|
121
|
+
# @!attribute [r] lang
|
|
122
|
+
# The BCP 47 language tag
|
|
123
|
+
# @api public
|
|
124
|
+
# @return [String, nil] the language tag
|
|
125
|
+
# @example Get the language
|
|
126
|
+
# post.lang
|
|
127
|
+
attribute :lang
|
|
128
|
+
|
|
129
|
+
# @!attribute [r] created_at
|
|
130
|
+
# The time when the post was created
|
|
131
|
+
# @api public
|
|
132
|
+
# @return [Time, nil] the creation time
|
|
133
|
+
# @example Get the creation time
|
|
134
|
+
# post.created_at
|
|
135
|
+
attribute :created_at, :time
|
|
136
|
+
|
|
137
|
+
# @!attribute [r] author_id
|
|
138
|
+
# The identifier of the author
|
|
139
|
+
# @api public
|
|
140
|
+
# @return [Integer, nil] the author identifier
|
|
141
|
+
# @example Get the author identifier
|
|
142
|
+
# post.author_id
|
|
143
|
+
attribute :author_id, :integer
|
|
144
|
+
|
|
145
|
+
# @!attribute [r] conversation_id
|
|
146
|
+
# The identifier of the first post in the conversation
|
|
147
|
+
# @api public
|
|
148
|
+
# @return [Integer, nil] the conversation identifier
|
|
149
|
+
# @example Get the conversation identifier
|
|
150
|
+
# post.conversation_id
|
|
151
|
+
attribute :conversation_id, :integer
|
|
152
|
+
|
|
153
|
+
# @!attribute [r] community_id
|
|
154
|
+
# The identifier of the community the post was made in
|
|
155
|
+
# @api public
|
|
156
|
+
# @return [Integer, nil] the community identifier
|
|
157
|
+
# @example Get the community identifier
|
|
158
|
+
# post.community_id
|
|
159
|
+
attribute :community_id, :integer
|
|
160
|
+
|
|
161
|
+
# @!attribute [r] in_reply_to_user_id
|
|
162
|
+
# The identifier of the user being replied to
|
|
163
|
+
# @api public
|
|
164
|
+
# @return [Integer, nil] the replied-to user identifier
|
|
165
|
+
# @example Get the replied-to user identifier
|
|
166
|
+
# post.in_reply_to_user_id
|
|
167
|
+
attribute :in_reply_to_user_id, :integer
|
|
168
|
+
|
|
169
|
+
# @!attribute [r] possibly_sensitive
|
|
170
|
+
# Whether the post may contain sensitive content
|
|
171
|
+
# @api public
|
|
172
|
+
# @return [Boolean, nil] true if the post may be sensitive
|
|
173
|
+
# @example Check whether a post may be sensitive
|
|
174
|
+
# post.possibly_sensitive?
|
|
175
|
+
attribute :possibly_sensitive, :boolean
|
|
176
|
+
|
|
177
|
+
# @!method possibly_sensitive?
|
|
178
|
+
# Check whether the post may contain sensitive content
|
|
179
|
+
# @api public
|
|
180
|
+
# @return [Boolean] true if the post may be sensitive
|
|
181
|
+
# @example Check whether the post may contain sensitive content
|
|
182
|
+
# post.possibly_sensitive?
|
|
183
|
+
|
|
184
|
+
# @!attribute [r] reply_settings
|
|
185
|
+
# Who can reply: everyone, mentionedUsers, or following
|
|
186
|
+
# @api public
|
|
187
|
+
# @return [String, nil] the reply settings
|
|
188
|
+
# @example Get the reply settings
|
|
189
|
+
# post.reply_settings
|
|
190
|
+
attribute :reply_settings
|
|
191
|
+
|
|
192
|
+
# @!attribute [r] edit_history_post_ids
|
|
193
|
+
# The identifiers of every version of the post
|
|
194
|
+
# @api public
|
|
195
|
+
# @return [Array<Integer>] the edit history identifiers, empty if there are none
|
|
196
|
+
# @example Get the edit history identifiers
|
|
197
|
+
# post.edit_history_post_ids
|
|
198
|
+
attribute :edit_history_post_ids, :integers, tweet_key: %w[edit_history_tweet_ids]
|
|
199
|
+
|
|
200
|
+
# @!attribute [r] edit_controls
|
|
201
|
+
# The edit controls
|
|
202
|
+
# @api public
|
|
203
|
+
# @return [Hash, nil] the edit controls
|
|
204
|
+
# @example Get the edit controls
|
|
205
|
+
# post.edit_controls
|
|
206
|
+
attribute :edit_controls, :object
|
|
207
|
+
|
|
208
|
+
# @!attribute [r] display_text_range
|
|
209
|
+
# The range of characters of the text the API gives the post that is shown
|
|
210
|
+
#
|
|
211
|
+
# It leaves out the mentions a reply begins with and the link to media it ends with. It is a range of the text
|
|
212
|
+
# the post holds itself, which for a long post is the text cut short, not the full text {#text} reads.
|
|
213
|
+
#
|
|
214
|
+
# @api public
|
|
215
|
+
# @return [Range<Integer>, nil] the range, which leaves out its end, or nil if the response holds none
|
|
216
|
+
# @raise [InvalidAttribute] if the response holds something other than the start and end of a range
|
|
217
|
+
# @example Read the text that is shown
|
|
218
|
+
# post.display_text_range&.then { |range| post.attrs["text"][range] }
|
|
219
|
+
attribute :display_text_range, :range
|
|
220
|
+
|
|
221
|
+
# @!attribute [r] scopes
|
|
222
|
+
# Who may see the post
|
|
223
|
+
#
|
|
224
|
+
# A post its author shared with followers alone holds `{"followers" => true}`.
|
|
225
|
+
#
|
|
226
|
+
# @api public
|
|
227
|
+
# @return [Hash, nil] the scopes
|
|
228
|
+
# @example Check whether the post is shared with followers alone
|
|
229
|
+
# post.scopes&.fetch("followers", false)
|
|
230
|
+
attribute :scopes, :object
|
|
231
|
+
|
|
232
|
+
# @!attribute [r] card_uri
|
|
233
|
+
# The URI of the card the post shows
|
|
234
|
+
# @api public
|
|
235
|
+
# @return [String, nil] the card URI
|
|
236
|
+
# @example Get the card URI
|
|
237
|
+
# post.card_uri
|
|
238
|
+
attribute :card_uri
|
|
239
|
+
|
|
240
|
+
# @!attribute [r] article
|
|
241
|
+
# The article the post publishes, with its title
|
|
242
|
+
# @api public
|
|
243
|
+
# @return [Hash, nil] the article
|
|
244
|
+
# @example Get the title of an article
|
|
245
|
+
# post.article&.fetch("title")
|
|
246
|
+
attribute :article, :object
|
|
247
|
+
|
|
248
|
+
# @!attribute [r] article_title
|
|
249
|
+
# What the API describes of the title of the article the post publishes
|
|
250
|
+
# @api public
|
|
251
|
+
# @return [Hash, nil] the metadata of the article, or nil for a post that publishes none
|
|
252
|
+
# @example Get the metadata of the title of an article
|
|
253
|
+
# post.article_title
|
|
254
|
+
attribute :article_title, :object
|
|
255
|
+
|
|
256
|
+
# @!attribute [r] media_metadata
|
|
257
|
+
# What the post describes of the media it attaches
|
|
258
|
+
# @api public
|
|
259
|
+
# @return [Array<Hash>] the metadata of each medium, empty if there is none
|
|
260
|
+
# @example Get the metadata of the media
|
|
261
|
+
# post.media_metadata
|
|
262
|
+
attribute :media_metadata, :objects
|
|
263
|
+
|
|
264
|
+
# @!attribute [r] paid_partnership
|
|
265
|
+
# Whether the post is a paid partnership
|
|
266
|
+
# @api public
|
|
267
|
+
# @return [Boolean, nil] true if the post is a paid partnership
|
|
268
|
+
# @example Check whether a post is a paid partnership
|
|
269
|
+
# post.paid_partnership?
|
|
270
|
+
attribute :paid_partnership, :boolean
|
|
271
|
+
|
|
272
|
+
# @!method paid_partnership?
|
|
273
|
+
# Check whether the post is a paid partnership
|
|
274
|
+
# @api public
|
|
275
|
+
# @return [Boolean] true if the post is a paid partnership
|
|
276
|
+
# @example Check whether the post is a paid partnership
|
|
277
|
+
# post.paid_partnership?
|
|
278
|
+
|
|
279
|
+
# The entities found in the full text
|
|
280
|
+
#
|
|
281
|
+
# The entities, such as links, mentions, and hashtags, come from note_post for a long post, whose note the API
|
|
282
|
+
# gives entities when the full text has any, and from the post itself for a short one, so that each lies where
|
|
283
|
+
# {#text} holds it. A long post whose note holds none has none: the entities the post holds itself, such as its
|
|
284
|
+
# annotations, lie in the text cut short, which attrs holds.
|
|
285
|
+
#
|
|
286
|
+
# @api public
|
|
287
|
+
# @return [Hash, nil] the entities
|
|
288
|
+
# @raise [InvalidAttribute] if the response holds a note_post, or entities, that is not an object
|
|
289
|
+
# @example Get the entities
|
|
290
|
+
# post.entities
|
|
291
|
+
def entities = Shape.read_object("#{self.class}#entities", full["entities"])
|
|
292
|
+
|
|
293
|
+
# The links in the full text, each with its shortened url and its expanded_url
|
|
294
|
+
#
|
|
295
|
+
# @api public
|
|
296
|
+
# @return [Array<Hash>] the links, empty if there are none
|
|
297
|
+
# @raise [InvalidAttribute] if the response holds entities, or links, that are not what the API documents
|
|
298
|
+
# @example Get the links
|
|
299
|
+
# post.urls # => [{"url" => "https://t.co/...", "expanded_url" => "https://github.com/sferik/x-ruby", ...}]
|
|
300
|
+
def urls = Shape.objects("#{self.class}#urls", entities&.[]("urls"))
|
|
301
|
+
|
|
302
|
+
# The rules of the filtered stream this post matched
|
|
303
|
+
#
|
|
304
|
+
# A post the filtered stream delivers names the rules it matched, and any other post names none. The streaming
|
|
305
|
+
# client of x-streams builds the posts of a stream given X::Post as its object_class.
|
|
306
|
+
#
|
|
307
|
+
# @api public
|
|
308
|
+
# @return [Array<MatchingRule>] the rules, empty for a post that did not come from the filtered stream
|
|
309
|
+
# @raise [InvalidAttribute] if the response holds the rules as something other than a list of objects, or a rule
|
|
310
|
+
# without an identifier that is a number, or with a tag that is not a String
|
|
311
|
+
# @example Print the tags of the rules each post of the filtered stream matched
|
|
312
|
+
# streaming_client.stream("tweets/search/stream", object_class: X::Post) { |post| p post.matching_rules.map(&:tag) }
|
|
313
|
+
def matching_rules
|
|
314
|
+
reader = "#{self.class}#matching_rules"
|
|
315
|
+
Shape.objects(reader, attrs["matching_rules"]).map { |rule| Utils.read(reader, rule) { MatchingRule.new(rule) } }.freeze
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
attribute_names.push(:text, :entities, :urls, :matching_rules)
|
|
319
|
+
|
|
320
|
+
# @!attribute [r] context_annotations
|
|
321
|
+
# The context annotations
|
|
322
|
+
# @api public
|
|
323
|
+
# @return [Array<Hash>] the context annotations, empty if there are none
|
|
324
|
+
# @example Get the context annotations
|
|
325
|
+
# post.context_annotations
|
|
326
|
+
attribute :context_annotations, :objects
|
|
327
|
+
|
|
328
|
+
# @!attribute [r] referenced_posts
|
|
329
|
+
# The referenced posts with their types and identifiers
|
|
330
|
+
# @api public
|
|
331
|
+
# @return [Array<Hash>] the referenced posts, empty if there are none
|
|
332
|
+
# @example Get the referenced posts
|
|
333
|
+
# post.referenced_posts
|
|
334
|
+
attribute :referenced_posts, :objects, tweet_key: %w[referenced_tweets]
|
|
335
|
+
reference_keys.push(%w[referenced_posts], %w[referenced_tweets])
|
|
336
|
+
|
|
337
|
+
# @!attribute [r] attachments
|
|
338
|
+
# The attachment keys and identifiers
|
|
339
|
+
# @api public
|
|
340
|
+
# @return [Hash, nil] the attachments
|
|
341
|
+
# @example Get the attachments
|
|
342
|
+
# post.attachments
|
|
343
|
+
attribute :attachments, :object
|
|
344
|
+
|
|
345
|
+
# @!attribute [r] coordinates
|
|
346
|
+
# The longitude and latitude the post was tagged with
|
|
347
|
+
# @api public
|
|
348
|
+
# @return [Array<Numeric>, nil] the longitude and latitude, each an Integer when the API gives a whole number
|
|
349
|
+
# @example Get the coordinates
|
|
350
|
+
# post.coordinates # => [-122.4, 37.8]
|
|
351
|
+
attribute :coordinates, key: %w[geo coordinates coordinates]
|
|
352
|
+
|
|
353
|
+
# @!attribute [r] geo
|
|
354
|
+
# The tagged place and coordinates
|
|
355
|
+
# @api public
|
|
356
|
+
# @return [Hash, nil] the geo details
|
|
357
|
+
# @example Get the geo details
|
|
358
|
+
# post.geo
|
|
359
|
+
attribute :geo, :object
|
|
360
|
+
|
|
361
|
+
# @!attribute [r] withheld
|
|
362
|
+
# The withholding details
|
|
363
|
+
# @api public
|
|
364
|
+
# @return [Hash, nil] the withholding details
|
|
365
|
+
# @example Get the withholding details
|
|
366
|
+
# post.withheld
|
|
367
|
+
attribute :withheld, :object
|
|
368
|
+
|
|
369
|
+
# @!attribute [r] note_post
|
|
370
|
+
# The full text and entities of a long post
|
|
371
|
+
# @api public
|
|
372
|
+
# @return [Hash, nil] the note details
|
|
373
|
+
# @example Get the note details
|
|
374
|
+
# post.note_post
|
|
375
|
+
attribute :note_post, :object, tweet_key: %w[note_tweet]
|
|
376
|
+
|
|
377
|
+
# @!attribute [r] public_metrics
|
|
378
|
+
# The public metrics
|
|
379
|
+
# @api public
|
|
380
|
+
# @return [Hash, nil] the public metrics
|
|
381
|
+
# @example Get the public metrics
|
|
382
|
+
# post.public_metrics
|
|
383
|
+
attribute :public_metrics, :object
|
|
384
|
+
|
|
385
|
+
# @!attribute [r] repost_count
|
|
386
|
+
# The number of reposts
|
|
387
|
+
# @api public
|
|
388
|
+
# @return [Integer, nil] the repost count
|
|
389
|
+
# @example Get the repost count
|
|
390
|
+
# post.repost_count
|
|
391
|
+
attribute :repost_count, :integer, key: %w[public_metrics repost_count], tweet_key: %w[public_metrics retweet_count]
|
|
392
|
+
|
|
393
|
+
# @!attribute [r] reply_count
|
|
394
|
+
# The number of replies
|
|
395
|
+
# @api public
|
|
396
|
+
# @return [Integer, nil] the reply count
|
|
397
|
+
# @example Get the reply count
|
|
398
|
+
# post.reply_count
|
|
399
|
+
attribute :reply_count, :integer, key: %w[public_metrics reply_count]
|
|
400
|
+
|
|
401
|
+
# @!attribute [r] like_count
|
|
402
|
+
# The number of likes
|
|
403
|
+
# @api public
|
|
404
|
+
# @return [Integer, nil] the like count
|
|
405
|
+
# @example Get the like count
|
|
406
|
+
# post.like_count
|
|
407
|
+
attribute :like_count, :integer, key: %w[public_metrics like_count]
|
|
408
|
+
|
|
409
|
+
# @!attribute [r] quote_count
|
|
410
|
+
# The number of quotes
|
|
411
|
+
# @api public
|
|
412
|
+
# @return [Integer, nil] the quote count
|
|
413
|
+
# @example Get the quote count
|
|
414
|
+
# post.quote_count
|
|
415
|
+
attribute :quote_count, :integer, key: %w[public_metrics quote_count]
|
|
416
|
+
|
|
417
|
+
# @!attribute [r] bookmark_count
|
|
418
|
+
# The number of bookmarks
|
|
419
|
+
# @api public
|
|
420
|
+
# @return [Integer, nil] the bookmark count
|
|
421
|
+
# @example Get the bookmark count
|
|
422
|
+
# post.bookmark_count
|
|
423
|
+
attribute :bookmark_count, :integer, key: %w[public_metrics bookmark_count]
|
|
424
|
+
|
|
425
|
+
# @!attribute [r] impression_count
|
|
426
|
+
# The number of impressions
|
|
427
|
+
# @api public
|
|
428
|
+
# @return [Integer, nil] the impression count
|
|
429
|
+
# @example Get the impression count
|
|
430
|
+
# post.impression_count
|
|
431
|
+
attribute :impression_count, :integer, key: %w[public_metrics impression_count]
|
|
432
|
+
|
|
433
|
+
# @!method author
|
|
434
|
+
# The author, resolved from the includes or as a stub holding only its identifier
|
|
435
|
+
# @api public
|
|
436
|
+
# @return [User, nil] the author
|
|
437
|
+
# @example Get the author's username
|
|
438
|
+
# post.author.username
|
|
439
|
+
reference :author, :User, key: %w[author_id]
|
|
440
|
+
|
|
441
|
+
# @!method in_reply_to_user
|
|
442
|
+
# The user being replied to, resolved from the includes or built as a stub
|
|
443
|
+
# @api public
|
|
444
|
+
# @return [User, nil] the replied-to user
|
|
445
|
+
# @example Get the replied-to user
|
|
446
|
+
# post.in_reply_to_user
|
|
447
|
+
reference :in_reply_to_user, :User, key: %w[in_reply_to_user_id]
|
|
448
|
+
|
|
449
|
+
# @!method community
|
|
450
|
+
# The community the post was made in, as a stub holding only its identifier
|
|
451
|
+
# @api public
|
|
452
|
+
# @return [Community, nil] the community
|
|
453
|
+
# @example Get the community's name
|
|
454
|
+
# post.community.hydrate.name
|
|
455
|
+
reference :community, :Community, key: %w[community_id]
|
|
456
|
+
|
|
457
|
+
# @!method place
|
|
458
|
+
# The tagged place, from the includes or as a stub holding only its identifier
|
|
459
|
+
# @api public
|
|
460
|
+
# @return [Place, nil] the place
|
|
461
|
+
# @example Get the place
|
|
462
|
+
# post.place
|
|
463
|
+
reference :place, :Place, key: %w[geo place_id]
|
|
464
|
+
|
|
465
|
+
# @!method media
|
|
466
|
+
# The attached media, from the includes or as stubs holding only their keys
|
|
467
|
+
# @api public
|
|
468
|
+
# @return [Array<Media>] the media
|
|
469
|
+
# @example Get the media URLs
|
|
470
|
+
# post.media.map(&:url)
|
|
471
|
+
references :media, :Media, key: %w[attachments media_keys]
|
|
472
|
+
|
|
473
|
+
# @!method polls
|
|
474
|
+
# The attached polls, from the includes or as stubs holding only their identifiers
|
|
475
|
+
# @api public
|
|
476
|
+
# @return [Array<Poll>] the polls
|
|
477
|
+
# @example Get the poll options
|
|
478
|
+
# post.polls.first.options
|
|
479
|
+
references :polls, :Poll, key: %w[attachments poll_ids]
|
|
480
|
+
|
|
481
|
+
# @!method media_source_posts
|
|
482
|
+
# The posts the attached media was first posted with
|
|
483
|
+
#
|
|
484
|
+
# A post that attaches media another post was made with, as one that shares a video does, names that post as
|
|
485
|
+
# the source of the media. Each is read from the includes, or built as a stub holding only its identifier.
|
|
486
|
+
#
|
|
487
|
+
# @api public
|
|
488
|
+
# @return [Array<Post>] the posts, empty if the media was first posted with this post, or it has none
|
|
489
|
+
# @example Credit the author of shared media
|
|
490
|
+
# post.media_source_posts.map { |source| source.author&.username }
|
|
491
|
+
references :media_source_posts, :Post, key: %w[attachments media_source_tweet_id]
|
|
492
|
+
|
|
493
|
+
# The other names of attributes, as the aliases YARD reads them as
|
|
494
|
+
#
|
|
495
|
+
# @!parse
|
|
496
|
+
# alias_method :retweet_count, :repost_count
|
|
497
|
+
# alias_method :edit_history_tweet_ids, :edit_history_post_ids
|
|
498
|
+
# alias_method :note_tweet, :note_post
|
|
499
|
+
# alias_method :referenced_tweets, :referenced_posts
|
|
500
|
+
attribute_alias :retweet_count, :repost_count
|
|
501
|
+
attribute_alias :edit_history_tweet_ids, :edit_history_post_ids
|
|
502
|
+
attribute_alias :note_tweet, :note_post
|
|
503
|
+
attribute_alias :referenced_tweets, :referenced_posts
|
|
504
|
+
alias_method :media_source_tweets, :media_source_posts
|
|
505
|
+
|
|
506
|
+
# The permalink of the post, by the author's username when known
|
|
507
|
+
#
|
|
508
|
+
# @api public
|
|
509
|
+
# @return [String] the x.com address of the post
|
|
510
|
+
# @raise [InvalidAttribute] if the response holds the username of the author as something other than a String
|
|
511
|
+
# @example Get the permalink
|
|
512
|
+
# post.permalink # => "https://x.com/sferik/status/1234567890"
|
|
513
|
+
def permalink = "https://x.com/#{Shape.read_string("#{self.class}#permalink", author&.username) || "i"}/status/#{id}"
|
|
514
|
+
|
|
515
|
+
# The permalink of the post as a URI
|
|
516
|
+
#
|
|
517
|
+
# @api public
|
|
518
|
+
# @return [URI::Generic] the x.com address of the post
|
|
519
|
+
# @raise [InvalidAttribute] if the response holds the username of the author as something other than a String
|
|
520
|
+
# @example Get the address as a URI
|
|
521
|
+
# post.uri # => #<URI::HTTPS https://x.com/sferik/status/1234567890>
|
|
522
|
+
def uri = URI(permalink)
|
|
523
|
+
|
|
524
|
+
# The text with every shortened link replaced by the URL it stands for
|
|
525
|
+
#
|
|
526
|
+
# A link the API expanded to no URL is left as it is, and a link without a url, which the API documents as one
|
|
527
|
+
# that may hold none, is passed over, as it holds nothing to replace.
|
|
528
|
+
#
|
|
529
|
+
# Every link is replaced in one pass over the text, so a URL a link stands for is never read for the links
|
|
530
|
+
# after it, and holds what it holds, even the shortened url of another link of the post. A url that begins
|
|
531
|
+
# another, longer one is not read in it.
|
|
532
|
+
#
|
|
533
|
+
# The rest of the text is as {#text} reads it, HTML-escaped as the API sends it, and each URL is HTML-escaped as
|
|
534
|
+
# it is put in, since the API sends a URL as it is, so the whole text is escaped alike: unescape it to display it.
|
|
535
|
+
#
|
|
536
|
+
# @api public
|
|
537
|
+
# @return [String, nil] the text with expanded links
|
|
538
|
+
# @raise [InvalidAttribute] if the response holds a link whose url, or whose expanded_url beside a url, is neither
|
|
539
|
+
# a String nor null, or text that is not a String
|
|
540
|
+
# @example Display a post with its links in full
|
|
541
|
+
# CGI.unescapeHTML(post.expanded_text)
|
|
542
|
+
def expanded_text
|
|
543
|
+
replacements = expansions
|
|
544
|
+
Shape.read_string("#{self.class}#expanded_text", text)&.gsub(Regexp.union(replacements.keys.sort_by { |url| -url.length }), replacements)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# Delete this post as the authenticated user
|
|
548
|
+
#
|
|
549
|
+
# @api public
|
|
550
|
+
# @return [Boolean] true if the post was deleted
|
|
551
|
+
# @example Delete a post
|
|
552
|
+
# post.delete
|
|
553
|
+
def delete
|
|
554
|
+
self.class.delete(self, client: client!)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Hide this reply, as the author of the post it replies to
|
|
558
|
+
#
|
|
559
|
+
# @api public
|
|
560
|
+
# @return [Boolean] true if the reply is now hidden
|
|
561
|
+
# @example Hide a reply
|
|
562
|
+
# reply.hide_reply
|
|
563
|
+
def hide_reply
|
|
564
|
+
self.class.hide_reply(self, client: client!)
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Show this reply after hiding it, as the author of the post it replies to
|
|
568
|
+
#
|
|
569
|
+
# @api public
|
|
570
|
+
# @return [Boolean] true if the reply is no longer hidden
|
|
571
|
+
# @example Show a hidden reply
|
|
572
|
+
# reply.unhide_reply
|
|
573
|
+
def unhide_reply
|
|
574
|
+
self.class.unhide_reply(self, client: client!)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
private
|
|
578
|
+
|
|
579
|
+
# The attributes that hold the full text and its entities
|
|
580
|
+
#
|
|
581
|
+
# A long post holds them in note_post, and any other post holds them itself.
|
|
582
|
+
#
|
|
583
|
+
# @api private
|
|
584
|
+
# @return [Hash] the attributes
|
|
585
|
+
def full = note_post || attrs
|
|
586
|
+
|
|
587
|
+
# The shortened url of each link that holds one, and the URL it stands for
|
|
588
|
+
# @api private
|
|
589
|
+
# @return [Hash{String => String}] each shortened url and the HTML-escaped URL it stands for
|
|
590
|
+
# @raise [InvalidAttribute] if a link holds a url, or an expanded_url beside a url, that is neither a String nor null
|
|
591
|
+
def expansions = urls.reject { |link| link["url"].nil? }.to_h { |link| Utils.read("#{self.class}#expanded_text", link) { expansion(link) } }
|
|
592
|
+
|
|
593
|
+
# The shortened url of a link, and the URL it stands for
|
|
594
|
+
#
|
|
595
|
+
# A link the API expanded to no URL stands for its url itself. The URL is HTML-escaped, as the text it goes into is.
|
|
596
|
+
#
|
|
597
|
+
# @api private
|
|
598
|
+
# @param link [Hash] the link
|
|
599
|
+
# @return [Array(String, String)] the shortened url and the HTML-escaped URL it stands for
|
|
600
|
+
# @raise [KeyError] if the link has no url
|
|
601
|
+
# @raise [ArgumentError] if the link names a URL that is not a String
|
|
602
|
+
def expansion(link)
|
|
603
|
+
url = link.fetch("url")
|
|
604
|
+
expanded_url = link["expanded_url"] || url
|
|
605
|
+
raise ArgumentError, "a link needs a url, and an expanded_url if any, that are Strings" unless [url, expanded_url].all?(String)
|
|
606
|
+
|
|
607
|
+
[url, CGI.escapeHTML(expanded_url)]
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Alias for Post, the name the API gave a post before it named it a post
|
|
613
|
+
# @api public
|
|
614
|
+
Tweet = Post
|
|
615
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module X
|
|
4
|
+
module Resources
|
|
5
|
+
# The collections of a post: the users who liked and reposted it, and its reposts and quotes, included into Post
|
|
6
|
+
#
|
|
7
|
+
# Internal to x-resources: the methods it gives a post, such as liked_by, are public API, but the module is only how
|
|
8
|
+
# they are shared, and which classes extend or include it can change within 1.x.
|
|
9
|
+
#
|
|
10
|
+
# @api semipublic
|
|
11
|
+
module PostCollections
|
|
12
|
+
# Maximum number of users or posts per page
|
|
13
|
+
# @api private
|
|
14
|
+
MAX_RESULTS = 100
|
|
15
|
+
private_constant :MAX_RESULTS
|
|
16
|
+
|
|
17
|
+
# The users who liked this post
|
|
18
|
+
#
|
|
19
|
+
# @api public
|
|
20
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
21
|
+
# @return [Cursor] a cursor over the users who liked the post
|
|
22
|
+
# @example Print the users who liked a post
|
|
23
|
+
# post.liked_by.each { |user| puts user.username }
|
|
24
|
+
def liked_by(**params)
|
|
25
|
+
cursor(User, "tweets/#{id}/liking_users", max_results: MAX_RESULTS, **params)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The users who reposted this post
|
|
29
|
+
#
|
|
30
|
+
# @api public
|
|
31
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
32
|
+
# @return [Cursor] a cursor over the reposting users
|
|
33
|
+
# @example Print the reposting users
|
|
34
|
+
# post.reposted_by.each { |user| puts user.username }
|
|
35
|
+
def reposted_by(**params)
|
|
36
|
+
cursor(User, "tweets/#{id}/retweeted_by", max_results: MAX_RESULTS, **params)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The reposts of this post, each a post of its own by the user who reposted it
|
|
40
|
+
#
|
|
41
|
+
# @api public
|
|
42
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
43
|
+
# @return [Cursor] a cursor over the reposts
|
|
44
|
+
# @example Print when and by whom a post was reposted
|
|
45
|
+
# post.reposts.each { |repost| puts "#{repost.author.username} at #{repost.created_at}" }
|
|
46
|
+
def reposts(**params)
|
|
47
|
+
cursor(Post, "tweets/#{id}/retweets", max_results: MAX_RESULTS, **params)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The posts quoting this post
|
|
51
|
+
#
|
|
52
|
+
# @api public
|
|
53
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
54
|
+
# @return [Cursor] a cursor over the quotes
|
|
55
|
+
# @example Print the quotes
|
|
56
|
+
# post.quotes.each { |quote| puts quote.text }
|
|
57
|
+
def quotes(**params)
|
|
58
|
+
cursor(Post, "tweets/#{id}/quote_tweets", max_results: MAX_RESULTS, min_results: 10, **params)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
alias_method :retweeted_by, :reposted_by
|
|
62
|
+
alias_method :retweets, :reposts
|
|
63
|
+
alias_method :quote_tweets, :quotes
|
|
64
|
+
end
|
|
65
|
+
private_constant :PostCollections
|
|
66
|
+
end
|
|
67
|
+
end
|