rublox 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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