Current section
Files
Jump to
Current section
Files
lib/soulless/websocket/implementation/lobby.ex
defmodule Soulless.Websocket.Implementation.Lobby do
require Logger
alias Soulless.Game.Lq
alias Soulless.Packet.GamePacket
@behaviour Soulless.Websocket.Implementation
@impl Soulless.Websocket.Implementation
def init(opts) do
login_method = Keyword.fetch!(opts, :login_method)
login_method
|> required_credentials()
|> Keyword.new(fn key -> {key, Keyword.fetch!(opts, key)} end)
|> Keyword.put(:login_method, login_method)
|> Keyword.put(:endpoint_uri, nil)
|> Keyword.put(:server, Keyword.fetch!(opts, :server))
|> Map.new()
end
@impl Soulless.Websocket.Implementation
def fetch_state(state, path) do
get_in(state, path)
end
@impl Soulless.Websocket.Implementation
def endpoint_uri(state) do
state.endpoint_uri
end
@impl Soulless.Websocket.Implementation
def resolve_endpoint(state) do
case Soulless.Endpoint.lobby(state.server) do
{:ok, %{endpoint_url: endpoint_url, passport_url: passport_url, version: version}} ->
state =
Map.merge(state, %{
endpoint_uri: URI.parse(endpoint_url),
passport_url: passport_url,
version: version
})
{:ok, state}
{:error, error} ->
{:error, error}
end
end
@impl Soulless.Websocket.Implementation
def handle_auth_ready(%{login_method: :yostar = login_method} = state) do
device_id =
if state[:device_id] do
state[:device_id]
else
:crypto.hash(:md5, state.token)
|> Soulless.Util.format_uuid()
end
with {:ok, result} <-
Soulless.Auth.Yostar.login(
state.email,
state.token,
device_id,
state.server
) do
request = ws_oauth2_token_request(result.user_id, state.version, result.token, login_method)
{:send, request, state}
end
end
def handle_auth_ready(%{login_method: :yostar_session = login_method} = state) do
with {:ok, token} <-
Soulless.Auth.Yostar.login_with_saved_session(
state.user_id,
state.token,
state.device_id,
state.server
) do
request = ws_oauth2_token_request(state.user_id, state.version, token, login_method)
{:send, request, state}
end
end
def handle_auth_ready(%{login_method: :legacy = login_method} = state) do
with {:ok, passport} <-
Soulless.Auth.Legacy.login_with_token(
state.passport_url,
state.user_id,
state.token
) do
request =
ws_oauth2_token_request(
state.user_id,
state.version,
passport["accessToken"],
login_method
)
{:send, request, state}
end
end
def handle_auth_ready(%{login_method: :legacy_session = login_method} = state) do
request =
ws_oauth2_token_request(state.user_id, state.version, state.token, login_method)
{:send, request, state}
end
def handle_auth_ready(%{login_method: :password = login_method} = state) do
request = ws_password_login(state.email, state.password, state.version, login_method)
{:send, request, state}
end
@impl Soulless.Websocket.Implementation
def handle_auth_message(%{body: %Lq.ResOauth2Auth{}} = message, state) do
case is_nil(message.body.error) || message.body.error.code do
true ->
request =
ws_oauth2_login_request(state.version, message.body.access_token, state.login_method)
{:send, request, state}
error_code ->
Logger.error(
"[#{__MODULE__}] Fetching Oauth2 token failed. Received: #{inspect(message)}"
)
{:error, error_code}
end
end
def handle_auth_message(%{body: %Lq.ResLogin{}} = message, state) do
case is_nil(message.body.error) || message.body.error.code do
true ->
state = Map.put(state, :account, message.body.account)
{:done, state}
error_code ->
Logger.error("[#{__MODULE__}] Login failed. Received: #{inspect(message)}")
{:error, error_code}
end
end
@impl Soulless.Websocket.Implementation
def handle_ready(_client, state) do
{:authenticate, state}
end
@impl Soulless.Websocket.Implementation
def handle_disconnect(_client, _reason, state) do
{:reconnect, state}
end
@impl Soulless.Websocket.Implementation
def packet_request_id(decoded) do
decoded.request_id
end
@impl Soulless.Websocket.Implementation
def packet_body(decoded) do
decoded.body
end
@impl Soulless.Websocket.Implementation
def handle_encode(request_id, payload, state) do
try do
requests = Map.get(state, :requests, %{})
{encoded, requests} =
payload
|> Map.replace!(:request_id, request_id)
|> GamePacket.serialize(requests)
{:ok, {:binary, encoded, Map.put(state, :requests, requests)}}
rescue
e ->
{:error, Exception.format(:error, e, __STACKTRACE__)}
end
end
@impl Soulless.Websocket.Implementation
def handle_decode(:binary, data, state) do
requests = Map.get(state, :requests, %{})
with {:ok, {packet, requests}} <- GamePacket.parse(data, requests) do
{:ok, {packet, Map.put(state, :requests, requests)}}
end
end
def handle_decode(_frame_type, data, state) do
{:ok, {data, state}}
end
def ws_password_login(email, password, version, login_method) do
password_hash =
:crypto.mac(:hmac, :sha256, :binary.decode_hex("6C61696C6169"), password)
|> Base.encode16(case: :lower)
# XXX: We shouldn't construct packets manually like this
%Soulless.Packet.GamePacket{
body: %Lq.ReqLogin{
tag: "cn",
client_version_string: "web-#{version}",
type: login_type_for_login_method(login_method),
currency_platforms: [1, 2, 5, 6, 8, 10, 11],
gen_access_token: true,
random_key: Soulless.Util.generate_uuid_v4(),
client_version: %Lq.ClientVersionInfo{
resource: "#{version}.w"
},
device: %Lq.ClientDeviceInfo{
platform: "pc",
hardware: "pc",
is_browser: true,
os: "windows",
os_version: "win11",
sale_platform: "web",
software: "Chrome",
user_agent: Soulless.HTTP.default_user_agent(),
screen_type: 1,
screen_width: 1920,
screen_height: 1080
},
reconnect: false,
password: password_hash,
account: email
},
rpc: ".lq.Lobby.login",
kind: :request
}
end
def ws_oauth2_token_request(uid, version, transient_token, login_method) do
# XXX: We shouldn't construct packets manually like this
%Soulless.Packet.GamePacket{
kind: :request,
rpc: ".lq.Lobby.oauth2Auth",
body: %Lq.ReqOauth2Auth{
type: login_type_for_login_method(login_method),
code: transient_token,
uid: uid,
client_version_string: "web-#{version}"
}
}
end
def ws_oauth2_login_request(version, ws_oauth2_token, login_method) do
# XXX: We shouldn't construct packets manually like this
%Soulless.Packet.GamePacket{
kind: :request,
rpc: ".lq.Lobby.oauth2Login",
body: %Lq.ReqOauth2Login{
type: login_type_for_login_method(login_method),
access_token: ws_oauth2_token,
device: %Lq.ClientDeviceInfo{
platform: "pc",
hardware: "pc",
is_browser: true,
os: "windows",
os_version: "win11",
sale_platform: "web",
software: "Chrome",
user_agent: Soulless.HTTP.default_user_agent(),
screen_type: 1,
screen_width: 1920,
screen_height: 1080
},
random_key: Soulless.Util.generate_uuid_v4(),
currency_platforms: [1, 4, 5, 9, 12],
client_version: %Lq.ClientVersionInfo{
resource: "#{version}.w"
},
client_version_string: "web-#{version}",
gen_access_token: nil,
reconnect: false,
version: 0
}
}
end
def login_type_for_login_method(login_method) do
case login_method do
:password -> 0
:legacy -> 7
:legacy_session -> 7
:yostar -> 22
:yostar_session -> 22
end
end
def required_credentials(login_method) do
case login_method do
:legacy -> [:user_id, :token]
:legacy_session -> [:user_id, :token]
:yostar -> [:email, :token, :device_id]
:yostar_session -> [:user_id, :token, :device_id]
:password -> [:email, :password]
end
end
end