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,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "utils"
|
|
4
|
+
|
|
5
|
+
module X
|
|
6
|
+
module Resources
|
|
7
|
+
# Reads the objects and lists of a response, which must be what the API documents them to be
|
|
8
|
+
#
|
|
9
|
+
# A value the object layer reads into, such as the public_metrics a count is read from, or the links of a post, is
|
|
10
|
+
# checked to be an object or a list before it is read, so that one that is not raises InvalidAttribute, as any
|
|
11
|
+
# value of a response that cannot be read does, rather than the NoMethodError or TypeError of reading into it.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module Shape
|
|
15
|
+
# The pattern of a conversation identifier: two user identifiers joined with a hyphen, or a group's own number
|
|
16
|
+
CONVERSATION_ID = /\A\d+(?:-\d+)?\z/
|
|
17
|
+
|
|
18
|
+
extend self
|
|
19
|
+
|
|
20
|
+
# Read the value at a path of keys, through the object each key but the last names
|
|
21
|
+
#
|
|
22
|
+
# A key that names nothing reads as nil, as Hash#dig does, but one that names something other than an object,
|
|
23
|
+
# such as a String where the API documents public_metrics, cannot be read further.
|
|
24
|
+
#
|
|
25
|
+
# @api private
|
|
26
|
+
# @param name [String] what the value is, such as the reader that reads it
|
|
27
|
+
# @param value [Hash, nil] the object the path starts at
|
|
28
|
+
# @param path [Array<String>] the keys
|
|
29
|
+
# @return [Object, nil] the value, or nil if a key names nothing
|
|
30
|
+
# @raise [InvalidAttribute] if the path passes through something other than an object
|
|
31
|
+
# @example Read the number of times a post was liked
|
|
32
|
+
# X::Resources::Shape.dig("X::Post#like_count", attrs, %w[public_metrics like_count])
|
|
33
|
+
def dig(name, value, path) = path.reduce(value) { |current, key| read_object(name, current)&.[](key) }
|
|
34
|
+
|
|
35
|
+
# Read the value at the first of several paths of keys that names one
|
|
36
|
+
#
|
|
37
|
+
# @api private
|
|
38
|
+
# @param name [String] what the value is, such as the reader that reads it
|
|
39
|
+
# @param value [Hash, nil] the object the paths start at
|
|
40
|
+
# @param paths [Array<Array<String>>] the paths, in the order they are tried
|
|
41
|
+
# @return [Object, nil] the value, or nil if no path names one
|
|
42
|
+
# @raise [InvalidAttribute] if a path passes through something other than an object
|
|
43
|
+
# @example Read the number of times a post was reposted, by either name
|
|
44
|
+
# X::Resources::Shape.dig_first("X::Post#repost_count", attrs, [%w[public_metrics repost_count], %w[public_metrics retweet_count]])
|
|
45
|
+
def dig_first(name, value, paths)
|
|
46
|
+
paths.each do |path|
|
|
47
|
+
found = dig(name, value, path)
|
|
48
|
+
return found unless found.nil?
|
|
49
|
+
end
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Read a String, which the response may leave out
|
|
54
|
+
#
|
|
55
|
+
# @api private
|
|
56
|
+
# @param name [String] what the value is, such as the reader that reads it
|
|
57
|
+
# @param value [String, nil] the String
|
|
58
|
+
# @return [String, nil] the String, or nil if the value is missing
|
|
59
|
+
# @raise [InvalidAttribute] if the value is not a String
|
|
60
|
+
# @example Read the username a permalink names
|
|
61
|
+
# X::Resources::Shape.read_string("X::User#permalink", user.username)
|
|
62
|
+
def read_string(name, value) = Utils.read(name, value) { String.try_convert(value) || (raise ArgumentError unless value.nil?) }
|
|
63
|
+
|
|
64
|
+
# Read an object, which the response may leave out
|
|
65
|
+
#
|
|
66
|
+
# @api private
|
|
67
|
+
# @param name [String] what the value is, such as the reader that reads it
|
|
68
|
+
# @param value [Hash, nil] the object
|
|
69
|
+
# @return [Hash, nil] the object, or nil if the value is missing
|
|
70
|
+
# @raise [InvalidAttribute] if the value is not an object
|
|
71
|
+
# @example Read the entities of a post
|
|
72
|
+
# X::Resources::Shape.read_object("X::Post#entities", attrs["entities"])
|
|
73
|
+
def read_object(name, value) = Utils.read(name, value) { object(value) }
|
|
74
|
+
|
|
75
|
+
# Read a list of objects, which the response may leave out
|
|
76
|
+
#
|
|
77
|
+
# @api private
|
|
78
|
+
# @param name [String] what the value is, such as the reader that reads it
|
|
79
|
+
# @param value [Array<Hash>, nil] the list
|
|
80
|
+
# @return [Array<Hash>] the objects, empty if the list is missing
|
|
81
|
+
# @raise [InvalidAttribute] if the value is not a list, or holds something other than an object
|
|
82
|
+
# @example Read the links of a post
|
|
83
|
+
# X::Resources::Shape.objects("X::Post#urls", entities["urls"])
|
|
84
|
+
def objects(name, value) = Utils.read(name, value) { object_list(value) }
|
|
85
|
+
|
|
86
|
+
# Check that a value the API documents as a list of objects is one, if it is there
|
|
87
|
+
#
|
|
88
|
+
# @api private
|
|
89
|
+
# @param value [Array<Hash>, nil] the value
|
|
90
|
+
# @return [Array<Hash>] the objects, empty if the value is missing
|
|
91
|
+
# @raise [ArgumentError] if the value is not a list, or holds something other than an object
|
|
92
|
+
def object_list(value) = Array(list(value)).each { |element| object!(element) }.freeze
|
|
93
|
+
|
|
94
|
+
# Check that a value the API documents as an object is one, if it is there
|
|
95
|
+
#
|
|
96
|
+
# @api private
|
|
97
|
+
# @param value [Hash, nil] the value
|
|
98
|
+
# @return [Hash, nil] the object, or nil if the value is missing
|
|
99
|
+
# @raise [ArgumentError] if the value is not an object
|
|
100
|
+
def object(value) = (object!(value) unless value.nil?)
|
|
101
|
+
|
|
102
|
+
# Check that a value the API documents as an object is one
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
105
|
+
# @param value [Hash] the value
|
|
106
|
+
# @return [Hash] the object
|
|
107
|
+
# @raise [ArgumentError] if the value is not an object
|
|
108
|
+
def object!(value) = Hash.try_convert(value) || raise(ArgumentError, "#{value.inspect} is not an object")
|
|
109
|
+
|
|
110
|
+
# Check that a value the API documents as a list is one, if it is there
|
|
111
|
+
#
|
|
112
|
+
# @api private
|
|
113
|
+
# @param value [Array, nil] the value
|
|
114
|
+
# @return [Array, nil] the list, or nil if the value is missing
|
|
115
|
+
# @raise [ArgumentError] if the value is not a list
|
|
116
|
+
def list(value) = (Array.try_convert(value) || raise(ArgumentError, "#{value.inspect} is not a list") unless value.nil?)
|
|
117
|
+
|
|
118
|
+
# Read a range of characters, which the response may leave out
|
|
119
|
+
#
|
|
120
|
+
# The API gives a range as a list of its start and its end, which the range leaves out.
|
|
121
|
+
#
|
|
122
|
+
# @api private
|
|
123
|
+
# @param value [Array<Integer>, nil] the start and end of the range
|
|
124
|
+
# @return [Range<Integer>, nil] the range, which leaves out its end, or nil if the value is missing
|
|
125
|
+
# @raise [ArgumentError] if the value is not a list of two Integers that are not negative
|
|
126
|
+
# @example Read the range of the text a post shows
|
|
127
|
+
# X::Resources::Shape.range([8, 10]) # => 8...10
|
|
128
|
+
def range(value)
|
|
129
|
+
bounds = list(value)
|
|
130
|
+
return if bounds.nil?
|
|
131
|
+
raise ArgumentError, "#{bounds} is not the start and end of a range" unless bounds.size.eql?(2) && bounds.all? { |bound| Integer === bound && !bound.negative? }
|
|
132
|
+
|
|
133
|
+
Range.new(bounds.first, bounds.last, true)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Read a numeric identifier or a count as an Integer, if it is there
|
|
137
|
+
#
|
|
138
|
+
# It is read as strictly as an identifier is checked: an Integer that is not negative, or a String of digits
|
|
139
|
+
# alone, with no sign, underscore, or whitespace, so that a count reads as the whole number it is, and the
|
|
140
|
+
# identifier of a resource a reader such as author_id returns is one that resource is found by.
|
|
141
|
+
#
|
|
142
|
+
# @api private
|
|
143
|
+
# @param value [String, Integer, nil] the identifier or count
|
|
144
|
+
# @return [Integer, nil] the Integer, or nil if the value is missing
|
|
145
|
+
# @raise [ArgumentError] if the value is neither an Integer that is not negative nor a String of digits
|
|
146
|
+
def integer(value)
|
|
147
|
+
return value if value.nil? || (Integer === value && !value.negative?)
|
|
148
|
+
raise ArgumentError, "invalid value for Integer(): #{value.to_s.inspect}" unless String === value && Utils::NUMERIC_ID.match?(value)
|
|
149
|
+
|
|
150
|
+
Integer(value, 10)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Read the identifier of a conversation of direct messages, if it is there
|
|
154
|
+
#
|
|
155
|
+
# @api private
|
|
156
|
+
# @param value [String, nil] the identifier
|
|
157
|
+
# @return [String, nil] the identifier, or nil if the value is missing
|
|
158
|
+
# @raise [ArgumentError] if the value is not a String of digits, or of two numbers joined with a hyphen
|
|
159
|
+
def conversation_id(value)
|
|
160
|
+
return value if value.nil? || (String === value && CONVERSATION_ID.match?(value))
|
|
161
|
+
|
|
162
|
+
raise ArgumentError, "#{value.inspect} is not a conversation identifier"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
private_constant :Shape
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "batch_finders"
|
|
4
|
+
require_relative "cursor"
|
|
5
|
+
require_relative "resource"
|
|
6
|
+
require_relative "topic"
|
|
7
|
+
|
|
8
|
+
module X
|
|
9
|
+
module Resources
|
|
10
|
+
# A live audio space
|
|
11
|
+
#
|
|
12
|
+
# The space endpoints, but for buyers, take app-only authentication or OAuth 2.0 user context, not OAuth 1.0a, so
|
|
13
|
+
# a client that signs with OAuth 1.0a reads spaces with a copy that authenticates as the app, while the spaces and
|
|
14
|
+
# posts it reads hold the client, so that they act as the user.
|
|
15
|
+
#
|
|
16
|
+
# @api public
|
|
17
|
+
class ::X::Space < Resource
|
|
18
|
+
extend BatchFinders
|
|
19
|
+
|
|
20
|
+
# The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X
|
|
21
|
+
#
|
|
22
|
+
# @!parse
|
|
23
|
+
# extend X::Resources::BatchFinders
|
|
24
|
+
|
|
25
|
+
# Every public space field
|
|
26
|
+
#
|
|
27
|
+
# A minor release may add to it the fields the API adds, so that a lookup asks for them too; see
|
|
28
|
+
# {Resource#hydrated?} for what that means for a resource looked up with a list of fields of its own.
|
|
29
|
+
FIELDS = %w[created_at ended_at id is_ticketed lang participant_count scheduled_start started_at state
|
|
30
|
+
subscriber_count title updated_at].freeze
|
|
31
|
+
# Every expansion available on space endpoints
|
|
32
|
+
#
|
|
33
|
+
# A minor release may add to it the expansions the API adds, so that a lookup asks for them too; see
|
|
34
|
+
# {Resource#hydrated?} for what that means for a resource looked up with a list of expansions of its own.
|
|
35
|
+
EXPANSIONS = %w[creator_id host_ids invited_user_ids speaker_ids topic_ids].freeze
|
|
36
|
+
# Maximum number of posts or buyers per page, or of spaces a search returns
|
|
37
|
+
# @api private
|
|
38
|
+
MAX_RESULTS = 100
|
|
39
|
+
private_constant :MAX_RESULTS
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# The API endpoint used to look up spaces by identifier
|
|
43
|
+
#
|
|
44
|
+
# @api private
|
|
45
|
+
# @return [String] the endpoint
|
|
46
|
+
# @example Get the endpoint
|
|
47
|
+
# X::Space.__send__(:endpoint) # => "spaces"
|
|
48
|
+
def endpoint
|
|
49
|
+
"spaces"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The type of the identifier, which is letters and digits rather than a number
|
|
53
|
+
#
|
|
54
|
+
# @api private
|
|
55
|
+
# @return [Symbol] raw
|
|
56
|
+
# @example Get the identifier type
|
|
57
|
+
# X::Space.__send__(:id_type) # => :raw
|
|
58
|
+
def id_type = :raw
|
|
59
|
+
|
|
60
|
+
# The client a space lookup requests with
|
|
61
|
+
#
|
|
62
|
+
# The space endpoints refuse OAuth 1.0a, so a client that signs with it looks spaces up with a copy that
|
|
63
|
+
# reuses its bearer token, as a client signed in with OAuth 2.0 as a user that holds the app's credentials does.
|
|
64
|
+
# One that holds none looks them up as it is.
|
|
65
|
+
#
|
|
66
|
+
# @api private
|
|
67
|
+
# @param client [Object] the client the lookup was given
|
|
68
|
+
# @return [Object] the client's app-only client, or the client itself
|
|
69
|
+
# @example Get the client a space lookup requests with
|
|
70
|
+
# X::Space.__send__(:client_for, client)
|
|
71
|
+
def client_for(client) = Utils.app_client(client)
|
|
72
|
+
|
|
73
|
+
# The query parameter that selects space fields
|
|
74
|
+
#
|
|
75
|
+
# @api private
|
|
76
|
+
# @return [String] the fields parameter
|
|
77
|
+
# @example Get the fields parameter
|
|
78
|
+
# X::Space.__send__(:fields_key) # => "space.fields"
|
|
79
|
+
def fields_key = "space.fields"
|
|
80
|
+
|
|
81
|
+
private :endpoint, :id_type, :client_for, :fields_key
|
|
82
|
+
|
|
83
|
+
# The default query parameters requesting every space field and expansion
|
|
84
|
+
#
|
|
85
|
+
# @api public
|
|
86
|
+
# @return [Hash{String => Array<String>}] the default query parameters
|
|
87
|
+
# @example Get the default parameters
|
|
88
|
+
# X::Space.default_params["space.fields"]
|
|
89
|
+
def default_params
|
|
90
|
+
{"space.fields" => FIELDS, "user.fields" => User::FIELDS, "topic.fields" => Topic::FIELDS, "expansions" => EXPANSIONS}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Search spaces by their titles
|
|
94
|
+
#
|
|
95
|
+
# The API returns the matching spaces in one response, of up to 100 spaces.
|
|
96
|
+
#
|
|
97
|
+
# @api public
|
|
98
|
+
# @param query [String] the search query
|
|
99
|
+
# @param client [Object] the client used to make the request
|
|
100
|
+
# @param params [Hash] query parameters merged over the default parameters, such as state: live or scheduled
|
|
101
|
+
# @return [Cursor] a cursor over the matching spaces
|
|
102
|
+
# @example Print the live spaces about Ruby
|
|
103
|
+
# X::Space.search("ruby", client: client, state: "live").each { |space| puts space.title }
|
|
104
|
+
def search(query, client:, **params)
|
|
105
|
+
Cursor.__send__(:build, self, "spaces/search", client:, params: {query:, max_results: MAX_RESULTS}.merge(params), app_only: true)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Look up the live and scheduled spaces many users created, in parallel batches
|
|
110
|
+
#
|
|
111
|
+
# The API returns the spaces of up to 100 users at a time, in one response without pages, so the users are
|
|
112
|
+
# looked up that many at a time.
|
|
113
|
+
#
|
|
114
|
+
# @api public
|
|
115
|
+
# @param users [Array<User, String, Integer>] the users who created the spaces, or their identifiers
|
|
116
|
+
# @param client [Object] the client used to make the requests
|
|
117
|
+
# @param concurrency [Integer] the number of batches looked up at once, which must be at least one
|
|
118
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
119
|
+
# @return [Array<Space>] the spaces, frozen, empty if the users created none
|
|
120
|
+
# @raise [ArgumentError] if a user is not a user or the identifier of one, or the concurrency is less than one,
|
|
121
|
+
# before a request
|
|
122
|
+
# @yieldparam problem [Problem] each problem the API reported
|
|
123
|
+
# @example Print the spaces two users created
|
|
124
|
+
# X::Space.find_all_by_creator([7505382, 783214], client: client).each { |space| puts space.title }
|
|
125
|
+
def self.find_all_by_creator(users, client:, concurrency: BatchFinders::DEFAULT_CONCURRENCY, **params, &)
|
|
126
|
+
ids = users.map { |user| Utils.id_of(user, User) }
|
|
127
|
+
spaces = lookup_in_batches("spaces/by/creator_ids", :user_ids, ids, client:, concurrency:, **params, &) #: Array[Space]
|
|
128
|
+
spaces.freeze
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @!attribute [r] title
|
|
132
|
+
# The title
|
|
133
|
+
# @api public
|
|
134
|
+
# @return [String, nil] the title
|
|
135
|
+
# @example Get the title
|
|
136
|
+
# space.title
|
|
137
|
+
attribute :title
|
|
138
|
+
|
|
139
|
+
# @!attribute [r] state
|
|
140
|
+
# The state: live, scheduled, or ended
|
|
141
|
+
# @api public
|
|
142
|
+
# @return [String, nil] the state
|
|
143
|
+
# @example Get the state
|
|
144
|
+
# space.state
|
|
145
|
+
attribute :state
|
|
146
|
+
|
|
147
|
+
# @!attribute [r] lang
|
|
148
|
+
# The BCP 47 language tag
|
|
149
|
+
# @api public
|
|
150
|
+
# @return [String, nil] the language tag
|
|
151
|
+
# @example Get the language
|
|
152
|
+
# space.lang
|
|
153
|
+
attribute :lang
|
|
154
|
+
|
|
155
|
+
# @!attribute [r] created_at
|
|
156
|
+
# The time when the space was created
|
|
157
|
+
# @api public
|
|
158
|
+
# @return [Time, nil] the creation time
|
|
159
|
+
# @example Get the creation time
|
|
160
|
+
# space.created_at
|
|
161
|
+
attribute :created_at, :time
|
|
162
|
+
|
|
163
|
+
# @!attribute [r] started_at
|
|
164
|
+
# The time when the space started
|
|
165
|
+
# @api public
|
|
166
|
+
# @return [Time, nil] the start time
|
|
167
|
+
# @example Get the start time
|
|
168
|
+
# space.started_at
|
|
169
|
+
attribute :started_at, :time
|
|
170
|
+
|
|
171
|
+
# @!attribute [r] ended_at
|
|
172
|
+
# The time when the space ended
|
|
173
|
+
# @api public
|
|
174
|
+
# @return [Time, nil] the end time
|
|
175
|
+
# @example Get the end time
|
|
176
|
+
# space.ended_at
|
|
177
|
+
attribute :ended_at, :time
|
|
178
|
+
|
|
179
|
+
# @!attribute [r] scheduled_start
|
|
180
|
+
# The scheduled start time
|
|
181
|
+
# @api public
|
|
182
|
+
# @return [Time, nil] the scheduled start time
|
|
183
|
+
# @example Get the scheduled start time
|
|
184
|
+
# space.scheduled_start
|
|
185
|
+
attribute :scheduled_start, :time
|
|
186
|
+
|
|
187
|
+
# @!attribute [r] updated_at
|
|
188
|
+
# The time when the space was last updated
|
|
189
|
+
# @api public
|
|
190
|
+
# @return [Time, nil] the update time
|
|
191
|
+
# @example Get the update time
|
|
192
|
+
# space.updated_at
|
|
193
|
+
attribute :updated_at, :time
|
|
194
|
+
|
|
195
|
+
# @!attribute [r] ticketed
|
|
196
|
+
# Whether the space requires a ticket, the is_ticketed field
|
|
197
|
+
# @api public
|
|
198
|
+
# @return [Boolean, nil] true if the space is ticketed
|
|
199
|
+
# @example Check whether a space is ticketed
|
|
200
|
+
# space.ticketed?
|
|
201
|
+
attribute :ticketed, :boolean, key: %w[is_ticketed]
|
|
202
|
+
|
|
203
|
+
# @!method ticketed?
|
|
204
|
+
# Check whether the space requires a ticket
|
|
205
|
+
# @api public
|
|
206
|
+
# @return [Boolean] true if the space is ticketed
|
|
207
|
+
# @example Check whether a space is ticketed
|
|
208
|
+
# space.ticketed?
|
|
209
|
+
|
|
210
|
+
# @!attribute [r] participant_count
|
|
211
|
+
# The number of participants
|
|
212
|
+
# @api public
|
|
213
|
+
# @return [Integer, nil] the participant count
|
|
214
|
+
# @example Get the participant count
|
|
215
|
+
# space.participant_count
|
|
216
|
+
attribute :participant_count, :integer
|
|
217
|
+
|
|
218
|
+
# @!attribute [r] subscriber_count
|
|
219
|
+
# The number of subscribers
|
|
220
|
+
# @api public
|
|
221
|
+
# @return [Integer, nil] the subscriber count
|
|
222
|
+
# @example Get the subscriber count
|
|
223
|
+
# space.subscriber_count
|
|
224
|
+
attribute :subscriber_count, :integer
|
|
225
|
+
|
|
226
|
+
# @!attribute [r] creator_id
|
|
227
|
+
# The identifier of the creator
|
|
228
|
+
# @api public
|
|
229
|
+
# @return [Integer, nil] the creator identifier
|
|
230
|
+
# @example Get the creator identifier
|
|
231
|
+
# space.creator_id
|
|
232
|
+
attribute :creator_id, :integer
|
|
233
|
+
|
|
234
|
+
# @!attribute [r] host_ids
|
|
235
|
+
# The identifiers of the hosts
|
|
236
|
+
# @api public
|
|
237
|
+
# @return [Array<Integer>] the host identifiers, empty if there are none
|
|
238
|
+
# @example Get the host identifiers
|
|
239
|
+
# space.host_ids
|
|
240
|
+
attribute :host_ids, :integers
|
|
241
|
+
|
|
242
|
+
# @!attribute [r] speaker_ids
|
|
243
|
+
# The identifiers of the speakers
|
|
244
|
+
# @api public
|
|
245
|
+
# @return [Array<Integer>] the speaker identifiers, empty if there are none
|
|
246
|
+
# @example Get the speaker identifiers
|
|
247
|
+
# space.speaker_ids
|
|
248
|
+
attribute :speaker_ids, :integers
|
|
249
|
+
|
|
250
|
+
# @!attribute [r] invited_user_ids
|
|
251
|
+
# The identifiers of the invited users
|
|
252
|
+
# @api public
|
|
253
|
+
# @return [Array<Integer>] the invited user identifiers, empty if there are none
|
|
254
|
+
# @example Get the invited user identifiers
|
|
255
|
+
# space.invited_user_ids
|
|
256
|
+
attribute :invited_user_ids, :integers
|
|
257
|
+
|
|
258
|
+
# @!attribute [r] topic_ids
|
|
259
|
+
# The identifiers of the topics
|
|
260
|
+
# @api public
|
|
261
|
+
# @return [Array<Integer>] the topic identifiers, empty if there are none
|
|
262
|
+
# @example Get the topic identifiers
|
|
263
|
+
# space.topic_ids
|
|
264
|
+
attribute :topic_ids, :integers
|
|
265
|
+
|
|
266
|
+
# @!method creator
|
|
267
|
+
# The creator, resolved from the includes or as a stub holding only its identifier
|
|
268
|
+
# @api public
|
|
269
|
+
# @return [User, nil] the creator
|
|
270
|
+
# @example Get the creator's username
|
|
271
|
+
# space.creator.username
|
|
272
|
+
reference :creator, :User, key: %w[creator_id]
|
|
273
|
+
|
|
274
|
+
# @!method hosts
|
|
275
|
+
# The hosts, resolved from the includes or as stubs holding only their identifiers
|
|
276
|
+
# @api public
|
|
277
|
+
# @return [Array<User>] the hosts
|
|
278
|
+
# @example Get the hosts
|
|
279
|
+
# space.hosts
|
|
280
|
+
references :hosts, :User, key: %w[host_ids]
|
|
281
|
+
|
|
282
|
+
# @!method speakers
|
|
283
|
+
# The speakers, from the includes or as stubs holding only their identifiers
|
|
284
|
+
# @api public
|
|
285
|
+
# @return [Array<User>] the speakers
|
|
286
|
+
# @example Get the speakers
|
|
287
|
+
# space.speakers
|
|
288
|
+
references :speakers, :User, key: %w[speaker_ids]
|
|
289
|
+
|
|
290
|
+
# @!method invited_users
|
|
291
|
+
# The invited users, from the includes or as stubs holding only their identifiers
|
|
292
|
+
# @api public
|
|
293
|
+
# @return [Array<User>] the invited users
|
|
294
|
+
# @example Get the invited users
|
|
295
|
+
# space.invited_users
|
|
296
|
+
references :invited_users, :User, key: %w[invited_user_ids]
|
|
297
|
+
|
|
298
|
+
# @!method topics
|
|
299
|
+
# The topics, from the includes or as stubs holding only their identifiers
|
|
300
|
+
# @api public
|
|
301
|
+
# @return [Array<Topic>] the topics
|
|
302
|
+
# @example Get the names of the topics
|
|
303
|
+
# space.topics.map(&:name)
|
|
304
|
+
references :topics, :Topic, key: %w[topic_ids]
|
|
305
|
+
|
|
306
|
+
# The posts shared in this space
|
|
307
|
+
#
|
|
308
|
+
# @api public
|
|
309
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
310
|
+
# @return [Cursor] a cursor over the posts
|
|
311
|
+
# @example Print the shared posts
|
|
312
|
+
# space.posts.each { |post| puts post.text }
|
|
313
|
+
def posts(**params)
|
|
314
|
+
cursor(Post, "spaces/#{id}/tweets", max_results: MAX_RESULTS, app_only: true, **params)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# The users who bought a ticket to this space
|
|
318
|
+
#
|
|
319
|
+
# The authenticated user must have created the space. The endpoint takes only OAuth 2.0 user context, which the object layer cannot route around, so a client that
|
|
320
|
+
# signs with OAuth 1.0a, or authenticates as the app, is refused.
|
|
321
|
+
#
|
|
322
|
+
# @api public
|
|
323
|
+
# @param params [Hash] query parameters merged over the default parameters
|
|
324
|
+
# @return [Cursor] a cursor over the buyers
|
|
325
|
+
# @example Print the buyers of a ticketed space
|
|
326
|
+
# space.buyers.each { |user| puts user.username }
|
|
327
|
+
def buyers(**params) = cursor(User, "spaces/#{id}/buyers", max_results: MAX_RESULTS, **params)
|
|
328
|
+
|
|
329
|
+
alias_method :tweets, :posts
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resource"
|
|
4
|
+
|
|
5
|
+
module X
|
|
6
|
+
# A topic a space is about
|
|
7
|
+
#
|
|
8
|
+
# The API offers no lookup of topics, so a topic is read from the response of the space that expanded it, and the
|
|
9
|
+
# class answers no finder. from_id builds one from its identifier, which a topic the response did not expand is built
|
|
10
|
+
# as too, and which compares equal to the topic it identifies, but hydrate and refresh raise UnsupportedOperation for
|
|
11
|
+
# one that is not hydrated, since there is nothing to look it up with.
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
class Topic < Resource
|
|
15
|
+
# Every public topic field
|
|
16
|
+
#
|
|
17
|
+
# A minor release may add to it the fields the API adds, so that a lookup asks for them too; see
|
|
18
|
+
# {Resource#hydrated?} for what that means for a resource looked up with a list of fields of its own.
|
|
19
|
+
FIELDS = %w[description id name].freeze
|
|
20
|
+
|
|
21
|
+
# The default query parameters, which request every field
|
|
22
|
+
#
|
|
23
|
+
# The API offers no lookup of topics, so they are requested by the spaces that expand them, which ask for these
|
|
24
|
+
# fields, and a topic a response included with all of them is hydrated.
|
|
25
|
+
#
|
|
26
|
+
# @api public
|
|
27
|
+
# @return [Hash{String => Array<String>}] the default query parameters
|
|
28
|
+
# @example Get the default parameters
|
|
29
|
+
# X::Topic.default_params # => {"topic.fields" => [...]}
|
|
30
|
+
def self.default_params = {"topic.fields" => FIELDS}
|
|
31
|
+
|
|
32
|
+
# The key under which topics appear in the includes of a response
|
|
33
|
+
#
|
|
34
|
+
# @api private
|
|
35
|
+
# @return [String] the includes key
|
|
36
|
+
# @example Get the includes key
|
|
37
|
+
# X::Topic.__send__(:includes_key) # => "topics"
|
|
38
|
+
def self.includes_key
|
|
39
|
+
"topics"
|
|
40
|
+
end
|
|
41
|
+
private_class_method :includes_key
|
|
42
|
+
|
|
43
|
+
# @!attribute [r] name
|
|
44
|
+
# The name of the topic
|
|
45
|
+
# @api public
|
|
46
|
+
# @return [String, nil] the name
|
|
47
|
+
# @example Get the name
|
|
48
|
+
# topic.name # => "Technology"
|
|
49
|
+
attribute :name
|
|
50
|
+
|
|
51
|
+
# @!attribute [r] description
|
|
52
|
+
# What the topic is about
|
|
53
|
+
# @api public
|
|
54
|
+
# @return [String, nil] the description
|
|
55
|
+
# @example Get the description
|
|
56
|
+
# topic.description # => "All about technology"
|
|
57
|
+
attribute :description
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
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 topic trending in a place, as the trends of the place report it
|
|
12
|
+
#
|
|
13
|
+
# A trend has no identifier, and the API offers no lookup of one, so trends are read a place at a time, a place
|
|
14
|
+
# named by its Yahoo! Where On Earth identifier (WOEID), such as 1 for the whole world.
|
|
15
|
+
#
|
|
16
|
+
# @api public
|
|
17
|
+
class ::X::Trend
|
|
18
|
+
include Serialization
|
|
19
|
+
include ValueEquality
|
|
20
|
+
include ValueMarshalling
|
|
21
|
+
|
|
22
|
+
# The mixins of the class by their full names, which YARD needs to resolve them in a class opened under X
|
|
23
|
+
#
|
|
24
|
+
# @!parse
|
|
25
|
+
# include X::Resources::Serialization
|
|
26
|
+
# include X::Resources::ValueEquality
|
|
27
|
+
# include X::Resources::ValueMarshalling
|
|
28
|
+
|
|
29
|
+
# Every trend field
|
|
30
|
+
#
|
|
31
|
+
# A minor release may add to it the fields the API adds, so that the trends of a place ask for them too.
|
|
32
|
+
FIELDS = %w[trend_name tweet_count].freeze
|
|
33
|
+
# The most trends the API returns for a place, which it returns 20 of unless asked for more
|
|
34
|
+
# @api private
|
|
35
|
+
MAX_TRENDS = 50
|
|
36
|
+
private_constant :MAX_TRENDS
|
|
37
|
+
|
|
38
|
+
# The raw attributes of the trend
|
|
39
|
+
# @api public
|
|
40
|
+
# @return [Hash{String => Object}] the attributes
|
|
41
|
+
# @example Get the raw attributes
|
|
42
|
+
# trend.attrs # => {"trend_name" => "#ruby", "tweet_count" => 1234}
|
|
43
|
+
attr_reader :attrs
|
|
44
|
+
|
|
45
|
+
# The topics trending in a place
|
|
46
|
+
#
|
|
47
|
+
# The endpoint takes app-only authentication, or OAuth 2.0 as a user, so a client that signs with OAuth 1.0a
|
|
48
|
+
# reads the trends with a copy that authenticates as the app.
|
|
49
|
+
#
|
|
50
|
+
# @api public
|
|
51
|
+
# @param woeid [Integer, String] the Yahoo! Where On Earth identifier of the place, such as 1 for the world
|
|
52
|
+
# @param client [Object] the client used to make the request
|
|
53
|
+
# @param params [Hash] query parameters, such as max_trends, which is 50, the most the API returns, unless given
|
|
54
|
+
# @return [Array<Trend>] the trends, frozen
|
|
55
|
+
# @raise [ArgumentError] if the WOEID is not a number, before a request
|
|
56
|
+
# @raise [InvalidAttribute] if the response holds the trends as something other than a list of objects
|
|
57
|
+
# @example Print the topics trending in the world
|
|
58
|
+
# X::Trend.at(1, client: client).each { |trend| puts trend.name }
|
|
59
|
+
def self.at(woeid, client:, **params)
|
|
60
|
+
path = "trends/by/woeid/#{woeid_of(woeid)}"
|
|
61
|
+
body = Utils.app_client(client).get(Utils.path(path, {"trend.fields" => FIELDS, "max_trends" => MAX_TRENDS}.merge(params)), **Utils::JSON_CLASSES)
|
|
62
|
+
Shape.objects("#{self}.at", body.to_h["data"]).map { |attrs| new(attrs) }.freeze
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The WOEID of a place, which must be a number
|
|
66
|
+
#
|
|
67
|
+
# @api private
|
|
68
|
+
# @param woeid [Integer, String] the WOEID
|
|
69
|
+
# @return [String] the WOEID
|
|
70
|
+
# @raise [ArgumentError] if the WOEID is not an Integer or a String of digits
|
|
71
|
+
def self.woeid_of(woeid)
|
|
72
|
+
value = case woeid
|
|
73
|
+
when String, Integer then woeid.to_s
|
|
74
|
+
end
|
|
75
|
+
return value if value&.match?(Utils::NUMERIC_ID)
|
|
76
|
+
|
|
77
|
+
raise ArgumentError, "#{woeid.inspect} is not a WOEID: pass an Integer, or a String of digits"
|
|
78
|
+
end
|
|
79
|
+
private_class_method :woeid_of
|
|
80
|
+
|
|
81
|
+
# Initialize a trend from the attributes the API reported
|
|
82
|
+
#
|
|
83
|
+
# @api public
|
|
84
|
+
# @param attrs [Hash{String, Symbol => Object}] the attributes
|
|
85
|
+
# @return [Trend] a new trend
|
|
86
|
+
# @raise [ArgumentError] if the attributes are not a Hash
|
|
87
|
+
# @example Build a trend
|
|
88
|
+
# X::Trend.new({"trend_name" => "#ruby", "tweet_count" => 1234})
|
|
89
|
+
def initialize(attrs)
|
|
90
|
+
@attrs = Utils.deep_freeze(Utils.attributes!(attrs))
|
|
91
|
+
freeze
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The name of the trend, such as a hashtag or a phrase
|
|
95
|
+
#
|
|
96
|
+
# @api public
|
|
97
|
+
# @return [String, nil] the name
|
|
98
|
+
# @example Get the name
|
|
99
|
+
# trend.name # => "#ruby"
|
|
100
|
+
def name = attrs["trend_name"]
|
|
101
|
+
|
|
102
|
+
# The number of posts about the trend
|
|
103
|
+
#
|
|
104
|
+
# It is read from post_count, or from tweet_count, the name the API gives it before it names tweets posts there,
|
|
105
|
+
# as the post count of a user is.
|
|
106
|
+
#
|
|
107
|
+
# @api public
|
|
108
|
+
# @return [Integer, nil] the number of posts, or nil if the API reported none
|
|
109
|
+
# @raise [InvalidAttribute] if the response holds a number of posts that is not a number
|
|
110
|
+
# @example Get the number of posts
|
|
111
|
+
# trend.post_count # => 1234
|
|
112
|
+
def post_count = Utils.read("#{self.class}#post_count", attrs["post_count"] || attrs["tweet_count"]) { |value| Shape.integer(value) }
|
|
113
|
+
|
|
114
|
+
alias_method :tweet_count, :post_count
|
|
115
|
+
|
|
116
|
+
# Deconstruct the trend into what its readers read, so it matches a hash pattern
|
|
117
|
+
#
|
|
118
|
+
# A pattern that asks for every key gets name and post_count, and one can ask for the number of posts as
|
|
119
|
+
# tweet_count too, as it can of a user.
|
|
120
|
+
#
|
|
121
|
+
# @api public
|
|
122
|
+
# @param keys [Array<Symbol>, nil] the keys the pattern asks for, or nil for every reader
|
|
123
|
+
# @return [Hash{Symbol => Object}] what the readers read
|
|
124
|
+
# @raise [InvalidAttribute] if the pattern asks for a number of posts the response holds as something else
|
|
125
|
+
# @example Keep the topics of more than 10,000 posts
|
|
126
|
+
# X::Trend.at(1, client: client).select { |trend| trend in {post_count: 10_000..} }
|
|
127
|
+
def deconstruct_keys(keys) = Utils.deconstruct(self, keys, %i[name post_count], %i[tweet_count])
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|