class W3DHub class Api LOG_TAG = "W3DHub::Api".freeze API_TIMEOUT = 10 # seconds USER_AGENT = "Cyberarm's Linux Friendly W3D Hub Launcher v#{W3DHub::VERSION}".freeze DEFAULT_HEADERS = [ ["user-agent", USER_AGENT], ["accept", "application/json"] ].freeze FORM_ENCODED_HEADERS = [ ["user-agent", USER_AGENT], ["accept", "application/json"], ["content-type", "application/x-www-form-urlencoded"] ].freeze def self.on_thread(method, *args, &callback) Api.send(method, *args, &callback) end #! === W3D Hub API === !# W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # ALT_W3DHUB_API_ENDPOINT = "https://w3dhub-api.w3d.cyberarm.dev".freeze # "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # HTTP_CLIENTS = {} def self.async_http(method:, path:, headers:, body:, backend:, async:, &callback) raise "NO CALLBACK DEFINED!" unless callback case backend when :w3dhub endpoint = W3DHUB_API_ENDPOINT when :alt_w3dhub endpoint = ALT_W3DHUB_API_ENDPOINT when :gsh endpoint = SERVER_LIST_ENDPOINT end # Handle arbitrary urls that may come through if path.start_with?("http") uri = URI(path) endpoint = uri.origin path = uri.request_uri end url = "#{endpoint}#{path}" logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." } # Inject Authorization header if account data is populated if Store.account logger.debug(LOG_TAG) { " Injecting Authorization header..." } headers = headers.dup headers << ["authorization", "Bearer #{Store.account.access_token}"] end Store.network_manager.request(method, url, headers, body, async, &callback) end def self.post(path:, headers: DEFAULT_HEADERS, body: nil, backend: :w3dhub, async: true, &callback) async_http(method: :post, path: path, headers: headers, body: body, backend: backend, async: async, &callback) end def self.get(path:, headers: DEFAULT_HEADERS, body: nil, backend: :w3dhub, async: true, &callback) async_http(method: :get, path: path, headers: headers, body: body, backend: backend, async: async, &callback) end # Api.get but handles any URL instead of known hosts def self.fetch(path:, headers: DEFAULT_HEADERS, body: nil, backend: :w3dhub, async: true, &callback) async_http(method: :get, path: path, headers: headers, body: body, backend: backend, async: async, &callback) end # Method: POST # FORMAT: JSON # /apis/launcher/1/user-login # For an already logged in user the launcher sends # a "refreshToken" in the data field: data={"refreshToken":"TOKEN_STRING"} # # For a logging in user the launcher sends # data={"username":"NAME","password":"password_as_plaintext_but_over_https"} # # On successful login/token refresh the service responds with: # {"session_token":"string","userid:"1234"...} # # On a failed login the service responds with: # {"error":"login-failed"} def self.refresh_user_login(refresh_token, backend = :w3dhub, &callback) body = URI.encode_www_form("data": JSON.dump({ refreshToken: refresh_token })) handler = lambda do |result| if result.okay? user_data = JSON.parse(result.data, symbolize_names: true) if user_data[:error] callback.call(CyberarmEngine::Result.new(data: false)) next end user_details_data = user_details(user_data[:userid]) || {} callback.call(CyberarmEngine::Result.new(data: Account.new(user_data, user_details_data))) else logger.error(LOG_TAG) { "Failed to fetch refresh user login:" } logger.error(LOG_TAG) { result.error } callback.call(result) end end post(path: "/apis/launcher/1/user-login", headers: FORM_ENCODED_HEADERS, body: body, backend: backend, &handler) end # See #user_refresh_token def self.user_login(username, password, backend = :w3dhub, &callback) body = URI.encode_www_form("data": JSON.dump({ username: username, password: password })) handler = lambda do |result| if result.okay? user_data = JSON.parse(result.data, symbolize_names: true) if user_data[:error] callback.call(CyberarmEngine::Result.new(data: false)) next end user_details_data = user_details(user_data[:userid]) || {} callback.call(CyberarmEngine::Result.new(data: Account.new(user_data, user_details_data))) else logger.error(LOG_TAG) { "Failed to fetch user login:" } logger.error(LOG_TAG) { result.error } callback.call(result) end end post(path: "/apis/launcher/1/user-login", headers: FORM_ENCODED_HEADERS, body: body, backend: backend, &handler) end # /apis/w3dhub/1/get-user-details # # Response: avatar-uri (Image download uri), id, username def self.user_details(id, backend = :w3dhub, &callback) body = URI.encode_www_form("data": JSON.dump({ id: id })) handler = lambda do |result| if result.okay? callback.call(CyberarmEngine::Result.new(data: JSON.parse(result.data, symbolize_names: true))) else logger.error(LOG_TAG) { "Failed to fetch user details:" } logger.error(LOG_TAG) { result.error } callback.call(result) end end post(path: "/apis/w3dhub/1/get-user-details", headers: FORM_ENCODED_HEADERS, body: body, backend: backend, &handler) end # /apis/w3dhub/1/get-service-status # Service response: # {"services":{"authentication":true,"packageDownload":true}} def self.service_status(backend = :w3dhub, &callback) handler = lambda do |result| if result.okay? callback.call(CyberarmEngine::Result.new(data: ServiceStatus.new(result.data))) else logger.error(LOG_TAG) { "Failed to fetch service status:" } logger.error(LOG_TAG) { result.error } callback.call(result) end end post(path: "/apis/w3dhub/1/get-service-status", backend: backend, &handler) end # /apis/launcher/1/get-applications # Client sends an Authorization header bearer token which is received from logging in (Optional) # Launcher sends an empty data request: data={} # Response is a list of applications/games def self.applications(backend = :w3dhub, &callback) async = !callback.nil? # Complicated why to "return" direct value callback = ->(result) { result } handler = lambda do |result| if result.okay? callback.call(CyberarmEngine::Result.new(data: Applications.new(result.data, backend))) else logger.error(LOG_TAG) { "Failed to fetch applications list:" } logger.error(LOG_TAG) { result.error } callback.call(result) end end post(path: "/apis/launcher/1/get-applications", async: async, backend: backend, &handler) end # Populate applications list from primary and alternate backends # (alternate only has latest public builds of _most_ games) def self._applications(&callback) handler = lambda do |result| # nothing special on offer if we're not logged in applications_primary = Store.account ? Api.applications(:w3dhub).data : false applications_alternate = Api.applications(:alt_w3dhub).data # Fail if we fail to fetch applications list from either backend unless applications_primary || applications_alternate callback.call(CyberarmEngine::Result.new) next end unless applications_primary callback.call(CyberarmEngine::Result.new(data: applications_alternate)) next end # Merge the two app lists together apps = applications_alternate if applications_primary applications_primary.games.each do |game| # Check if game exists in alternate list _game = apps.games.find { |g| g.id == game.id } unless _game apps.games << game # App didn't exist in alternates list # comparing channels isn't useful next end # If it does, check that all of its channels also exist in alternate list # and that the primary versions are the same as the alternates list game.channels.each do |channel| _channel = _game.channels.find { |c| c.id == channel.id } unless _channel _game.channels << channel # App didn't have channel in alternates list # comparing channel isn't useful next end # If channel versions and access levels match then all's well if channel.current_version == _channel.current_version && channel.user_level == _channel.user_level # All's Well! next end # If the access levels don't match then overwrite alternate's channel with primary's channel if channel.user_level != _channel.user_level # Replace alternate's channel with primary's channel _game.channels[_game.channels.index(_channel)] = channel # Replaced, continue. next end # If versions don't match then pick whichever one is higher if Gem::Version.new(channel.current_version) > Gem::Version.new(_channel.current_version) # Replace alternate's channel with primary's channel _game.channels[_game.channels.index(_channel)] = channel else # Do nothing, alternate backend version is greater. end end end end callback.call(CyberarmEngine::Result.new(data: apps)) end # Bit hacky but we just need to run this handler from the networking thread and async reactor get(path: "", backend: nil, &handler) end # /apis/w3dhub/1/get-news # Client sends an Authorization header bearer token which is received from logging in (Optional) # Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates) # Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one def self.news(category, backend = :w3dhub, &callback) handler = lambda do |result| if result.okay? callback.call(CyberarmEngine::Result.new(data: News.new(result.data))) else logger.error(LOG_TAG) { "Failed to fetch news for:" } logger.error(LOG_TAG) { category } logger.error(LOG_TAG) { result.error } callback.call(result) end end body = URI.encode_www_form("data": JSON.dump({ category: category })) post(path: "/apis/w3dhub/1/get-news", headers: FORM_ENCODED_HEADERS, body: body, backend: backend, &handler) end # Downloading games # /apis/launcher/1/get-package-details # client requests package details: data={"packages":[{"category":"games","name":"apb.ico","subcategory":"apb","version":""}]} def self.package_details(packages, backend = :w3dhub, &callback) handler = lambda do |result| if result.okay? hash = JSON.parse(result.data, symbolize_names: true) callback.call(CyberarmEngine::Result.new(data: hash[:packages].map { |pkg| Package.new(pkg) })) else logger.error(LOG_TAG) { "Failed to fetch package details for:" } logger.error(LOG_TAG) { packages } logger.error(LOG_TAG) { result.error } callback.call(result) end end body = URI.encode_www_form("data": JSON.dump({ packages: packages })) post(path: "/apis/launcher/1/get-package-details", headers: FORM_ENCODED_HEADERS, body: body, backend: backend, &handler) end # /apis/launcher/1/get-package # client requests package: data={"category":"games","name":"ECW_Asteroids.zip","subcategory":"ecw","version":"1.0.0.0"} # # server responds with download bytes, probably supports chunked download and resume # FIXME: REFACTOR Cache.fetch_package to use HttpClient def self.package(package, &callback) Cache.fetch_package(package, callback) end # /apis/w3dhub/1/get-events # # clients requests events: data={"serverPath":"apb"} def self.events(app_id, backend = :w3dhub, &callback) handler = lambda do |result| if result.okay? array = JSON.parse(result.data, symbolize_names: true) callback.call(CyberarmEngine::Result.new(data: array.map { |e| Event.new(e) })) else callback.call(result) end end body = URI.encode_www_form("data": JSON.dump({ serverPath: app_id })) post(path: "/apis/w3dhub/1/get-server-events", headers: FORM_ENCODED_HEADERS, body: body, backend: backend, &handler) end #! === Server List API === !# # SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze SERVER_LIST_ENDPOINT = "https://gsh.w3d.cyberarm.dev".freeze # SERVER_LIST_ENDPOINT = "http://127.0.0.1:9292".freeze # Method: GET # FORMAT: JSON # /listings/getAll/v2?statusLevel=#{0-2} # statusLevel = 0 returns: # id, game, address, port, and region # statusLevel = 1 returns: (This is the default for the Launcher) # id, game, address, port, region, and status: # name, map, maxplayers, numplayers, started (DateTime), and remaining (RenTime) # statusLevel = 2 returns: # id, game, address, port, region and # ...status: # name, map, maxplayers, numplayers, started (DateTime), and remaining (RenTime) # ...teams[]: # id, name, score, kills, deaths # ...players[]: # nick, team (index of teams array), score, kills, deaths def self.server_list(level = 1, backend = :gsh, &callback) handler = lambda do |result| if result.okay? data = JSON.parse(result.data, symbolize_names: true) callback.call(CyberarmEngine::Result.new(data: data.map { |hash| ServerListServer.new(hash) })) else callback.call(result) end end get(path: "/listings/getAll/v2?statusLevel=#{level}", backend: backend, &handler) end # /listings/getStatus/v2/:id?statusLevel=#{0-2} # statusLevel = 0 returns: # Empty/Blank response, assume 500 or 400 error # statusLevel = 1 returns: # name, map, maxplayers, numplayers, started (DateTime), remaining (RenTime) # statusLevel = 2 returns: # name, map, maxplayers, numplayers, started (DateTime), remaining (RenTime) # ...teams[]: # id, name, score, kills, deaths # ...players[]: # nick, team (index of teams array), score, kills, deaths def self.server_details(id, level, backend = :gsh, &callback) return false unless id && level handler = lambda do |result| if result.okay? callback.call(CyberarmEngine::Result.new(data: JSON.parse(result.data, symbolize_names: true))) else callback.call(result) end end get(path: "/listings/getStatus/v2/#{id}?statusLevel=#{level}", backend: backend, &handler) end # /listings/push/v2/negotiate?negotiateVersion=1 ##? /listings/push/v2/?id=#{websocket token?} ## Websocket server list listener def self.server_list_push(id, &callback) end end end