rublox 0.1.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.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Rublox
6
+ # The state of the presence.
7
+ #
8
+ # Can be offline, online/on the website, playing a game, developing on studio.
9
+ module PresenceType
10
+ # The user is/was offline.
11
+ OFFLINE = :Offline
12
+ # The user is/was online (this also applies to the website).
13
+ ONLINE = :Online
14
+ # The user is/was playing a game.
15
+ GAME = :"In game"
16
+ # The user is/was developing on studio.
17
+ STUDIO = :"In Studio"
18
+
19
+ # @!visibility private
20
+ PRESENCE_MAP = [
21
+ OFFLINE,
22
+ ONLINE,
23
+ GAME,
24
+ STUDIO
25
+ ].freeze
26
+
27
+ # Convert the Roblox PresenceType enum response to rublox's PresenceType
28
+ # equivalent.
29
+ # @!visibility private
30
+ # @param enum [Integer]
31
+ # @return [Symbol]
32
+ def self.enum_to_presence_type(enum)
33
+ PRESENCE_MAP[enum]
34
+ end
35
+ end
36
+
37
+ # @note This class is handled internally by the public interface such as
38
+ # {Client#user_presence_from_id}. You should not be creating it yourself.
39
+ # The {Presence} class corresponds to a response you can get via
40
+ # https://presence.roblox.com/v1/presence/users. You can use it to get information
41
+ # about the presence states of users.
42
+ class Presence
43
+ # @return [PresenceType] the current presence type
44
+ attr_reader :presence_type
45
+
46
+ # @return [PresenceType] the last presence type of the user
47
+ attr_reader :last_presence_type
48
+
49
+ # @note Unlike it sounds, this is not a numerical ID like of a user. It's a
50
+ # randomly generated string with hexadecimal numbers containing the server's
51
+ # job ID (which looks like "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"), it can be
52
+ # also accessed in-game via the game.JobId property.
53
+ # @return [String, nil] the job ID of the game last played by the user.
54
+ attr_reader :game_job_id
55
+
56
+ # @return [Time] the date at which the user was last online
57
+ attr_reader :last_online_date
58
+
59
+ def initialize(data, client)
60
+ @presence_type = PresenceType.enum_to_presence_type(data["userPresenceType"])
61
+ @last_location = data["lastLocation"]
62
+ @place_id = data["placeId"]
63
+ @root_place_id = data["rootPlaceId"]
64
+ @game_job_id = data["gameId"]
65
+ @universe_id = data["universeId"]
66
+ @user_id = data["userId"]
67
+ @last_online_date = Time.iso8601(data["lastOnline"])
68
+
69
+ @client = client
70
+ end
71
+
72
+ # @todo add Place class
73
+ # @return [Place, nil] the place last visited by the user, can be nil if the
74
+ # user has never played a game
75
+ def place
76
+ return unless @place_id
77
+ end
78
+
79
+ # @todo add Place class
80
+ # @return [Place, nil] the root of the place last visited by the user, can
81
+ # be nil if the user has never played a game
82
+ def root_place
83
+ return unless @root_place_id
84
+ end
85
+
86
+ # @todo add Universe class
87
+ # @return [Universe, nil] the universe of the place last visited by the user,
88
+ # can be nil if the user has never played a game
89
+ def universe
90
+ return unless @universe_id
91
+ end
92
+
93
+ # @return [FullUser] the user tied to the presence
94
+ def user
95
+ @client.user_from_id(@user_id)
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rublox
4
+ # @note Only use if you have an use case that the library doesn't cover (and
5
+ # create an issue and perhaps we'll implement it!).
6
+ module Cache
7
+ # The key for the user cache.
8
+ USER = :users
9
+ # The key for the group cache.
10
+ GROUP = :groups
11
+ # The key for the page cache.
12
+ PAGE = :pages
13
+
14
+ # @!visiblity private
15
+ @cache = {
16
+ users: {},
17
+ groups: {},
18
+ pages: {}
19
+ }
20
+
21
+ # Try to get an object from cache.
22
+ # @param type [Symbol] {USER}, {GROUP} or {PAGE}
23
+ # @param id [Integer] the ID of the object
24
+ # @return [FullUser, FullGroup, Pages, nil]
25
+ def self.get(type, id)
26
+ @cache[type][id]
27
+ end
28
+
29
+ # Set an object in the cache, under the type's key.
30
+ # @param type [Symbol] {USER}, {GROUP} or {PAGE}
31
+ # @param id [Integer] the ID of the object
32
+ # @param object [FullUser, FullGroup, Pages] the object to be added to the
33
+ # cache
34
+ # @return [nil]
35
+ def self.set(type, id, object)
36
+ @cache[type][id] = object
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rublox
4
+ # This module contains errors that can be raised by rublox methods.
5
+ module Errors
6
+ # Exception raised when a user cannot be found.
7
+ class UserNotFoundError < StandardError
8
+ # @return [Integer, "(no ID information provided)"] the given user's ID
9
+ attr_reader :user_id
10
+
11
+ # @return [String, "(no username information provided)"] the given user's
12
+ # username
13
+ attr_reader :user_username
14
+
15
+ # @param user_id [Integer]
16
+ # @param username [String, nil]
17
+ def initialize(
18
+ user_id = "(no ID information provided)",
19
+ username = "(no username information provided)"
20
+ )
21
+ @user_id = user_id
22
+ @user_username = username
23
+ super("The user of ID #{user_id} and username #{username} could not be found.")
24
+ end
25
+ end
26
+
27
+ # Exception raised when a group cannot be found.
28
+ class GroupNotFoundError < StandardError
29
+ # @return [Integer] the given group's ID
30
+ attr_reader :group_id
31
+
32
+ # @param group_id [Integer]
33
+ def initialize(group_id)
34
+ @group_id = group_id
35
+ super("The group of ID #{group_id} could not be found.")
36
+ end
37
+ end
38
+
39
+ # Exception raised when a user's presence cannot be found.
40
+ class PresenceNotFoundError < StandardError
41
+ # @return [Integer] the presence user's ID
42
+ attr_reader :user_id
43
+
44
+ def initialize(user_id)
45
+ @user_id = user_id
46
+ super("The presence of the user with ID #{user_id} could not be found.")
47
+ end
48
+ end
49
+
50
+ # Exception raised when a user is not part of a group.
51
+ class MemberNotFoundError < StandardError
52
+ # @return [Integer] the given user's ID
53
+ attr_reader :user_id
54
+
55
+ # @return [Integer] the given group's ID
56
+ attr_reader :group_id
57
+
58
+ # @param id [Integer]
59
+ # @param group_id [Integer]
60
+ def initialize(id, group_id)
61
+ @user_id = id
62
+ @group_id = group_id
63
+ super("The user of ID #{id} is not part of this group of ID #{group_id}")
64
+ end
65
+ end
66
+
67
+ # Exception raised when a role doesn't exist.
68
+ class RoleNotFoundError < StandardError
69
+ # @return [Integer] the given role's ID
70
+ attr_reader :role_id
71
+
72
+ # @return [Integer] the given group's ID
73
+ attr_reader :group_id
74
+
75
+ # @param role_id [Integer]
76
+ # @param group_id [Integer]
77
+ def initialize(role_id, group_id)
78
+ @role_id = role_id
79
+ @group_id = group_id
80
+ super("The role of ID #{role_id} does not exist in group of ID #{group_id}.")
81
+ end
82
+ end
83
+
84
+ # Exception raised when an unhandled status code is returned.
85
+ class UnhandledStatusCodeError < StandardError
86
+ # @return [HTTP::Response::Status] the unhandled status code
87
+ attr_reader :status_code
88
+
89
+ # @return [String] a string containing all the errors returned by the API
90
+ # neatly formatted
91
+ attr_reader :errors
92
+
93
+ # @param status_code [Integer]
94
+ # @param errors [String, nil]
95
+ def initialize(status_code, errors = "")
96
+ super("Unhandled status code #{status_code}.\nRoblox errors:\n#{errors}")
97
+ @status_code = status_code
98
+ @errors = errors
99
+ end
100
+ end
101
+
102
+ # Exception raised when an invalid .ROBLOSECURITY cookie is used.
103
+ class InvalidROBLOSECURITYError < StandardError
104
+ def initialize
105
+ super("A valid .ROBLOSECURITY cookie needs to be passed to Rublox's constructor for this action.")
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http"
4
+ require "json"
5
+
6
+ require "rublox/util/errors"
7
+
8
+ module Rublox
9
+ # @note Only use if you have an use case that the library doesn't cover (and
10
+ # create an issue and perhaps we'll implement it!).
11
+ # The {HTTPClient} is a wrapper around the http library, designed specifically
12
+ # for the Roblox API. It automatically handles the X-CSRF-TOKEN and the
13
+ # .ROBLOSECURITY cookie.
14
+ class HTTPClient
15
+ # @param roblosecurity [String]
16
+ def initialize(roblosecurity = "")
17
+ @client = HTTP.cookies(
18
+ {
19
+ ".ROBLOSECURITY": roblosecurity
20
+ }
21
+ )
22
+ end
23
+
24
+ # Send a GET request to the specified URL.
25
+ # @param url [String]
26
+ # @return [Hash]
27
+ def get(url, *args)
28
+ request(:get, url, *args)
29
+ end
30
+
31
+ # Send a POST request to the specified URL.
32
+ # @param url [String]
33
+ # @return [Hash]
34
+ def post(url, *args)
35
+ request(:post, url, *args)
36
+ end
37
+
38
+ # Send a PATCH request to the specified URL.
39
+ # @param url [String]
40
+ # @return [Hash]
41
+ def patch(url, *args)
42
+ request(:patch, url, *args)
43
+ end
44
+
45
+ # Send a DELETE request to the specified URL.
46
+ # @param url [String]
47
+ # @return [Hash]
48
+ def delete(url, *args)
49
+ request(:delete, url, *args)
50
+ end
51
+
52
+ private
53
+
54
+ # @param verb [Symbol]
55
+ # @param url [String]
56
+ # @return [Hash]
57
+ def request(verb, url, *args)
58
+ response = @client.request(verb, url, *args)
59
+ return JSON.parse(response.body) if response.status == 200
60
+
61
+ handle_status_code(response, verb, url, *args)
62
+ end
63
+
64
+ # @param response [HTTP::Response]
65
+ def handle_status_code(response, verb, url, *args)
66
+ case response.status
67
+ # token validation failed
68
+ when 403
69
+ @client = @client.headers(
70
+ {
71
+ "x-csrf-token": response.headers["x-csrf-token"]
72
+ }
73
+ )
74
+ # retry the request
75
+ request(verb, url, *args)
76
+ # invalid .ROBLOSECURITY cookie
77
+ when 401
78
+ raise Errors::InvalidROBLOSECURITYError
79
+ else
80
+ raise Errors::UnhandledStatusCodeError.new(response.status, get_errors_from_response(response))
81
+ end
82
+ end
83
+
84
+ # @param response [HTTP::Response]
85
+ def get_errors_from_response(response)
86
+ body = JSON.parse(response.body)
87
+ rescue JSON::ParserError
88
+ "\ncould not parse errors, raw body:\n#{response.body}\n"
89
+ else
90
+ body["errors"].reduce("") do |error_message, error|
91
+ error_message + "\tCode: #{error['code']}\n\tMessage: #{error['message']}\n\n"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rublox/util/url"
4
+ require "rublox/util/cache"
5
+
6
+ module Rublox
7
+ # The order to sort pages in.
8
+ module SortOrder
9
+ # Sort the pages ina ascending order.
10
+ ASCENDING = "Asc"
11
+ # Sort the pages in descending order.
12
+ DESCENDING = "Desc"
13
+ end
14
+
15
+ # The {Pages} class acts as an iterator over pages returned by the API.
16
+ # @!visibility private
17
+ # @todo Make more customizable, polish and battle-test
18
+ # @note Only use if you have an use case that the library doesn't cover (and
19
+ # create an issue and perhaps we'll implement it!).
20
+ class Pages
21
+ # How many items are returned per page
22
+ PAGE_DATA_LIMIT = 100
23
+
24
+ # @!visibility private
25
+ DEFAULT_REQUEST_PARAMETERS = {
26
+ params: {
27
+ sortOrder: SortOrder::DESCENDING,
28
+ limit: PAGE_DATA_LIMIT
29
+ }
30
+ }.freeze
31
+
32
+ # @param client [Client]
33
+ # @param initial_data [Hash]
34
+ # @param url [String]
35
+ # @param request_params [Hash]
36
+ def initialize(client, initial_data, url, request_params = {}, &data_handler)
37
+ request_params[:params] = DEFAULT_REQUEST_PARAMETERS[:params].merge(
38
+ request_params
39
+ )
40
+
41
+ @client = client
42
+ @raw_data = initial_data
43
+ @url = url
44
+ @request_parameters = request_parameters
45
+ @data_handler = data_handler
46
+
47
+ @data = []
48
+ end
49
+
50
+ # Iterate over the pages
51
+ def each
52
+ raise unless block_given?
53
+ # i = 0
54
+
55
+ # until @raw_data
56
+ # if i == @data.length
57
+ # i = 0
58
+ # data = Cache.get(Cache::PAGE, @raw_data["nextPageCursor"])
59
+ # if data
60
+ # @data = data
61
+ # else
62
+ # @raw_data, @data = next_page
63
+ # break if @raw_data.empty?
64
+
65
+ # Cache.set(Cache::PAGE, @raw_data["nextPageCursor"], @data)
66
+ # end
67
+ # end
68
+
69
+ # yield @data[i]
70
+ # i += 1
71
+ # end
72
+ end
73
+
74
+ private
75
+
76
+ # @return [Array] tuple {raw data, processed data}
77
+ def next_page
78
+ @request_parameters[:params][:cursor] = @raw_data["nextPageCursor"]
79
+
80
+ data = @client.http_client.get(@url, @request_parameters)
81
+
82
+ [data["data"], @data_handler.call(data["data"])]
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rublox
4
+ # Module for making URLs.
5
+ module URL
6
+ # The base URL to be used. If using a proxy, you can change the URL as long
7
+ # as it follows Roblox's API structure:
8
+ # https://users.roblox.com/v1/users/1
9
+ # Bad:
10
+ # https://myproxy.test/get-user/1
11
+ # https://myproxy.test/users/1
12
+ # https://users.myproxy.test/users/1
13
+ # Good:
14
+ # https://users.myproxy.test/v1/users/1
15
+ BASE_URL = "roblox.com"
16
+
17
+ # Creates an endpoint URL from the the given ApiSite and path.
18
+ # @param api_site [String] ApiSite
19
+ # @param path [String]
20
+ # @return [String] the endpoint
21
+ def self.endpoint(api_site, path)
22
+ "https://#{api_site}.#{BASE_URL}/#{path}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rublox
4
+ # Current version of rublox
5
+ VERSION = "0.1.0"
6
+ end
data/lib/rublox.rb ADDED
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http"
4
+
5
+ require "rublox/version"
6
+ require "rublox/util/http_client"
7
+ require "rublox/util/cache"
8
+ require "rublox/models/full_user"
9
+ require "rublox/models/full_group"
10
+ require "rublox/models/presence"
11
+
12
+ # rublox is a Roblox web API wrapper written in Ruby. It aims to provide an
13
+ # object oriented interface to get and modify data from Roblox's web API.
14
+ #
15
+ # Repository: https://github.com/roblox-api-wrappers/rublox
16
+ #
17
+ # Docs: https://rubydoc.info/gems/rublox
18
+ module Rublox
19
+ # The {Client} object is the gateway to the API. Tt supplies methods that
20
+ # return classes modeled after the interactions you can do with the API.
21
+ #
22
+ # Initialize the client with a .ROBLOSECURITY cookie if you need functionality
23
+ # that requires it.
24
+ # @example
25
+ # require "rublox"
26
+ # # without a cookie
27
+ # client = Rublox::Client.new
28
+ # # with a cookie
29
+ # client = Rublox::Client.new("_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this ...")
30
+ class Client
31
+ # @note The HTTP client should only be used when there are no methods
32
+ # provided by the library to achieve what you want.
33
+ # @return [HTTPClient]
34
+ attr_reader :http_client
35
+
36
+ # Initialize the client with a .ROBLOSECURITY cookie if you require functionality
37
+ # that needs it.
38
+ # @example
39
+ # require "rublox"
40
+ # # without a cookie
41
+ # client = Rublox::Client.new
42
+ # # with a cookie
43
+ # client = Rublox::Client.new("_|WARNING:-DO-NOT-SHARE-THIS.--Sharing-this ...")
44
+ # @param roblosecurity [String, nil] a valid .ROBLOSECURITY cookie
45
+ def initialize(roblosecurity = "")
46
+ @http_client = HTTPClient.new(roblosecurity)
47
+ end
48
+
49
+ # @example
50
+ # client = Rublox::Client.new
51
+ # user = client.user_from_id(1)
52
+ # puts user.username # -> Roblox
53
+ # @param id [Integer] the user's ID
54
+ # @return [FullUser] a model of the user specified by the ID
55
+ def user_from_id(id)
56
+ user = Cache.get(Cache::USER, id)
57
+ return user if user
58
+
59
+ data = @http_client.get(
60
+ URL.endpoint("users", "v1/users/#{id}")
61
+ )
62
+ rescue Errors::UnhandledStatusCodeError
63
+ raise Errors::UserNotFoundError, id
64
+ else
65
+ user = FullUser.new(
66
+ data,
67
+ self
68
+ )
69
+ Cache.set(Cache::USER, id, user)
70
+
71
+ user
72
+ end
73
+
74
+ # @note This method sends 2 requests, use {#user_from_id} if possible.
75
+ # @example
76
+ # client = Rublox::Client.new
77
+ # user = client.user_from_username("Roblox")
78
+ # puts user.id # -> 1
79
+ # @param username [String] the user's username
80
+ # @return [FullUser] a model of the user specified by the ID
81
+ def user_from_username(username)
82
+ data = @http_client.post(
83
+ URL.endpoint("users", "/v1/usernames/users"),
84
+ json: {
85
+ usernames: [username],
86
+ excludeBannedUsers: false
87
+ }
88
+ )["data"]
89
+ raise Errors::UserNotFoundError.new(nil, username) if data.empty?
90
+
91
+ user_from_id(
92
+ data[0]["id"]
93
+ )
94
+ end
95
+
96
+ # @example
97
+ # client = Rublox::Client.new
98
+ # group = client.group_from_id(1)
99
+ # puts group.name # -> RobloHunks
100
+ # @param id [Integer] the groups's ID
101
+ # @return [FullGroup] a model of the group specified by the ID
102
+ def group_from_id(id)
103
+ group = Cache.get(Cache::GROUP, id)
104
+ return group if group
105
+
106
+ data = @http_client.get(
107
+ URL.endpoint("groups", "v1/groups/#{id}")
108
+ )
109
+ rescue Errors::UnhandledStatusCodeError
110
+ raise Errors::GroupNotFoundError, id
111
+ else
112
+ group = FullGroup.new(data, self)
113
+ Cache.set(Cache::GROUP, id, group)
114
+
115
+ group
116
+ end
117
+
118
+ # @param id [Integer] the user's ID
119
+ # @return [Presence] a model of the presence specified by the user's ID
120
+ def user_presence_from_id(id)
121
+ data = http_client.post(
122
+ URL.endpoint("presence", "v1/presence/users"),
123
+ json: {
124
+ userIds: [id]
125
+ }
126
+ )
127
+ rescue Errors::UnhandledStatusCodeError
128
+ raise Errors::PresenceNotFoundError, id
129
+ else
130
+ Presence.new(
131
+ data["userPresences"][0],
132
+ self
133
+ )
134
+ end
135
+ end
136
+ end
data/rublox.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rublox/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rublox"
7
+ spec.version = Rublox::VERSION
8
+ spec.authors = %w[Zamdie Keef]
9
+ spec.email = "rorg.devv@gmail.com"
10
+ spec.summary = "A Roblox web API wrapper written in Ruby"
11
+ spec.description = "This gem allows easy interaction with the Roblox web API via class models."
12
+ spec.homepage = "https://github.com/roblox-api-wrappers/rublox"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 3.0"
15
+ spec.files = Dir[
16
+ "LICENSE",
17
+ "CHANGELOG.MD",
18
+ "lib/**/*.rb",
19
+ "rublox.gemspec",
20
+ "Gemfile",
21
+ "Rakefile"
22
+ ]
23
+ spec.extra_rdoc_files = ["README.MD"]
24
+
25
+ spec.add_dependency "http", "~> 5.0.2"
26
+
27
+ spec.add_development_dependency "dotenv", "~> 2.7"
28
+ spec.add_development_dependency "rake", "~> 13.0.6"
29
+ spec.add_development_dependency "rubocop", "~> 1.21.0"
30
+ spec.add_development_dependency "rubocop-performance", "~> 1.11.5"
31
+ spec.add_development_dependency "yard", "~> 0.9.26"
32
+ end