aboutsummaryrefslogtreecommitdiff
path: root/launcher/minecraft/auth/flows/AuthContext.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'launcher/minecraft/auth/flows/AuthContext.cpp')
-rw-r--r--launcher/minecraft/auth/flows/AuthContext.cpp671
1 files changed, 0 insertions, 671 deletions
diff --git a/launcher/minecraft/auth/flows/AuthContext.cpp b/launcher/minecraft/auth/flows/AuthContext.cpp
deleted file mode 100644
index 00957fd4..00000000
--- a/launcher/minecraft/auth/flows/AuthContext.cpp
+++ /dev/null
@@ -1,671 +0,0 @@
-#include <QNetworkAccessManager>
-#include <QNetworkRequest>
-#include <QNetworkReply>
-#include <QDesktopServices>
-#include <QMetaEnum>
-#include <QDebug>
-#include <QJsonDocument>
-#include <QJsonObject>
-#include <QJsonArray>
-#include <QUuid>
-#include <QUrlQuery>
-
-#include "AuthContext.h"
-#include "katabasis/Globals.h"
-#include "AuthRequest.h"
-
-#include "Parsers.h"
-
-#include <Application.h>
-
-using OAuth2 = Katabasis::DeviceFlow;
-using Activity = Katabasis::Activity;
-
-AuthContext::AuthContext(AccountData * data, QObject *parent) :
- AccountTask(data, parent)
-{
-}
-
-void AuthContext::beginActivity(Activity activity) {
- if(isBusy()) {
- throw 0;
- }
- m_activity = activity;
- changeState(STATE_WORKING, "Initializing");
- emit activityChanged(m_activity);
-}
-
-void AuthContext::finishActivity() {
- if(!isBusy()) {
- throw 0;
- }
- m_activity = Katabasis::Activity::Idle;
- setStage(AuthStage::Complete);
- m_data->validity_ = m_data->minecraftProfile.validity;
- emit activityChanged(m_activity);
-}
-
-void AuthContext::initMSA() {
- if(m_oauth2) {
- return;
- }
-
- OAuth2::Options opts;
- opts.scope = "XboxLive.signin offline_access";
- opts.clientIdentifier = APPLICATION->msaClientId();
- opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
- opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
-
- // FIXME: OAuth2 is not aware of our fancy shared pointers
- m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get());
-
- connect(m_oauth2, &OAuth2::activityChanged, this, &AuthContext::onOAuthActivityChanged);
- connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &AuthContext::showVerificationUriAndCode);
-}
-
-void AuthContext::initMojang() {
- if(m_yggdrasil) {
- return;
- }
- m_yggdrasil = new Yggdrasil(m_data, this);
-
- connect(m_yggdrasil, &Task::failed, this, &AuthContext::onMojangFailed);
- connect(m_yggdrasil, &Task::succeeded, this, &AuthContext::onMojangSucceeded);
-}
-
-void AuthContext::onMojangSucceeded() {
- doMinecraftProfile();
-}
-
-
-void AuthContext::onMojangFailed() {
- finishActivity();
- m_error = m_yggdrasil->m_error;
- m_aborted = m_yggdrasil->m_aborted;
- changeState(m_yggdrasil->accountState(), tr("Mojang user authentication failed."));
-}
-
-void AuthContext::onOAuthActivityChanged(Katabasis::Activity activity) {
- switch(activity) {
- case Katabasis::Activity::Idle:
- case Katabasis::Activity::LoggingIn:
- case Katabasis::Activity::Refreshing:
- case Katabasis::Activity::LoggingOut: {
- // We asked it to do something, it's doing it. Nothing to act upon.
- return;
- }
- case Katabasis::Activity::Succeeded: {
- // Succeeded or did not invalidate tokens
- emit hideVerificationUriAndCode();
- if (!m_oauth2->linked()) {
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Microsoft user authentication ended with an impossible state (succeeded, but not succeeded at the same time)."));
- return;
- }
- QVariantMap extraTokens = m_oauth2->extraTokens();
-#ifndef NDEBUG
- if (!extraTokens.isEmpty()) {
- qDebug() << "Extra tokens in response:";
- foreach (QString key, extraTokens.keys()) {
- qDebug() << "\t" << key << ":" << extraTokens.value(key);
- }
- }
-#endif
- doUserAuth();
- return;
- }
- case Katabasis::Activity::FailedSoft: {
- emit hideVerificationUriAndCode();
- finishActivity();
- changeState(STATE_FAILED_SOFT, tr("Microsoft user authentication failed with a soft error."));
- return;
- }
- case Katabasis::Activity::FailedGone:
- case Katabasis::Activity::FailedHard: {
- emit hideVerificationUriAndCode();
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Microsoft user authentication failed."));
- return;
- }
- default: {
- emit hideVerificationUriAndCode();
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result."));
- return;
- }
-
- }
-}
-
-void AuthContext::doUserAuth() {
- setStage(AuthStage::UserAuth);
- changeState(STATE_WORKING, tr("Starting user authentication"));
-
- QString xbox_auth_template = R"XXX(
-{
- "Properties": {
- "AuthMethod": "RPS",
- "SiteName": "user.auth.xboxlive.com",
- "RpsTicket": "d=%1"
- },
- "RelyingParty": "http://auth.xboxlive.com",
- "TokenType": "JWT"
-}
-)XXX";
- auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token);
-
- QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate"));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Accept", "application/json");
- auto *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onUserAuthDone);
- requestor->post(request, xbox_auth_data.toUtf8());
- qDebug() << "First layer of XBox auth ... commencing.";
-}
-
-void AuthContext::onUserAuthDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("XBox user authentication failed."));
- return;
- }
-
- Katabasis::Token temp;
- if(!Parsers::parseXTokenResponse(replyData, temp, "UToken")) {
- qWarning() << "Could not parse user authentication response...";
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("XBox user authentication response could not be understood."));
- return;
- }
- m_data->userToken = temp;
-
- setStage(AuthStage::XboxAuth);
- changeState(STATE_WORKING, tr("Starting XBox authentication"));
-
- doSTSAuthMinecraft();
- doSTSAuthGeneric();
-}
-/*
- url = "https://xsts.auth.xboxlive.com/xsts/authorize"
- headers = {"x-xbl-contract-version": "1"}
- data = {
- "RelyingParty": relying_party,
- "TokenType": "JWT",
- "Properties": {
- "UserTokens": [self.user_token.token],
- "SandboxId": "RETAIL",
- },
- }
-*/
-void AuthContext::doSTSAuthMinecraft() {
- QString xbox_auth_template = R"XXX(
-{
- "Properties": {
- "SandboxId": "RETAIL",
- "UserTokens": [
- "%1"
- ]
- },
- "RelyingParty": "rp://api.minecraftservices.com/",
- "TokenType": "JWT"
-}
-)XXX";
- auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
-
- QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Accept", "application/json");
- AuthRequest *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onSTSAuthMinecraftDone);
- requestor->post(request, xbox_auth_data.toUtf8());
- qDebug() << "Getting Minecraft services STS token...";
-}
-
-void AuthContext::processSTSError(QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
- if(error == QNetworkReply::AuthenticationRequiredError) {
- QJsonParseError jsonError;
- QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
- if(jsonError.error) {
- qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString();
- return;
- }
-
- int64_t errorCode = -1;
- auto obj = doc.object();
- if(!Parsers::getNumber(obj.value("XErr"), errorCode)) {
- qWarning() << "XErr is not a number";
- return;
- }
- stsErrors.insert(errorCode);
- stsFailed = true;
- }
-}
-
-
-void AuthContext::onSTSAuthMinecraftDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
- processSTSError(error, replyData, headers);
- failResult(m_mcAuthSucceeded);
- return;
- }
-
- Katabasis::Token temp;
- if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthMinecraft")) {
- qWarning() << "Could not parse authorization response for access to mojang services...";
- failResult(m_mcAuthSucceeded);
- return;
- }
-
- if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
- qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
- failResult(m_mcAuthSucceeded);
- return;
- }
- m_data->mojangservicesToken = temp;
-
- doMinecraftAuth();
-}
-
-void AuthContext::doMinecraftAuth() {
- auto requestURL = "https://api.minecraftservices.com/launcher/login";
- auto uhs = m_data->mojangservicesToken.extra["uhs"].toString();
- auto xToken = m_data->mojangservicesToken.token;
-
- QString mc_auth_template = R"XXX(
-{
- "xtoken": "XBL3.0 x=%1;%2",
- "platform": "PC_LAUNCHER"
-}
-)XXX";
- auto requestBody = mc_auth_template.arg(uhs, xToken);
-
- QNetworkRequest request = QNetworkRequest(QUrl(requestURL));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Accept", "application/json");
- AuthRequest *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftAuthDone);
- requestor->post(request, requestBody.toUtf8());
- qDebug() << "Getting Minecraft access token...";
-}
-
-void AuthContext::onMinecraftAuthDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
- qDebug() << replyData;
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- failResult(m_mcAuthSucceeded);
- return;
- }
-
- if(!Parsers::parseMojangResponse(replyData, m_data->yggdrasilToken)) {
- qWarning() << "Could not parse login_with_xbox response...";
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- failResult(m_mcAuthSucceeded);
- return;
- }
-
- succeedResult(m_mcAuthSucceeded);
-}
-
-void AuthContext::doSTSAuthGeneric() {
- QString xbox_auth_template = R"XXX(
-{
- "Properties": {
- "SandboxId": "RETAIL",
- "UserTokens": [
- "%1"
- ]
- },
- "RelyingParty": "http://xboxlive.com",
- "TokenType": "JWT"
-}
-)XXX";
- auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token);
-
- QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Accept", "application/json");
- AuthRequest *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onSTSAuthGenericDone);
- requestor->post(request, xbox_auth_data.toUtf8());
- qDebug() << "Getting generic STS token...";
-}
-
-void AuthContext::onSTSAuthGenericDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
- processSTSError(error, replyData, headers);
- failResult(m_xboxProfileSucceeded);
- return;
- }
-
- Katabasis::Token temp;
- if(!Parsers::parseXTokenResponse(replyData, temp, "STSAuthGeneric")) {
- qWarning() << "Could not parse authorization response for access to xbox API...";
- failResult(m_xboxProfileSucceeded);
- return;
- }
-
- if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
- qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
- failResult(m_xboxProfileSucceeded);
- return;
- }
- m_data->xboxApiToken = temp;
-
- doXBoxProfile();
-}
-
-void AuthContext::doXBoxProfile() {
- auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
- QUrlQuery q;
- q.addQueryItem(
- "settings",
- "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
- "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix,"
- "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
- "PreferredColor,Location,Bio,Watermarks,"
- "RealName,RealNameOverride,IsQuarantined"
- );
- url.setQuery(q);
-
- QNetworkRequest request = QNetworkRequest(url);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Accept", "application/json");
- request.setRawHeader("x-xbl-contract-version", "3");
- request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8());
- AuthRequest *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onXBoxProfileDone);
- requestor->get(request);
- qDebug() << "Getting Xbox profile...";
-}
-
-void AuthContext::onXBoxProfileDone(
- QNetworkReply::NetworkError error,
- QByteArray replyData,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
- if (error != QNetworkReply::NoError) {
- qWarning() << "Reply error:" << error;
-#ifndef NDEBUG
- qDebug() << replyData;
-#endif
- failResult(m_xboxProfileSucceeded);
- return;
- }
-
-#ifndef NDEBUG
- qDebug() << "XBox profile: " << replyData;
-#endif
-
- succeedResult(m_xboxProfileSucceeded);
-}
-
-void AuthContext::succeedResult(bool& flag) {
- m_requestsDone ++;
- flag = true;
- checkResult();
-}
-
-void AuthContext::failResult(bool& flag) {
- m_requestsDone ++;
- flag = false;
- checkResult();
-}
-
-void AuthContext::checkResult() {
- qDebug() << "AuthContext::checkResult called";
- if(m_requestsDone != 2) {
- qDebug() << "Number of ready results:" << m_requestsDone;
- return;
- }
- if(m_mcAuthSucceeded && m_xboxProfileSucceeded) {
- doEntitlements();
- }
- else {
- finishActivity();
- if(stsFailed) {
- if(stsErrors.contains(2148916233)) {
- changeState(
- STATE_FAILED_HARD,
- tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.")
- .arg("<a href=\"https://www.minecraft.net/en-us/store/minecraft-java-edition\">minecraft.net</a>")
- );
- }
- else if (stsErrors.contains(2148916235)){
- // NOTE: this is the Grulovia error
- changeState(
- STATE_FAILED_HARD,
- tr("XBox Live is not available in your country. You've been blocked.")
- );
- }
- else if (stsErrors.contains(2148916238)){
- changeState(
- STATE_FAILED_HARD,
- tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.")
- .arg("<a href=\"https://help.minecraft.net/hc/en-us/articles/4403181904525\">help.minecraft.net</a>")
- );
- }
- else {
- QStringList errorList;
- for(auto & error: stsErrors) {
- errorList.append(QString::number(error));
- }
- changeState(
- STATE_FAILED_HARD,
- tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorList.join("\n"))
- );
- }
- }
- else {
- changeState(STATE_FAILED_HARD, tr("XBox and/or Mojang authentication steps did not succeed"));
- }
- }
-}
-
-void AuthContext::doEntitlements() {
- auto uuid = QUuid::createUuid();
- entitlementsRequestId = uuid.toString().remove('{').remove('}');
- auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + entitlementsRequestId;
- QNetworkRequest request = QNetworkRequest(url);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Accept", "application/json");
- request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
- AuthRequest *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onEntitlementsDone);
- requestor->get(request);
- qDebug() << "Getting Xbox profile...";
-}
-
-
-void AuthContext::onEntitlementsDone(
- QNetworkReply::NetworkError error,
- QByteArray data,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
-#ifndef NDEBUG
- qDebug() << data;
-#endif
- // TODO: check presence of same entitlementsRequestId?
- // TODO: validate JWTs?
- Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement);
- doMinecraftProfile();
-}
-
-void AuthContext::doMinecraftProfile() {
- setStage(AuthStage::MinecraftProfile);
- changeState(STATE_WORKING, tr("Starting minecraft profile acquisition"));
-
- auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
- QNetworkRequest request = QNetworkRequest(url);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- // request.setRawHeader("Accept", "application/json");
- request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
-
- AuthRequest *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onMinecraftProfileDone);
- requestor->get(request);
-}
-
-void AuthContext::onMinecraftProfileDone(
- QNetworkReply::NetworkError error,
- QByteArray data,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
-#ifndef NDEBUG
- qDebug() << data;
-#endif
- if (error == QNetworkReply::ContentNotFoundError) {
- // NOTE: Succeed even if we do not have a profile. This is a valid account state.
- if(m_data->type == AccountType::Mojang) {
- m_data->minecraftEntitlement.canPlayMinecraft = false;
- m_data->minecraftEntitlement.ownsMinecraft = false;
- }
- m_data->minecraftProfile = MinecraftProfile();
- succeed();
- return;
- }
- if (error != QNetworkReply::NoError) {
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Minecraft Java profile acquisition failed."));
- return;
- }
- if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
- m_data->minecraftProfile = MinecraftProfile();
- finishActivity();
- changeState(STATE_FAILED_HARD, tr("Minecraft Java profile response could not be parsed"));
- return;
- }
-
- if(m_data->type == AccountType::Mojang) {
- auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
- m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
- m_data->minecraftEntitlement.ownsMinecraft = validProfile;
- doMigrationEligibilityCheck();
- }
- else {
- doGetSkin();
- }
-}
-
-void AuthContext::doMigrationEligibilityCheck() {
- setStage(AuthStage::MigrationEligibility);
- changeState(STATE_WORKING, tr("Starting check for migration eligibility"));
-
- auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration");
- QNetworkRequest request = QNetworkRequest(url);
- request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
- request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
-
- AuthRequest *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onMigrationEligibilityCheckDone);
- requestor->get(request);
-}
-
-void AuthContext::onMigrationEligibilityCheckDone(
- QNetworkReply::NetworkError error,
- QByteArray data,
- QList<QNetworkReply::RawHeaderPair> headers
-) {
- if (error == QNetworkReply::NoError) {
- Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA);
- }
- doGetSkin();
-}
-
-void AuthContext::doGetSkin() {
- setStage(AuthStage::Skin);
- changeState(STATE_WORKING, tr("Fetching player skin"));
-
- auto url = QUrl(m_data->minecraftProfile.skin.url);
- QNetworkRequest request = QNetworkRequest(url);
- AuthRequest *requestor = new AuthRequest(this);
- connect(requestor, &AuthRequest::finished, this, &AuthContext::onSkinDone);
- requestor->get(request);
-}
-
-void AuthContext::onSkinDone(
- QNetworkReply::NetworkError error,
- QByteArray data,
- QList<QNetworkReply::RawHeaderPair>
-) {
- if (error == QNetworkReply::NoError) {
- m_data->minecraftProfile.skin.data = data;
- }
- succeed();
-
-}
-
-void AuthContext::succeed() {
- m_data->validity_ = Katabasis::Validity::Certain;
- finishActivity();
- changeState(STATE_SUCCEEDED, tr("Finished all authentication steps"));
-}
-
-void AuthContext::setStage(AuthContext::AuthStage stage) {
- m_stage = stage;
- emit progress((int)m_stage, (int)AuthStage::Complete);
-}
-
-
-QString AuthContext::getStateMessage() const {
- switch (m_accountState)
- {
- case STATE_WORKING:
- switch(m_stage) {
- case AuthStage::Initial: {
- QString loginMessage = tr("Logging in as %1 user");
- if(m_data->type == AccountType::MSA) {
- return loginMessage.arg("Microsoft");
- }
- else {
- return loginMessage.arg("Mojang");
- }
- }
- case AuthStage::UserAuth:
- return tr("Logging in as XBox user");
- case AuthStage::XboxAuth:
- return tr("Logging in with XBox and Mojang services");
- case AuthStage::MinecraftProfile:
- return tr("Getting Minecraft profile");
- case AuthStage::MigrationEligibility:
- return tr("Checking for migration eligibility");
- case AuthStage::Skin:
- return tr("Getting Minecraft skin");
- case AuthStage::Complete:
- return tr("Finished");
- default:
- break;
- }
- default:
- return AccountTask::getStateMessage();
- }
-}