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,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "serialization"
|
|
4
|
+
require_relative "shape"
|
|
5
|
+
require_relative "utils"
|
|
6
|
+
require_relative "value_equality"
|
|
7
|
+
require_relative "value_marshalling"
|
|
8
|
+
|
|
9
|
+
module X
|
|
10
|
+
module Resources
|
|
11
|
+
# A rule of the filtered stream that a post the stream delivered matched: the identifier the API gave the rule, and
|
|
12
|
+
# the tag it is labelled with
|
|
13
|
+
#
|
|
14
|
+
# The stream names a rule a post matched by its identifier and its tag alone, not by the value it matches, so this is
|
|
15
|
+
# not an X::StreamRule of x-streams, which holds that value. It names the rule to X::StreamingClient#delete_rules
|
|
16
|
+
# by its identifier, as rule.id, or as rule.to_h, and it is found among the rules X::StreamingClient#rules reads by
|
|
17
|
+
# the identifier they share. x-resources depends on x-streams for none of this: a stream builds each post with
|
|
18
|
+
# X::Post.from_response, as a request does.
|
|
19
|
+
#
|
|
20
|
+
# It is frozen, compares equal to a rule of the same attributes, and matches a pattern of its identifier and tag,
|
|
21
|
+
# as in `rule in {tag: "ruby"}`. Its attributes are what the stream sent of the rule, as those of a resource are, so
|
|
22
|
+
# to_h, as_json, and to_json give them as the stream did, whatever the stream comes to send of a rule beside its
|
|
23
|
+
# identifier and tag.
|
|
24
|
+
#
|
|
25
|
+
# @api public
|
|
26
|
+
class ::X::MatchingRule
|
|
27
|
+
include Serialization
|
|
28
|
+
include ValueEquality
|
|
29
|
+
include ValueMarshalling
|
|
30
|
+
|
|
31
|
+
# The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X
|
|
32
|
+
#
|
|
33
|
+
# @!parse
|
|
34
|
+
# include X::Resources::Serialization
|
|
35
|
+
# include X::Resources::ValueEquality
|
|
36
|
+
# include X::Resources::ValueMarshalling
|
|
37
|
+
|
|
38
|
+
# The attributes of the rule, as the stream sends them
|
|
39
|
+
#
|
|
40
|
+
# The identifier is a String, as the stream sends it, and a rule without a tag holds none.
|
|
41
|
+
#
|
|
42
|
+
# @api public
|
|
43
|
+
# @return [Hash{String => Object}] the attributes, frozen
|
|
44
|
+
# @example Get the attributes
|
|
45
|
+
# rule.attrs # => {"id" => "1165037377523306498", "tag" => "ruby"}
|
|
46
|
+
attr_reader :attrs
|
|
47
|
+
|
|
48
|
+
# Initialize a rule a post matched from the attributes the stream sent of it
|
|
49
|
+
#
|
|
50
|
+
# The identifier and tag are read as it is built, so that a rule that holds either as something else raises
|
|
51
|
+
# here, rather than from its readers.
|
|
52
|
+
#
|
|
53
|
+
# @api public
|
|
54
|
+
# @param attrs [Hash{String, Symbol => Object}] the attributes, which name the identifier as id, as an Integer or
|
|
55
|
+
# as the String the stream sends, and the tag as tag, unless the rule has none
|
|
56
|
+
# @return [MatchingRule] the frozen rule
|
|
57
|
+
# @raise [ArgumentError] if the attributes are not a Hash, the identifier is not one, or the tag is neither a
|
|
58
|
+
# String nor nil
|
|
59
|
+
# @example Build a rule a post matched
|
|
60
|
+
# X::MatchingRule.new({"id" => "1165037377523306498", "tag" => "ruby"})
|
|
61
|
+
def initialize(attrs)
|
|
62
|
+
@attrs = Utils.deep_freeze(Utils.attributes!(attrs))
|
|
63
|
+
id
|
|
64
|
+
raise ArgumentError, "tag must be a String, not #{tag.inspect}" unless tag.nil? || tag.is_a?(String)
|
|
65
|
+
|
|
66
|
+
freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The identifier the API gave the rule
|
|
70
|
+
#
|
|
71
|
+
# The API sends it as a String, and it is read as an Integer, as the identifier of a stream rule is, and as
|
|
72
|
+
# strictly as any identifier is read: a String of digits alone, with no sign, underscore, or whitespace.
|
|
73
|
+
#
|
|
74
|
+
# @api public
|
|
75
|
+
# @return [Integer] the identifier
|
|
76
|
+
# @raise [ArgumentError] if the attributes hold no identifier, or one that is neither an Integer that is not
|
|
77
|
+
# negative nor a String of digits
|
|
78
|
+
# @example Get the identifier
|
|
79
|
+
# rule.id # => 1165037377523306498
|
|
80
|
+
def id = Shape.integer(attrs["id"]) || raise(ArgumentError, "a matching rule needs an id")
|
|
81
|
+
|
|
82
|
+
# The tag the rule is labelled with
|
|
83
|
+
# @api public
|
|
84
|
+
# @return [String, nil] the tag, or nil for a rule without one
|
|
85
|
+
# @example Get the tag
|
|
86
|
+
# rule.tag # => "ruby"
|
|
87
|
+
def tag = attrs["tag"]
|
|
88
|
+
|
|
89
|
+
# The identifier and tag of the rule, which a pattern matches against
|
|
90
|
+
#
|
|
91
|
+
# @api public
|
|
92
|
+
# @param keys [Array<Symbol>, nil] the keys the pattern asks for, or nil for both
|
|
93
|
+
# @return [Hash{Symbol => Integer, String, nil}] the identifier and tag the pattern asks for
|
|
94
|
+
# @example Keep the posts that matched the rule tagged ruby
|
|
95
|
+
# post.matching_rules.any? { |rule| rule in {tag: "ruby"} }
|
|
96
|
+
def deconstruct_keys(keys) = Utils.deconstruct(self, keys, %i[id tag])
|
|
97
|
+
|
|
98
|
+
# Summarize the rule for the console
|
|
99
|
+
#
|
|
100
|
+
# @api public
|
|
101
|
+
# @return [String] the class name, identifier, and tag
|
|
102
|
+
# @example Inspect a rule
|
|
103
|
+
# rule.inspect # => #<X::MatchingRule id=1165037377523306498 tag="ruby">
|
|
104
|
+
def inspect = "#<#{self.class} id=#{id} tag=#{tag.inspect}>"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "batch_finders"
|
|
4
|
+
require_relative "media_ids"
|
|
5
|
+
require_relative "resource"
|
|
6
|
+
|
|
7
|
+
module X
|
|
8
|
+
module Resources
|
|
9
|
+
# A photo, video, or animated GIF attached to a post, which its media key identifies
|
|
10
|
+
# @api public
|
|
11
|
+
class ::X::Media < Resource
|
|
12
|
+
extend BatchFinders
|
|
13
|
+
|
|
14
|
+
# The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X
|
|
15
|
+
#
|
|
16
|
+
# @!parse
|
|
17
|
+
# extend X::Resources::BatchFinders
|
|
18
|
+
|
|
19
|
+
# Every public media field
|
|
20
|
+
#
|
|
21
|
+
# A minor release may add to it the fields the API adds, so that a lookup asks for them too; see
|
|
22
|
+
# {Resource#hydrated?} for what that means for a resource looked up with a list of fields of its own.
|
|
23
|
+
FIELDS = %w[alt_text duration_ms height media_key preview_image_url public_metrics type url variants width].freeze
|
|
24
|
+
|
|
25
|
+
# The attribute holding the identifier
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
# @return [String] the identifier key
|
|
29
|
+
# @example Get the identifier key
|
|
30
|
+
# X::Media.__send__(:id_key) # => "media_key"
|
|
31
|
+
def self.id_key
|
|
32
|
+
"media_key"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The lookup endpoint
|
|
36
|
+
#
|
|
37
|
+
# @api private
|
|
38
|
+
# @return [String] the endpoint
|
|
39
|
+
# @example Get the lookup endpoint
|
|
40
|
+
# X::Media.__send__(:endpoint) # => "media"
|
|
41
|
+
def self.endpoint
|
|
42
|
+
"media"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The query parameter a batch lookup takes the media keys in
|
|
46
|
+
#
|
|
47
|
+
# @api private
|
|
48
|
+
# @return [Symbol] the parameter name
|
|
49
|
+
# @example Get the parameter of a batch lookup
|
|
50
|
+
# X::Media.__send__(:batch_key) # => :media_keys
|
|
51
|
+
def self.batch_key = :media_keys
|
|
52
|
+
|
|
53
|
+
# The name of the fields parameter
|
|
54
|
+
#
|
|
55
|
+
# @api private
|
|
56
|
+
# @return [String] the fields parameter
|
|
57
|
+
# @example Get the fields parameter
|
|
58
|
+
# X::Media.__send__(:fields_key) # => "media.fields"
|
|
59
|
+
def self.fields_key = "media.fields"
|
|
60
|
+
|
|
61
|
+
# The default query parameters requesting every public field
|
|
62
|
+
#
|
|
63
|
+
# @api public
|
|
64
|
+
# @return [Hash{String => Array<String>}] the default query parameters
|
|
65
|
+
# @example Get the default fields
|
|
66
|
+
# X::Media.default_params["media.fields"]
|
|
67
|
+
def self.default_params
|
|
68
|
+
{"media.fields" => FIELDS}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The type of the identifier, a media key
|
|
72
|
+
#
|
|
73
|
+
# Media is looked up by its media key, such as "3_1880028106020515840", and not by the numeric identifier an
|
|
74
|
+
# upload returns beside it, which the lookup endpoint does not find, so a number raises rather than look up
|
|
75
|
+
# nothing.
|
|
76
|
+
#
|
|
77
|
+
# @api private
|
|
78
|
+
# @return [Symbol] media_key
|
|
79
|
+
# @example Get the identifier type
|
|
80
|
+
# X::Media.__send__(:id_type) # => :media_key
|
|
81
|
+
def self.id_type = :media_key
|
|
82
|
+
|
|
83
|
+
# The media key of what an upload returned, or of a value that is one already
|
|
84
|
+
#
|
|
85
|
+
# What an upload returns holds both a media key and a numeric identifier, and the lookup endpoint takes the
|
|
86
|
+
# media key, so this reads that rather than the identifier a resource is usually found by.
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
# @param media [#media_key, String] the media, or its media key
|
|
90
|
+
# @return [Object] the media key
|
|
91
|
+
# @example Get the media key of an upload
|
|
92
|
+
# X::Media.__send__(:key_of, uploaded) # => "3_1880028106020515840"
|
|
93
|
+
def self.key_of(media) = media.respond_to?(:media_key) ? media.media_key : media
|
|
94
|
+
|
|
95
|
+
# Look up media by media key
|
|
96
|
+
#
|
|
97
|
+
# @api public
|
|
98
|
+
# @param media [#media_key, String, Media] the media key, what an upload returned, or media
|
|
99
|
+
# @param client [Object] the client used to make the request
|
|
100
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
101
|
+
# @return [Media, nil] the media, or nil if it was not found
|
|
102
|
+
# @yieldparam problem [Problem] each problem the API reported
|
|
103
|
+
# @example Look up what an upload returned
|
|
104
|
+
# X::Media.find(uploaded, client: client)
|
|
105
|
+
def self.find(media, client:, **params) = super(key_of(media), client:, **params) #: Media?
|
|
106
|
+
|
|
107
|
+
# Look up media by media key, which must exist
|
|
108
|
+
#
|
|
109
|
+
# The error it raises names the media key looked up, whether a media key, what an upload returned, or media was
|
|
110
|
+
# given.
|
|
111
|
+
#
|
|
112
|
+
# @api public
|
|
113
|
+
# @param media [#media_key, String, Media] the media key, what an upload returned, or media
|
|
114
|
+
# @param client [Object] the client used to make the request
|
|
115
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
116
|
+
# @return [Media] the media
|
|
117
|
+
# @raise [MissingResource] if the media was not found
|
|
118
|
+
# @example Look up what an upload returned
|
|
119
|
+
# X::Media.find!(uploaded, client: client)
|
|
120
|
+
def self.find!(media, client:, **params) = super(key_of(media), client:, **params) #: Media
|
|
121
|
+
|
|
122
|
+
# Refer to media by media key without a request
|
|
123
|
+
#
|
|
124
|
+
# @api public
|
|
125
|
+
# @param media [#media_key, String, Media] the media key, what an upload returned, or media
|
|
126
|
+
# @param client [Object, nil] the client used to fetch the media
|
|
127
|
+
# @return [Media] a stub that hydrates to the full media
|
|
128
|
+
# @example Refer to what an upload returned
|
|
129
|
+
# X::Media.from_id(uploaded, client: client).hydrate.url
|
|
130
|
+
def self.from_id(media, client: nil) = super #: Media
|
|
131
|
+
|
|
132
|
+
# Build a stub of media that hydrates with the stubs of a batch
|
|
133
|
+
#
|
|
134
|
+
# @api private
|
|
135
|
+
# @param media [#media_key, String, Media] the media key, what an upload returned, or media
|
|
136
|
+
# @param client [Object, nil] the client used to fetch the media
|
|
137
|
+
# @param batch [Resources::Batch, nil] the batch the stub hydrates with
|
|
138
|
+
# @return [Media] a stub that hydrates to the full media
|
|
139
|
+
# @example Build the stub of a page of media
|
|
140
|
+
# X::Media.__send__(:from_id_in_batch, "3_1", client: client, batch: batch)
|
|
141
|
+
def self.from_id_in_batch(media, client:, batch: nil) = super(key_of(media), client:, batch:)
|
|
142
|
+
|
|
143
|
+
# Look up many media by media key, in parallel batches
|
|
144
|
+
#
|
|
145
|
+
# @api public
|
|
146
|
+
# @param media [Array<#media_key, String, Media>] the media keys, what the uploads returned, or media
|
|
147
|
+
# @param client [Object] the client used to make the requests
|
|
148
|
+
# @param params [Hash] query parameters merged over the default parameters, and the concurrency of the batches
|
|
149
|
+
# @return [Array<Media>] the media that was found
|
|
150
|
+
# @yieldparam problem [Problem] each problem the API reported
|
|
151
|
+
# @example Look up what the uploads returned
|
|
152
|
+
# X::Media.find_all(uploads, client: client)
|
|
153
|
+
def self.find_all(media, client:, concurrency: BatchFinders::DEFAULT_CONCURRENCY, **params) = super(media.map { |value| key_of(value) }, client:, concurrency:, **params) #: Array[Media]
|
|
154
|
+
|
|
155
|
+
# The key under which media appear in the includes of a response
|
|
156
|
+
#
|
|
157
|
+
# @api private
|
|
158
|
+
# @return [String] the includes key
|
|
159
|
+
# @example Get the includes key
|
|
160
|
+
# X::Media.__send__(:includes_key) # => "media"
|
|
161
|
+
def self.includes_key
|
|
162
|
+
"media"
|
|
163
|
+
end
|
|
164
|
+
private_class_method :id_key, :endpoint, :batch_key, :fields_key, :id_type, :key_of, :from_id_in_batch, :includes_key
|
|
165
|
+
|
|
166
|
+
# @!method id
|
|
167
|
+
# The identifier of the media, which is its media key
|
|
168
|
+
#
|
|
169
|
+
# Media is looked up by its media key, so that is what identifies it here, where the identifier of uploaded
|
|
170
|
+
# media, and the one a post or a direct message attaches media by, is the number media_id reads. Both X::Media
|
|
171
|
+
# and the uploaded media of x-uploads answer media_key and media_id alike.
|
|
172
|
+
#
|
|
173
|
+
# @api public
|
|
174
|
+
# @return [String] the media key
|
|
175
|
+
# @example Get the media key
|
|
176
|
+
# media.id # => "3_1880028106020515840"
|
|
177
|
+
|
|
178
|
+
# @!attribute [r] media_key
|
|
179
|
+
# The media key
|
|
180
|
+
# @api public
|
|
181
|
+
# @return [String] the media key
|
|
182
|
+
# @example Get the media key
|
|
183
|
+
# media.media_key
|
|
184
|
+
attribute :media_key
|
|
185
|
+
|
|
186
|
+
# The numeric identifier of the media, which its media key names
|
|
187
|
+
#
|
|
188
|
+
# A media key names the identifier after the number of the type of the media and an underscore. It is the
|
|
189
|
+
# identifier an upload returns, and the one a post or a direct message attaches media by.
|
|
190
|
+
#
|
|
191
|
+
# @api public
|
|
192
|
+
# @return [Integer] the media identifier
|
|
193
|
+
# @example Get the identifier the media key names
|
|
194
|
+
# X::Media.from_id("3_1880028106020515840").media_id # => 1880028106020515840
|
|
195
|
+
def media_id = Integer(MediaIds.media_key_id(self), 10)
|
|
196
|
+
|
|
197
|
+
# @!attribute [r] type
|
|
198
|
+
# The media type: photo, video, or animated_gif
|
|
199
|
+
# @api public
|
|
200
|
+
# @return [String, nil] the media type
|
|
201
|
+
# @example Get the type
|
|
202
|
+
# media.type
|
|
203
|
+
attribute :type
|
|
204
|
+
|
|
205
|
+
# @!attribute [r] url
|
|
206
|
+
# The URL of a photo
|
|
207
|
+
# @api public
|
|
208
|
+
# @return [String, nil] the URL
|
|
209
|
+
# @example Get the URL
|
|
210
|
+
# media.url
|
|
211
|
+
attribute :url
|
|
212
|
+
|
|
213
|
+
# @!attribute [r] preview_image_url
|
|
214
|
+
# The URL of a video or GIF preview image
|
|
215
|
+
# @api public
|
|
216
|
+
# @return [String, nil] the preview image URL
|
|
217
|
+
# @example Get the preview image URL
|
|
218
|
+
# media.preview_image_url
|
|
219
|
+
attribute :preview_image_url
|
|
220
|
+
|
|
221
|
+
# @!attribute [r] alt_text
|
|
222
|
+
# The alternative text
|
|
223
|
+
# @api public
|
|
224
|
+
# @return [String, nil] the alternative text
|
|
225
|
+
# @example Get the alternative text
|
|
226
|
+
# media.alt_text
|
|
227
|
+
attribute :alt_text
|
|
228
|
+
|
|
229
|
+
# @!attribute [r] duration_ms
|
|
230
|
+
# The duration of a video in milliseconds
|
|
231
|
+
# @api public
|
|
232
|
+
# @return [Integer, nil] the duration in milliseconds
|
|
233
|
+
# @example Get the duration
|
|
234
|
+
# media.duration_ms
|
|
235
|
+
attribute :duration_ms, :integer
|
|
236
|
+
|
|
237
|
+
# @!attribute [r] height
|
|
238
|
+
# The height in pixels
|
|
239
|
+
# @api public
|
|
240
|
+
# @return [Integer, nil] the height in pixels
|
|
241
|
+
# @example Get the height
|
|
242
|
+
# media.height
|
|
243
|
+
attribute :height, :integer
|
|
244
|
+
|
|
245
|
+
# @!attribute [r] width
|
|
246
|
+
# The width in pixels
|
|
247
|
+
# @api public
|
|
248
|
+
# @return [Integer, nil] the width in pixels
|
|
249
|
+
# @example Get the width
|
|
250
|
+
# media.width
|
|
251
|
+
attribute :width, :integer
|
|
252
|
+
|
|
253
|
+
# @!attribute [r] variants
|
|
254
|
+
# The video variants with their bit rates, content types, and URLs
|
|
255
|
+
# @api public
|
|
256
|
+
# @return [Array<Hash>] the variants, empty if there are none
|
|
257
|
+
# @example Get the variants
|
|
258
|
+
# media.variants
|
|
259
|
+
attribute :variants, :objects
|
|
260
|
+
|
|
261
|
+
# @!attribute [r] public_metrics
|
|
262
|
+
# The public metrics
|
|
263
|
+
# @api public
|
|
264
|
+
# @return [Hash, nil] the public metrics
|
|
265
|
+
# @example Get the public metrics
|
|
266
|
+
# media.public_metrics
|
|
267
|
+
attribute :public_metrics, :object
|
|
268
|
+
|
|
269
|
+
# @!attribute [r] view_count
|
|
270
|
+
# The number of views
|
|
271
|
+
# @api public
|
|
272
|
+
# @return [Integer, nil] the view count
|
|
273
|
+
# @example Get the view count
|
|
274
|
+
# media.view_count
|
|
275
|
+
attribute :view_count, :integer, key: %w[public_metrics view_count]
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module X
|
|
4
|
+
module Resources
|
|
5
|
+
# Reads the media identifiers a new post or direct message attaches
|
|
6
|
+
#
|
|
7
|
+
# What an upload returns, media such as the media of another post, and a media identifier each name media, and
|
|
8
|
+
# this reads the identifier the API takes from any of them.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module MediaIds
|
|
12
|
+
# The pattern of a media key, which names the media identifier after the number of its type and an underscore
|
|
13
|
+
MEDIA_KEY = /\A\d+_(\d+)\z/
|
|
14
|
+
|
|
15
|
+
# The pattern of a media identifier the API takes: one to nineteen digits
|
|
16
|
+
MEDIA_ID = /\A\d{1,19}\z/
|
|
17
|
+
|
|
18
|
+
# The message of the error raised for something that is neither media nor the identifier of media
|
|
19
|
+
NOT_MEDIA = "media is what an upload returned, media such as X::Media, a media key, or a media identifier, not %s"
|
|
20
|
+
private_constant :MEDIA_KEY, :MEDIA_ID, :NOT_MEDIA
|
|
21
|
+
|
|
22
|
+
extend self
|
|
23
|
+
|
|
24
|
+
# Extract a media identifier from an upload, from media, or from a raw value
|
|
25
|
+
#
|
|
26
|
+
# What an upload returns is read with fetch, which a Hash answers and so does the uploaded media of
|
|
27
|
+
# x-uploads, which this gem does not depend on. Media, such as the media of a post, is read from its media
|
|
28
|
+
# key, which names the identifier, and so is a String that is a media key, such as the id of X::Media.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
# @param value [#fetch, #media_key, String, Integer] what an upload returned, holding an id, media, a media key,
|
|
32
|
+
# or an identifier
|
|
33
|
+
# @return [String] the media identifier
|
|
34
|
+
# @raise [ArgumentError] if the value is none of them, such as a Hash without an "id", or names an identifier the
|
|
35
|
+
# API would refuse
|
|
36
|
+
def media_id_of(value)
|
|
37
|
+
id = case value
|
|
38
|
+
when String then value[MEDIA_KEY, 1] || value
|
|
39
|
+
when Integer then value.to_s
|
|
40
|
+
else value.respond_to?(:fetch) ? value.fetch("id") { raise ArgumentError, "#{format(NOT_MEDIA, value.inspect)}, which holds no \"id\"" }.to_s : media_key_id(value)
|
|
41
|
+
end
|
|
42
|
+
id.match?(MEDIA_ID) ? id : raise(ArgumentError, format(NOT_MEDIA, value.inspect))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The media identifier a media key names
|
|
46
|
+
# @api private
|
|
47
|
+
# @param media [#media_key, Object] the media
|
|
48
|
+
# @return [String] the media identifier
|
|
49
|
+
# @raise [ArgumentError] if the media has no media key that names an identifier
|
|
50
|
+
def media_key_id(media)
|
|
51
|
+
key = media.media_key if media.respond_to?(:media_key)
|
|
52
|
+
key.to_s[MEDIA_KEY, 1] || raise(ArgumentError, format(NOT_MEDIA, media.inspect))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The media identifiers of one upload or of several
|
|
56
|
+
#
|
|
57
|
+
# One upload needs no array around it, so a single value is read as a list of one, and nil, like an empty
|
|
58
|
+
# list, as none.
|
|
59
|
+
#
|
|
60
|
+
# @api private
|
|
61
|
+
# @param media_ids [Array, #fetch, Media, String, Integer, nil] what the uploads returned, media, or identifiers,
|
|
62
|
+
# one or many
|
|
63
|
+
# @return [Array<String>] the media identifiers, empty for nil
|
|
64
|
+
def media_ids_of(media_ids)
|
|
65
|
+
case media_ids
|
|
66
|
+
when nil then []
|
|
67
|
+
when Array then media_ids.map { |media| media_id_of(media) }
|
|
68
|
+
else [media_id_of(media_ids)]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
private_constant :MediaIds
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module X
|
|
6
|
+
module Resources
|
|
7
|
+
# A thread-safe slot that holds one lazily computed value
|
|
8
|
+
# @api private
|
|
9
|
+
class Memo
|
|
10
|
+
# Marker for a slot that holds no value yet
|
|
11
|
+
UNSET = Object.new.freeze
|
|
12
|
+
private_constant :UNSET
|
|
13
|
+
|
|
14
|
+
# Initialize an empty memo
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
# @return [Memo] a new memo
|
|
18
|
+
def initialize
|
|
19
|
+
@monitor = Monitor.new
|
|
20
|
+
@value = UNSET
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Fetch the value, computing and storing it on first access
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
26
|
+
# @yield computes the value when none is stored
|
|
27
|
+
# @return [Object] the memoized value
|
|
28
|
+
def fetch
|
|
29
|
+
@monitor.synchronize do
|
|
30
|
+
@value = yield if @value.equal?(UNSET)
|
|
31
|
+
@value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check whether a value is stored, whether computed or stored
|
|
36
|
+
#
|
|
37
|
+
# It reads the slot without the lock, since a value, once stored, is only ever replaced by another.
|
|
38
|
+
#
|
|
39
|
+
# @api private
|
|
40
|
+
# @return [Boolean] true if the memo holds a value, which may be nil
|
|
41
|
+
def stored? = !@value.equal?(UNSET)
|
|
42
|
+
|
|
43
|
+
# Store a value, replacing any memoized value
|
|
44
|
+
#
|
|
45
|
+
# @api private
|
|
46
|
+
# @param value [Object] the value to store
|
|
47
|
+
# @return [Object] the stored value
|
|
48
|
+
def store(value)
|
|
49
|
+
@monitor.synchronize { @value = value }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
private_constant :Memo
|
|
53
|
+
end
|
|
54
|
+
end
|