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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X
4
+ module Resources
5
+ # The number the API publishes for a collection of a resource, such as the followers_count of a user, which a
6
+ # cursor over the collection reads, included into Resource
7
+ # @api private
8
+ module PublishedCount
9
+ private
10
+
11
+ # A block reading the attribute holding the number the API publishes
12
+ #
13
+ # A resource without the attribute, such as a stub, is hydrated to read it, which costs one lookup rather than
14
+ # paging through the collection. Given fresh: true, as by a cursor that was refreshed, the block looks the
15
+ # resource up again, as refresh does, to read the number the API publishes now.
16
+ #
17
+ # @api private
18
+ # @param total [Symbol, nil] the attribute name, or nil if the API publishes no number
19
+ # @return [Proc, nil] the block, or nil if the API publishes no number
20
+ def counter(total)
21
+ return if total.nil?
22
+
23
+ lambda do |fresh: false|
24
+ # @type var fresh: bool
25
+ fresh ? refresh&.public_send(total) : public_send(total) || hydrate&.public_send(total)
26
+ end
27
+ end
28
+ end
29
+ private_constant :PublishedCount
30
+ end
31
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module X
4
+ module Resources
5
+ # Resolves the posts a post refers to through its referenced_posts attribute
6
+ #
7
+ # Internal to x-resources: the methods it gives a post, such as replied_to, 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 References
12
+ # Referenced post type for replies
13
+ # @api private
14
+ REPLIED_TO = "replied_to"
15
+ # Referenced post type for quotes
16
+ # @api private
17
+ QUOTED = "quoted"
18
+ # Referenced post type for reposts, as the API labels them
19
+ # @api private
20
+ REPOSTED = "reposted"
21
+ # Referenced post type for reposts, as the API documentation labels them
22
+ # @api private
23
+ RETWEETED = "retweeted"
24
+ private_constant :REPLIED_TO, :QUOTED, :REPOSTED, :RETWEETED
25
+
26
+ # The referenced posts, resolved from the includes or built as stubs
27
+ #
28
+ # @api public
29
+ # @return [Array<Post>] the referenced posts
30
+ # @raise [InvalidAttribute] if the response holds a referenced post that is not an object
31
+ # @example Get the referenced posts
32
+ # post.references
33
+ def references
34
+ referenced_posts.filter_map do |reference|
35
+ resolve(Post, reference["id"]) #: Post?
36
+ end.freeze
37
+ end
38
+
39
+ # The post this post replies to
40
+ #
41
+ # @api public
42
+ # @return [Post, nil] the replied-to post
43
+ # @raise [InvalidAttribute] if the response holds a referenced post that is not an object
44
+ # @example Get the replied-to post
45
+ # post.replied_to
46
+ def replied_to
47
+ reference(REPLIED_TO)
48
+ end
49
+
50
+ # The post this post quotes
51
+ #
52
+ # @api public
53
+ # @return [Post, nil] the quoted post
54
+ # @raise [InvalidAttribute] if the response holds a referenced post that is not an object
55
+ # @example Get the quoted post
56
+ # post.quoted
57
+ def quoted
58
+ reference(QUOTED)
59
+ end
60
+
61
+ # The post this post reposts
62
+ #
63
+ # @api public
64
+ # @return [Post, nil] the reposted post
65
+ # @raise [InvalidAttribute] if the response holds a referenced post that is not an object
66
+ # @example Get the reposted post
67
+ # post.reposted
68
+ def reposted
69
+ reference(REPOSTED, RETWEETED)
70
+ end
71
+
72
+ # Check whether this post is a reply
73
+ #
74
+ # @api public
75
+ # @return [Boolean] true if the post replies to another post
76
+ # @example Check whether a post is a reply
77
+ # post.reply?
78
+ def reply?
79
+ !replied_to.nil?
80
+ end
81
+
82
+ # Check whether this post is a quote
83
+ #
84
+ # @api public
85
+ # @return [Boolean] true if the post quotes another post
86
+ # @example Check whether a post is a quote
87
+ # post.quote?
88
+ def quote?
89
+ !quoted.nil?
90
+ end
91
+
92
+ # Check whether this post is a repost
93
+ #
94
+ # @api public
95
+ # @return [Boolean] true if the post reposts another post
96
+ # @example Check whether a post is a repost
97
+ # post.repost?
98
+ def repost?
99
+ !reposted.nil?
100
+ end
101
+
102
+ alias_method :retweeted, :reposted
103
+ alias_method :retweet?, :repost?
104
+
105
+ private
106
+
107
+ # Resolve the first referenced post of any of some types
108
+ # @api private
109
+ # @param types [Array<String>] the referenced post types
110
+ # @return [Post, nil] the referenced post or nil if there is none of those types
111
+ # @raise [InvalidAttribute] if the response holds a referenced post that is not an object
112
+ def reference(*types)
113
+ found = referenced_posts.find { |element| types.include?(element["type"]) }
114
+ return if found.nil?
115
+
116
+ resolve(Post, found["id"]) #: Post?
117
+ end
118
+ end
119
+ private_constant :References
120
+ end
121
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "user"
4
+ require_relative "utils"
5
+
6
+ module X
7
+ module Resources
8
+ # Relating users, posts, and lists to the authenticated user, and removing the relations
9
+ #
10
+ # A relation, such as following a user or liking a post, is written as the authenticated user alone, so the methods
11
+ # of a client that write one, such as follow and like, write it here, for the user the client authenticates as,
12
+ # and no user does.
13
+ #
14
+ # @api private
15
+ module RelationWrites
16
+ extend self
17
+
18
+ # Relate a resource to the authenticated user and report the resulting state
19
+ #
20
+ # @api private
21
+ # @param client [Object] the client used to make the request
22
+ # @param user [Integer, String] the identifier of the authenticated user
23
+ # @param relation [String] the relation endpoint, such as following, likes, or pinned_lists
24
+ # @param field [Hash{String => String}] the request body, the identifier of the related resource under its field
25
+ # @param states [Array<String>] the response fields reporting the state, any of which is true once it exists
26
+ # @return [Boolean] true if the relation now exists
27
+ # @raise [ArgumentError] if the identifier of the authenticated user is not a number, before a request
28
+ # @example Like a post as the authenticated user
29
+ # X::Resources::RelationWrites.relate(client, client.current_user_id, "likes", {"tweet_id" => "1234567890"}, "liked")
30
+ def relate(client, user, relation, field, *states)
31
+ body = client.post("users/#{Utils.id_of(user, User)}/#{relation}", field, **Utils::JSON_CLASSES)
32
+ states.any? { |state| Utils.written(body, state).eql?(true) }
33
+ end
34
+
35
+ # Remove a relation from the authenticated user and report the resulting state
36
+ #
37
+ # @api private
38
+ # @param client [Object] the client used to make the request
39
+ # @param user [Integer, String] the identifier of the authenticated user
40
+ # @param relation [String] the relation endpoint, such as following, likes, or pinned_lists
41
+ # @param target [String] the identifier of the related resource
42
+ # @param state [String] the response field reporting the state
43
+ # @return [Boolean] true if the relation no longer exists
44
+ # @raise [ArgumentError] if the identifier of the authenticated user is not a number, before a request
45
+ # @example Unlike a post as the authenticated user
46
+ # X::Resources::RelationWrites.unrelate(client, client.current_user_id, "likes", "1234567890", "liked")
47
+ def unrelate(client, user, relation, target, state)
48
+ body = client.delete("users/#{Utils.id_of(user, User)}/#{relation}/#{target}", **Utils::JSON_CLASSES)
49
+ Utils.written(body, state).eql?(false)
50
+ end
51
+ end
52
+ private_constant :RelationWrites
53
+ end
54
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "x/core"
4
+ require_relative "page_limit"
5
+ require_relative "utils"
6
+
7
+ module X
8
+ module Resources
9
+ # The relationships of a user with other users, read as the authenticated user sees them
10
+ #
11
+ # It reads them and changes none: a relationship changes as the authenticated user alone, so the client changes it,
12
+ # as in client.follow(user), rather than a user, which may be anyone.
13
+ #
14
+ # Internal to x-resources: the methods it gives a user, such as follows?, are public API, but the module is only how
15
+ # they are shared, and which classes extend or include it can change within 1.x.
16
+ #
17
+ # @api semipublic
18
+ module Relationships
19
+ # Check whether this user follows a user
20
+ #
21
+ # When either user is the authenticated user, one lookup of the other's connection_status answers.
22
+ # Otherwise the users this user follows are scanned until one matches, up to 1,000 a page, and the
23
+ # API bills every user returned, so checking an account that follows thousands can cost dollars, and max_pages
24
+ # limits the pages the scan reads, raising PageLimitReached rather than read past them. A client that
25
+ # authenticates as the app alone has no authenticated user, which the API refuses to look up, so it scans.
26
+ # Any other failure to look the authenticated user up raises, rather than scan every user this one follows.
27
+ #
28
+ # @api public
29
+ # @param user [User, String, Integer] the user or their identifier
30
+ # @param max_pages [Integer, nil] the most pages of followed users to scan, or nil for no limit
31
+ # @return [Boolean] true if this user follows the user
32
+ # @raise [ArgumentError] if max_pages is neither an Integer of at least 1 nor nil, before a request
33
+ # @raise [PageLimitReached] if the scan reads max_pages pages without the user, and the API names another
34
+ # @example Check whether the authenticated user follows someone, in one lookup
35
+ # client.current_user!.follows?(other)
36
+ # @example Scan no more than five pages of the users another user follows
37
+ # X::User.find("jack", client: client).follows?(other, max_pages: 5)
38
+ def follows?(user, max_pages: nil)
39
+ target, max_pages = User.from_id(user), PageLimit.check!(max_pages)
40
+ case authenticated_user_id
41
+ when id then connection_status_of(target).include?("following")
42
+ when target.id then connection_status_of(self).include?("followed_by")
43
+ else PageLimit.scan(following.stubs, target, what: "User#follows?", max_pages:)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # The identifier of the authenticated user, when the client knows it
50
+ #
51
+ # A client that authenticates as the app alone has no authenticated user, and asks the API for one in vain,
52
+ # so the refusal of the credentials of the client leaves the identifier unknown rather than end the check. Any
53
+ # other error, such as a rate limit, a failure of the API, or of the network, ends it, since a scan in its place
54
+ # would page through every user this one follows, which the API bills.
55
+ #
56
+ # @api private
57
+ # @return [Integer, nil] the identifier, or nil if the client has no current_user_id or cannot read one
58
+ # @raise [X::Error] if the API fails to answer for another reason than the credentials of the client
59
+ def authenticated_user_id
60
+ current = client! #: untyped
61
+ current.current_user_id if current.respond_to?(:current_user_id)
62
+ rescue X::Forbidden, X::Unauthorized
63
+ nil
64
+ end
65
+
66
+ # How the authenticated user is connected to a user, in one lookup
67
+ # @api private
68
+ # @param user [User, String, Integer] the user or their identifier
69
+ # @return [Array<String>] the connection statuses, empty if the user was not found
70
+ def connection_status_of(user)
71
+ found = User.find(user, client: client!, "user.fields": "connection_status", "post.fields": nil, expansions: nil)
72
+ Array(found&.connection_status)
73
+ end
74
+ end
75
+ private_constant :Relationships
76
+ end
77
+ end