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.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +9 -0
  3. data/CHANGELOG.md +254 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +148 -0
  6. data/lib/x/resources/abstract_class.rb +29 -0
  7. data/lib/x/resources/actions/direct_messages.rb +99 -0
  8. data/lib/x/resources/actions/engagement.rb +95 -0
  9. data/lib/x/resources/actions/lists.rb +126 -0
  10. data/lib/x/resources/actions/posts.rb +77 -0
  11. data/lib/x/resources/actions/relationships.rb +91 -0
  12. data/lib/x/resources/actions.rb +22 -0
  13. data/lib/x/resources/api.rb +40 -0
  14. data/lib/x/resources/attributes.rb +203 -0
  15. data/lib/x/resources/batch.rb +43 -0
  16. data/lib/x/resources/batch_finders.rb +185 -0
  17. data/lib/x/resources/bookmark_folder.rb +23 -0
  18. data/lib/x/resources/community.rb +137 -0
  19. data/lib/x/resources/cursor.rb +493 -0
  20. data/lib/x/resources/direct_message.rb +325 -0
  21. data/lib/x/resources/direct_message_conversations.rb +147 -0
  22. data/lib/x/resources/errors.rb +104 -0
  23. data/lib/x/resources/finders.rb +255 -0
  24. data/lib/x/resources/identity.rb +65 -0
  25. data/lib/x/resources/includes.rb +216 -0
  26. data/lib/x/resources/list.rb +336 -0
  27. data/lib/x/resources/lookups/communities.rb +56 -0
  28. data/lib/x/resources/lookups/direct_messages.rb +86 -0
  29. data/lib/x/resources/lookups/lists.rb +44 -0
  30. data/lib/x/resources/lookups/media.rb +72 -0
  31. data/lib/x/resources/lookups/posts.rb +198 -0
  32. data/lib/x/resources/lookups/spaces.rb +87 -0
  33. data/lib/x/resources/lookups/trends.rb +38 -0
  34. data/lib/x/resources/lookups/users.rb +221 -0
  35. data/lib/x/resources/lookups.rb +25 -0
  36. data/lib/x/resources/marshalling.rb +93 -0
  37. data/lib/x/resources/matching_rule.rb +107 -0
  38. data/lib/x/resources/media.rb +278 -0
  39. data/lib/x/resources/media_ids.rb +74 -0
  40. data/lib/x/resources/memo.rb +54 -0
  41. data/lib/x/resources/page.rb +394 -0
  42. data/lib/x/resources/page_limit.rb +80 -0
  43. data/lib/x/resources/pages.rb +270 -0
  44. data/lib/x/resources/parallel.rb +82 -0
  45. data/lib/x/resources/personalized_trend.rb +124 -0
  46. data/lib/x/resources/place.rb +107 -0
  47. data/lib/x/resources/poll.rb +75 -0
  48. data/lib/x/resources/post.rb +615 -0
  49. data/lib/x/resources/post_collections.rb +67 -0
  50. data/lib/x/resources/post_counts.rb +215 -0
  51. data/lib/x/resources/post_search.rb +86 -0
  52. data/lib/x/resources/post_usage.rb +203 -0
  53. data/lib/x/resources/post_writes.rb +140 -0
  54. data/lib/x/resources/published_count.rb +31 -0
  55. data/lib/x/resources/references.rb +121 -0
  56. data/lib/x/resources/relation_writes.rb +54 -0
  57. data/lib/x/resources/relationships.rb +77 -0
  58. data/lib/x/resources/resource.rb +535 -0
  59. data/lib/x/resources/serialization.rb +58 -0
  60. data/lib/x/resources/shape.rb +167 -0
  61. data/lib/x/resources/space.rb +332 -0
  62. data/lib/x/resources/topic.rb +59 -0
  63. data/lib/x/resources/trend.rb +130 -0
  64. data/lib/x/resources/user.rb +502 -0
  65. data/lib/x/resources/user_collections.rb +213 -0
  66. data/lib/x/resources/user_finders.rb +282 -0
  67. data/lib/x/resources/utils.rb +358 -0
  68. data/lib/x/resources/value_equality.rb +38 -0
  69. data/lib/x/resources/value_marshalling.rb +89 -0
  70. data/lib/x/resources/version.rb +25 -0
  71. data/lib/x/resources.rb +22 -0
  72. data/sig/manifest.yaml +7 -0
  73. data/sig/x-resources.rbs +813 -0
  74. 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 &amp;, &lt;, and &gt;, 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 &amp; 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