mirror of
https://github.com/cyberarm/w3d_hub_linux_launcher.git
synced 2026-03-22 04:06:18 +00:00
424 lines
16 KiB
Ruby
424 lines
16 KiB
Ruby
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
|