NOISSUE bulk addition of code from Katabasis
This commit is contained in:
9
api/logic/minecraft/auth-msa/BuildConfig.cpp.in
Normal file
9
api/logic/minecraft/auth-msa/BuildConfig.cpp.in
Normal file
@ -0,0 +1,9 @@
|
||||
#include "BuildConfig.h"
|
||||
#include <QObject>
|
||||
|
||||
const Config BuildConfig;
|
||||
|
||||
Config::Config()
|
||||
{
|
||||
CLIENT_ID = "@MOJANGDEMO_CLIENT_ID@";
|
||||
}
|
11
api/logic/minecraft/auth-msa/BuildConfig.h
Normal file
11
api/logic/minecraft/auth-msa/BuildConfig.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
|
||||
class Config
|
||||
{
|
||||
public:
|
||||
Config();
|
||||
QString CLIENT_ID;
|
||||
};
|
||||
|
||||
extern const Config BuildConfig;
|
28
api/logic/minecraft/auth-msa/CMakeLists.txt
Normal file
28
api/logic/minecraft/auth-msa/CMakeLists.txt
Normal file
@ -0,0 +1,28 @@
|
||||
find_package(Qt5 COMPONENTS Core Gui Network Widgets REQUIRED)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
|
||||
|
||||
|
||||
set(MOJANGDEMO_CLIENT_ID "" CACHE STRING "Client ID used for OAuth2 in mojangdemo")
|
||||
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp")
|
||||
|
||||
set(mojang_SRCS
|
||||
main.cpp
|
||||
context.cpp
|
||||
context.h
|
||||
|
||||
mainwindow.cpp
|
||||
mainwindow.h
|
||||
mainwindow.ui
|
||||
|
||||
${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp
|
||||
BuildConfig.h
|
||||
)
|
||||
|
||||
add_executable( mojangdemo ${mojang_SRCS} )
|
||||
target_link_libraries( mojangdemo Katabasis Qt5::Gui Qt5::Widgets )
|
||||
target_include_directories(mojangdemo PRIVATE logic)
|
938
api/logic/minecraft/auth-msa/context.cpp
Normal file
938
api/logic/minecraft/auth-msa/context.cpp
Normal file
@ -0,0 +1,938 @@
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkReply>
|
||||
#include <QDesktopServices>
|
||||
#include <QMetaEnum>
|
||||
#include <QDebug>
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
|
||||
#include <QUrlQuery>
|
||||
|
||||
#include <QPixmap>
|
||||
#include <QPainter>
|
||||
|
||||
#include "context.h"
|
||||
#include "katabasis/Globals.h"
|
||||
#include "katabasis/StoreQSettings.h"
|
||||
#include "katabasis/Requestor.h"
|
||||
#include "BuildConfig.h"
|
||||
|
||||
using OAuth2 = Katabasis::OAuth2;
|
||||
using Requestor = Katabasis::Requestor;
|
||||
using Activity = Katabasis::Activity;
|
||||
|
||||
Context::Context(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
mgr = new QNetworkAccessManager(this);
|
||||
|
||||
Katabasis::OAuth2::Options opts;
|
||||
opts.scope = "XboxLive.signin offline_access";
|
||||
opts.clientIdentifier = BuildConfig.CLIENT_ID;
|
||||
opts.authorizationUrl = "https://login.live.com/oauth20_authorize.srf";
|
||||
opts.accessTokenUrl = "https://login.live.com/oauth20_token.srf";
|
||||
opts.listenerPorts = {28562, 28563, 28564, 28565, 28566};
|
||||
|
||||
oauth2 = new OAuth2(opts, m_account.msaToken, this, mgr);
|
||||
|
||||
connect(oauth2, &OAuth2::linkingFailed, this, &Context::onLinkingFailed);
|
||||
connect(oauth2, &OAuth2::linkingSucceeded, this, &Context::onLinkingSucceeded);
|
||||
connect(oauth2, &OAuth2::openBrowser, this, &Context::onOpenBrowser);
|
||||
connect(oauth2, &OAuth2::closeBrowser, this, &Context::onCloseBrowser);
|
||||
connect(oauth2, &OAuth2::activityChanged, this, &Context::onOAuthActivityChanged);
|
||||
}
|
||||
|
||||
void Context::beginActivity(Activity activity) {
|
||||
if(isBusy()) {
|
||||
throw 0;
|
||||
}
|
||||
activity_ = activity;
|
||||
emit activityChanged(activity_);
|
||||
}
|
||||
|
||||
void Context::finishActivity() {
|
||||
if(!isBusy()) {
|
||||
throw 0;
|
||||
}
|
||||
activity_ = Katabasis::Activity::Idle;
|
||||
m_account.validity_ = m_account.minecraftProfile.validity;
|
||||
emit activityChanged(activity_);
|
||||
}
|
||||
|
||||
QString Context::gameToken() {
|
||||
return m_account.minecraftToken.token;
|
||||
}
|
||||
|
||||
QString Context::userId() {
|
||||
return m_account.minecraftProfile.id;
|
||||
}
|
||||
|
||||
QString Context::userName() {
|
||||
return m_account.minecraftProfile.name;
|
||||
}
|
||||
|
||||
bool Context::silentSignIn() {
|
||||
if(isBusy()) {
|
||||
return false;
|
||||
}
|
||||
beginActivity(Activity::Refreshing);
|
||||
if(!oauth2->refresh()) {
|
||||
finishActivity();
|
||||
return false;
|
||||
}
|
||||
|
||||
requestsDone = 0;
|
||||
xboxProfileSucceeded = false;
|
||||
mcAuthSucceeded = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Context::signIn() {
|
||||
if(isBusy()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
requestsDone = 0;
|
||||
xboxProfileSucceeded = false;
|
||||
mcAuthSucceeded = false;
|
||||
|
||||
beginActivity(Activity::LoggingIn);
|
||||
oauth2->unlink();
|
||||
m_account = AccountData();
|
||||
oauth2->link();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Context::signOut() {
|
||||
if(isBusy()) {
|
||||
return false;
|
||||
}
|
||||
beginActivity(Activity::LoggingOut);
|
||||
oauth2->unlink();
|
||||
m_account = AccountData();
|
||||
finishActivity();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void Context::onOpenBrowser(const QUrl &url) {
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
void Context::onCloseBrowser() {
|
||||
|
||||
}
|
||||
|
||||
void Context::onLinkingFailed() {
|
||||
finishActivity();
|
||||
}
|
||||
|
||||
void Context::onLinkingSucceeded() {
|
||||
auto *o2t = qobject_cast<OAuth2 *>(sender());
|
||||
if (!o2t->linked()) {
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
QVariantMap extraTokens = o2t->extraTokens();
|
||||
if (!extraTokens.isEmpty()) {
|
||||
qDebug() << "Extra tokens in response:";
|
||||
foreach (QString key, extraTokens.keys()) {
|
||||
qDebug() << "\t" << key << ":" << extraTokens.value(key);
|
||||
}
|
||||
}
|
||||
doUserAuth();
|
||||
}
|
||||
|
||||
void Context::onOAuthActivityChanged(Katabasis::Activity activity) {
|
||||
// respond to activity change here
|
||||
}
|
||||
|
||||
void Context::doUserAuth() {
|
||||
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_account.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 Katabasis::Requestor(mgr, oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &Context::onUserAuthDone);
|
||||
requestor->post(request, xbox_auth_data.toUtf8());
|
||||
qDebug() << "First layer of XBox auth ... commencing.";
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool getDateTime(QJsonValue value, QDateTime & out) {
|
||||
if(!value.isString()) {
|
||||
return false;
|
||||
}
|
||||
out = QDateTime::fromString(value.toString(), Qt::ISODateWithMs);
|
||||
return out.isValid();
|
||||
}
|
||||
|
||||
bool getString(QJsonValue value, QString & out) {
|
||||
if(!value.isString()) {
|
||||
return false;
|
||||
}
|
||||
out = value.toString();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool getNumber(QJsonValue value, double & out) {
|
||||
if(!value.isDouble()) {
|
||||
return false;
|
||||
}
|
||||
out = value.toDouble();
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"IssueInstant":"2020-12-07T19:52:08.4463796Z",
|
||||
"NotAfter":"2020-12-21T19:52:08.4463796Z",
|
||||
"Token":"token",
|
||||
"DisplayClaims":{
|
||||
"xui":[
|
||||
{
|
||||
"uhs":"userhash"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
*/
|
||||
// TODO: handle error responses ...
|
||||
/*
|
||||
{
|
||||
"Identity":"0",
|
||||
"XErr":2148916238,
|
||||
"Message":"",
|
||||
"Redirect":"https://start.ui.xboxlive.com/AddChildToFamily"
|
||||
}
|
||||
// 2148916233 = missing XBox account
|
||||
// 2148916238 = child account not linked to a family
|
||||
*/
|
||||
|
||||
bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output) {
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
|
||||
if(jsonError.error) {
|
||||
qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto obj = doc.object();
|
||||
if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) {
|
||||
qWarning() << "User IssueInstant is not a timestamp";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
if(!getDateTime(obj.value("NotAfter"), output.notAfter)) {
|
||||
qWarning() << "User NotAfter is not a timestamp";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
if(!getString(obj.value("Token"), output.token)) {
|
||||
qWarning() << "User Token is not a timestamp";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
auto arrayVal = obj.value("DisplayClaims").toObject().value("xui");
|
||||
if(!arrayVal.isArray()) {
|
||||
qWarning() << "Missing xui claims array";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
bool foundUHS = false;
|
||||
for(auto item: arrayVal.toArray()) {
|
||||
if(!item.isObject()) {
|
||||
continue;
|
||||
}
|
||||
auto obj = item.toObject();
|
||||
if(obj.contains("uhs")) {
|
||||
foundUHS = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
// consume all 'display claims' ... whatever that means
|
||||
for(auto iter = obj.begin(); iter != obj.end(); iter++) {
|
||||
QString claim;
|
||||
if(!getString(obj.value(iter.key()), claim)) {
|
||||
qWarning() << "display claim " << iter.key() << " is not a string...";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
output.extra[iter.key()] = claim;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
if(!foundUHS) {
|
||||
qWarning() << "Missing uhs";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
output.validity = Katabasis::Validity::Certain;
|
||||
qDebug() << data;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Context::onUserAuthDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
Katabasis::Token temp;
|
||||
if(!parseXTokenResponse(replyData, temp)) {
|
||||
qWarning() << "Could not parse user authentication response...";
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
m_account.userToken = temp;
|
||||
|
||||
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 Context::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_account.userToken.token);
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &Context::onSTSAuthMinecraftDone);
|
||||
requestor->post(request, xbox_auth_data.toUtf8());
|
||||
qDebug() << "Second layer of XBox auth ... commencing.";
|
||||
}
|
||||
|
||||
void Context::onSTSAuthMinecraftDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
Katabasis::Token temp;
|
||||
if(!parseXTokenResponse(replyData, temp)) {
|
||||
qWarning() << "Could not parse authorization response for access to mojang services...";
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
|
||||
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
||||
qDebug() << replyData;
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
m_account.mojangservicesToken = temp;
|
||||
|
||||
doMinecraftAuth();
|
||||
}
|
||||
|
||||
void Context::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_account.userToken.token);
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &Context::onSTSAuthGenericDone);
|
||||
requestor->post(request, xbox_auth_data.toUtf8());
|
||||
qDebug() << "Second layer of XBox auth ... commencing.";
|
||||
}
|
||||
|
||||
void Context::onSTSAuthGenericDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
Katabasis::Token temp;
|
||||
if(!parseXTokenResponse(replyData, temp)) {
|
||||
qWarning() << "Could not parse authorization response for access to xbox API...";
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
if(temp.extra["uhs"] != m_account.userToken.extra["uhs"]) {
|
||||
qWarning() << "Server has changed user hash in the reply... something is wrong. ABORTING";
|
||||
qDebug() << replyData;
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
m_account.xboxApiToken = temp;
|
||||
|
||||
doXBoxProfile();
|
||||
}
|
||||
|
||||
|
||||
void Context::doMinecraftAuth() {
|
||||
QString mc_auth_template = R"XXX(
|
||||
{
|
||||
"identityToken": "XBL3.0 x=%1;%2"
|
||||
}
|
||||
)XXX";
|
||||
auto data = mc_auth_template.arg(m_account.mojangservicesToken.extra["uhs"].toString(), m_account.mojangservicesToken.token);
|
||||
|
||||
QNetworkRequest request = QNetworkRequest(QUrl("https://api.minecraftservices.com/authentication/login_with_xbox"));
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
request.setRawHeader("Accept", "application/json");
|
||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &Context::onMinecraftAuthDone);
|
||||
requestor->post(request, data.toUtf8());
|
||||
qDebug() << "Getting Minecraft access token...";
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) {
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
|
||||
if(jsonError.error) {
|
||||
qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto obj = doc.object();
|
||||
double expires_in = 0;
|
||||
if(!getNumber(obj.value("expires_in"), expires_in)) {
|
||||
qWarning() << "expires_in is not a valid number";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
auto currentTime = QDateTime::currentDateTimeUtc();
|
||||
output.issueInstant = currentTime;
|
||||
output.notAfter = currentTime.addSecs(expires_in);
|
||||
|
||||
QString username;
|
||||
if(!getString(obj.value("username"), username)) {
|
||||
qWarning() << "username is not valid";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: it's a JWT... validate it?
|
||||
if(!getString(obj.value("access_token"), output.token)) {
|
||||
qWarning() << "access_token is not valid";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
output.validity = Katabasis::Validity::Certain;
|
||||
qDebug() << data;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void Context::onMinecraftAuthDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
requestsDone++;
|
||||
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
qDebug() << replyData;
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
if(!parseMojangResponse(replyData, m_account.minecraftToken)) {
|
||||
qWarning() << "Could not parse login_with_xbox response...";
|
||||
qDebug() << replyData;
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
mcAuthSucceeded = true;
|
||||
|
||||
checkResult();
|
||||
}
|
||||
|
||||
void Context::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_account.userToken.extra["uhs"].toString(), m_account.xboxApiToken.token).toUtf8());
|
||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &Context::onXBoxProfileDone);
|
||||
requestor->get(request);
|
||||
qDebug() << "Getting Xbox profile...";
|
||||
}
|
||||
|
||||
void Context::onXBoxProfileDone(
|
||||
int requestId,
|
||||
QNetworkReply::NetworkError error,
|
||||
QByteArray replyData,
|
||||
QList<QNetworkReply::RawHeaderPair> headers
|
||||
) {
|
||||
requestsDone ++;
|
||||
|
||||
if (error != QNetworkReply::NoError) {
|
||||
qWarning() << "Reply error:" << error;
|
||||
qDebug() << replyData;
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "XBox profile: " << replyData;
|
||||
|
||||
xboxProfileSucceeded = true;
|
||||
checkResult();
|
||||
}
|
||||
|
||||
void Context::checkResult() {
|
||||
if(requestsDone != 2) {
|
||||
return;
|
||||
}
|
||||
if(mcAuthSucceeded && xboxProfileSucceeded) {
|
||||
doMinecraftProfile();
|
||||
}
|
||||
else {
|
||||
finishActivity();
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
|
||||
if(jsonError.error) {
|
||||
qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString();
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto obj = doc.object();
|
||||
if(!getString(obj.value("id"), output.id)) {
|
||||
qWarning() << "minecraft profile id is not a string";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!getString(obj.value("name"), output.name)) {
|
||||
qWarning() << "minecraft profile name is not a string";
|
||||
qDebug() << data;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto skinsArray = obj.value("skins").toArray();
|
||||
for(auto skin: skinsArray) {
|
||||
auto skinObj = skin.toObject();
|
||||
Skin skinOut;
|
||||
if(!getString(skinObj.value("id"), skinOut.id)) {
|
||||
continue;
|
||||
}
|
||||
QString state;
|
||||
if(!getString(skinObj.value("state"), state)) {
|
||||
continue;
|
||||
}
|
||||
if(state != "ACTIVE") {
|
||||
continue;
|
||||
}
|
||||
if(!getString(skinObj.value("url"), skinOut.url)) {
|
||||
continue;
|
||||
}
|
||||
if(!getString(skinObj.value("variant"), skinOut.variant)) {
|
||||
continue;
|
||||
}
|
||||
// we deal with only the active skin
|
||||
output.skin = skinOut;
|
||||
break;
|
||||
}
|
||||
auto capesArray = obj.value("capes").toArray();
|
||||
int i = -1;
|
||||
int currentCape = -1;
|
||||
for(auto cape: capesArray) {
|
||||
i++;
|
||||
auto capeObj = cape.toObject();
|
||||
Cape capeOut;
|
||||
if(!getString(capeObj.value("id"), capeOut.id)) {
|
||||
continue;
|
||||
}
|
||||
QString state;
|
||||
if(!getString(capeObj.value("state"), state)) {
|
||||
continue;
|
||||
}
|
||||
if(state == "ACTIVE") {
|
||||
currentCape = i;
|
||||
}
|
||||
if(!getString(capeObj.value("url"), capeOut.url)) {
|
||||
continue;
|
||||
}
|
||||
if(!getString(capeObj.value("alias"), capeOut.alias)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we deal with only the active skin
|
||||
output.capes.push_back(capeOut);
|
||||
}
|
||||
output.currentCape = currentCape;
|
||||
output.validity = Katabasis::Validity::Certain;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void Context::doMinecraftProfile() {
|
||||
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_account.minecraftToken.token).toUtf8());
|
||||
|
||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
|
||||
connect(requestor, &Requestor::finished, this, &Context::onMinecraftProfileDone);
|
||||
requestor->get(request);
|
||||
}
|
||||
|
||||
void Context::onMinecraftProfileDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair> headers) {
|
||||
qDebug() << data;
|
||||
if (error == QNetworkReply::ContentNotFoundError) {
|
||||
m_account.minecraftProfile = MinecraftProfile();
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
if (error != QNetworkReply::NoError) {
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
if(!parseMinecraftProfile(data, m_account.minecraftProfile)) {
|
||||
m_account.minecraftProfile = MinecraftProfile();
|
||||
finishActivity();
|
||||
return;
|
||||
}
|
||||
doGetSkin();
|
||||
}
|
||||
|
||||
void Context::doGetSkin() {
|
||||
auto url = QUrl(m_account.minecraftProfile.skin.url);
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
Requestor *requestor = new Requestor(mgr, oauth2, this);
|
||||
requestor->setAddAccessTokenInQuery(false);
|
||||
connect(requestor, &Requestor::finished, this, &Context::onSkinDone);
|
||||
requestor->get(request);
|
||||
}
|
||||
|
||||
void Context::onSkinDone(int, QNetworkReply::NetworkError error, QByteArray data, QList<QNetworkReply::RawHeaderPair>) {
|
||||
if (error == QNetworkReply::NoError) {
|
||||
m_account.minecraftProfile.skin.data = data;
|
||||
}
|
||||
finishActivity();
|
||||
}
|
||||
|
||||
namespace {
|
||||
void tokenToJSON(QJsonObject &parent, Katabasis::Token t, const char * tokenName) {
|
||||
if(t.validity == Katabasis::Validity::None || !t.persistent) {
|
||||
return;
|
||||
}
|
||||
QJsonObject out;
|
||||
if(t.issueInstant.isValid()) {
|
||||
out["iat"] = QJsonValue(t.issueInstant.toSecsSinceEpoch());
|
||||
}
|
||||
|
||||
if(t.notAfter.isValid()) {
|
||||
out["exp"] = QJsonValue(t.notAfter.toSecsSinceEpoch());
|
||||
}
|
||||
|
||||
if(!t.token.isEmpty()) {
|
||||
out["token"] = QJsonValue(t.token);
|
||||
}
|
||||
if(!t.refresh_token.isEmpty()) {
|
||||
out["refresh_token"] = QJsonValue(t.refresh_token);
|
||||
}
|
||||
if(t.extra.size()) {
|
||||
out["extra"] = QJsonObject::fromVariantMap(t.extra);
|
||||
}
|
||||
if(out.size()) {
|
||||
parent[tokenName] = out;
|
||||
}
|
||||
}
|
||||
|
||||
Katabasis::Token tokenFromJSON(const QJsonObject &parent, const char * tokenName) {
|
||||
Katabasis::Token out;
|
||||
auto tokenObject = parent.value(tokenName).toObject();
|
||||
if(tokenObject.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
auto issueInstant = tokenObject.value("iat");
|
||||
if(issueInstant.isDouble()) {
|
||||
out.issueInstant = QDateTime::fromSecsSinceEpoch((int64_t) issueInstant.toDouble());
|
||||
}
|
||||
|
||||
auto notAfter = tokenObject.value("exp");
|
||||
if(notAfter.isDouble()) {
|
||||
out.notAfter = QDateTime::fromSecsSinceEpoch((int64_t) notAfter.toDouble());
|
||||
}
|
||||
|
||||
auto token = tokenObject.value("token");
|
||||
if(token.isString()) {
|
||||
out.token = token.toString();
|
||||
out.validity = Katabasis::Validity::Assumed;
|
||||
}
|
||||
|
||||
auto refresh_token = tokenObject.value("refresh_token");
|
||||
if(refresh_token.isString()) {
|
||||
out.refresh_token = refresh_token.toString();
|
||||
}
|
||||
|
||||
auto extra = tokenObject.value("extra");
|
||||
if(extra.isObject()) {
|
||||
out.extra = extra.toObject().toVariantMap();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void profileToJSON(QJsonObject &parent, MinecraftProfile p, const char * tokenName) {
|
||||
if(p.id.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QJsonObject out;
|
||||
out["id"] = QJsonValue(p.id);
|
||||
out["name"] = QJsonValue(p.name);
|
||||
if(p.currentCape != -1) {
|
||||
out["cape"] = p.capes[p.currentCape].id;
|
||||
}
|
||||
|
||||
{
|
||||
QJsonObject skinObj;
|
||||
skinObj["id"] = p.skin.id;
|
||||
skinObj["url"] = p.skin.url;
|
||||
skinObj["variant"] = p.skin.variant;
|
||||
if(p.skin.data.size()) {
|
||||
skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64());
|
||||
}
|
||||
out["skin"] = skinObj;
|
||||
}
|
||||
|
||||
QJsonArray capesArray;
|
||||
for(auto & cape: p.capes) {
|
||||
QJsonObject capeObj;
|
||||
capeObj["id"] = cape.id;
|
||||
capeObj["url"] = cape.url;
|
||||
capeObj["alias"] = cape.alias;
|
||||
if(cape.data.size()) {
|
||||
capeObj["data"] = QString::fromLatin1(cape.data.toBase64());
|
||||
}
|
||||
capesArray.push_back(capeObj);
|
||||
}
|
||||
out["capes"] = capesArray;
|
||||
parent[tokenName] = out;
|
||||
}
|
||||
|
||||
MinecraftProfile profileFromJSON(const QJsonObject &parent, const char * tokenName) {
|
||||
MinecraftProfile out;
|
||||
auto tokenObject = parent.value(tokenName).toObject();
|
||||
if(tokenObject.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
{
|
||||
auto idV = tokenObject.value("id");
|
||||
auto nameV = tokenObject.value("name");
|
||||
if(!idV.isString() || !nameV.isString()) {
|
||||
qWarning() << "mandatory profile attributes are missing or of unexpected type";
|
||||
return MinecraftProfile();
|
||||
}
|
||||
out.name = nameV.toString();
|
||||
out.id = idV.toString();
|
||||
}
|
||||
|
||||
{
|
||||
auto skinV = tokenObject.value("skin");
|
||||
if(!skinV.isObject()) {
|
||||
qWarning() << "skin is missing";
|
||||
return MinecraftProfile();
|
||||
}
|
||||
auto skinObj = skinV.toObject();
|
||||
auto idV = skinObj.value("id");
|
||||
auto urlV = skinObj.value("url");
|
||||
auto variantV = skinObj.value("variant");
|
||||
if(!idV.isString() || !urlV.isString() || !variantV.isString()) {
|
||||
qWarning() << "mandatory skin attributes are missing or of unexpected type";
|
||||
return MinecraftProfile();
|
||||
}
|
||||
out.skin.id = idV.toString();
|
||||
out.skin.url = urlV.toString();
|
||||
out.skin.variant = variantV.toString();
|
||||
|
||||
// data for skin is optional
|
||||
auto dataV = skinObj.value("data");
|
||||
if(dataV.isString()) {
|
||||
// TODO: validate base64
|
||||
out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1());
|
||||
}
|
||||
else if (!dataV.isUndefined()) {
|
||||
qWarning() << "skin data is something unexpected";
|
||||
return MinecraftProfile();
|
||||
}
|
||||
}
|
||||
|
||||
auto capesV = tokenObject.value("capes");
|
||||
if(!capesV.isArray()) {
|
||||
qWarning() << "capes is not an array!";
|
||||
return MinecraftProfile();
|
||||
}
|
||||
auto capesArray = capesV.toArray();
|
||||
for(auto capeV: capesArray) {
|
||||
if(!capeV.isObject()) {
|
||||
qWarning() << "cape is not an object!";
|
||||
return MinecraftProfile();
|
||||
}
|
||||
auto capeObj = capeV.toObject();
|
||||
auto idV = capeObj.value("id");
|
||||
auto urlV = capeObj.value("url");
|
||||
auto aliasV = capeObj.value("alias");
|
||||
if(!idV.isString() || !urlV.isString() || !aliasV.isString()) {
|
||||
qWarning() << "mandatory skin attributes are missing or of unexpected type";
|
||||
return MinecraftProfile();
|
||||
}
|
||||
Cape cape;
|
||||
cape.id = idV.toString();
|
||||
cape.url = urlV.toString();
|
||||
cape.alias = aliasV.toString();
|
||||
|
||||
// data for cape is optional.
|
||||
auto dataV = capeObj.value("data");
|
||||
if(dataV.isString()) {
|
||||
// TODO: validate base64
|
||||
cape.data = QByteArray::fromBase64(dataV.toString().toLatin1());
|
||||
}
|
||||
else if (!dataV.isUndefined()) {
|
||||
qWarning() << "cape data is something unexpected";
|
||||
return MinecraftProfile();
|
||||
}
|
||||
out.capes.push_back(cape);
|
||||
}
|
||||
out.validity = Katabasis::Validity::Assumed;
|
||||
return out;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool Context::resumeFromState(QByteArray data) {
|
||||
QJsonParseError error;
|
||||
auto doc = QJsonDocument::fromJson(data, &error);
|
||||
if(error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "Failed to parse account data as JSON.";
|
||||
return false;
|
||||
}
|
||||
auto docObject = doc.object();
|
||||
m_account.msaToken = tokenFromJSON(docObject, "msa");
|
||||
m_account.userToken = tokenFromJSON(docObject, "utoken");
|
||||
m_account.xboxApiToken = tokenFromJSON(docObject, "xrp-main");
|
||||
m_account.mojangservicesToken = tokenFromJSON(docObject, "xrp-mc");
|
||||
m_account.minecraftToken = tokenFromJSON(docObject, "ygg");
|
||||
|
||||
m_account.minecraftProfile = profileFromJSON(docObject, "profile");
|
||||
|
||||
m_account.validity_ = m_account.minecraftProfile.validity;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray Context::saveState() {
|
||||
QJsonDocument doc;
|
||||
QJsonObject output;
|
||||
tokenToJSON(output, m_account.msaToken, "msa");
|
||||
tokenToJSON(output, m_account.userToken, "utoken");
|
||||
tokenToJSON(output, m_account.xboxApiToken, "xrp-main");
|
||||
tokenToJSON(output, m_account.mojangservicesToken, "xrp-mc");
|
||||
tokenToJSON(output, m_account.minecraftToken, "ygg");
|
||||
profileToJSON(output, m_account.minecraftProfile, "profile");
|
||||
doc.setObject(output);
|
||||
return doc.toJson(QJsonDocument::Indented);
|
||||
}
|
128
api/logic/minecraft/auth-msa/context.h
Normal file
128
api/logic/minecraft/auth-msa/context.h
Normal file
@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QList>
|
||||
#include <QVector>
|
||||
#include <QNetworkReply>
|
||||
#include <QImage>
|
||||
|
||||
#include <katabasis/OAuth2.h>
|
||||
|
||||
struct Skin {
|
||||
QString id;
|
||||
QString url;
|
||||
QString variant;
|
||||
|
||||
QByteArray data;
|
||||
};
|
||||
|
||||
struct Cape {
|
||||
QString id;
|
||||
QString url;
|
||||
QString alias;
|
||||
|
||||
QByteArray data;
|
||||
};
|
||||
|
||||
struct MinecraftProfile {
|
||||
QString id;
|
||||
QString name;
|
||||
Skin skin;
|
||||
int currentCape = -1;
|
||||
QVector<Cape> capes;
|
||||
Katabasis::Validity validity = Katabasis::Validity::None;
|
||||
};
|
||||
|
||||
enum class AccountType {
|
||||
MSA,
|
||||
Mojang
|
||||
};
|
||||
|
||||
struct AccountData {
|
||||
AccountType type = AccountType::MSA;
|
||||
|
||||
Katabasis::Token msaToken;
|
||||
Katabasis::Token userToken;
|
||||
Katabasis::Token xboxApiToken;
|
||||
Katabasis::Token mojangservicesToken;
|
||||
Katabasis::Token minecraftToken;
|
||||
|
||||
MinecraftProfile minecraftProfile;
|
||||
Katabasis::Validity validity_ = Katabasis::Validity::None;
|
||||
};
|
||||
|
||||
class Context : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Context(QObject *parent = 0);
|
||||
|
||||
QByteArray saveState();
|
||||
bool resumeFromState(QByteArray data);
|
||||
|
||||
bool isBusy() {
|
||||
return activity_ != Katabasis::Activity::Idle;
|
||||
};
|
||||
Katabasis::Validity validity() {
|
||||
return m_account.validity_;
|
||||
};
|
||||
|
||||
bool signIn();
|
||||
bool silentSignIn();
|
||||
bool signOut();
|
||||
|
||||
QString userName();
|
||||
QString userId();
|
||||
QString gameToken();
|
||||
signals:
|
||||
void succeeded();
|
||||
void failed();
|
||||
void activityChanged(Katabasis::Activity activity);
|
||||
|
||||
private slots:
|
||||
void onLinkingSucceeded();
|
||||
void onLinkingFailed();
|
||||
void onOpenBrowser(const QUrl &url);
|
||||
void onCloseBrowser();
|
||||
void onOAuthActivityChanged(Katabasis::Activity activity);
|
||||
|
||||
private:
|
||||
void doUserAuth();
|
||||
Q_SLOT void onUserAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void doSTSAuthMinecraft();
|
||||
Q_SLOT void onSTSAuthMinecraftDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void doMinecraftAuth();
|
||||
Q_SLOT void onMinecraftAuthDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void doSTSAuthGeneric();
|
||||
Q_SLOT void onSTSAuthGenericDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
void doXBoxProfile();
|
||||
Q_SLOT void onXBoxProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void doMinecraftProfile();
|
||||
Q_SLOT void onMinecraftProfileDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void doGetSkin();
|
||||
Q_SLOT void onSkinDone(int, QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
|
||||
|
||||
void checkResult();
|
||||
|
||||
private:
|
||||
void beginActivity(Katabasis::Activity activity);
|
||||
void finishActivity();
|
||||
void clearTokens();
|
||||
|
||||
private:
|
||||
Katabasis::OAuth2 *oauth2 = nullptr;
|
||||
|
||||
int requestsDone = 0;
|
||||
bool xboxProfileSucceeded = false;
|
||||
bool mcAuthSucceeded = false;
|
||||
Katabasis::Activity activity_ = Katabasis::Activity::Idle;
|
||||
|
||||
AccountData m_account;
|
||||
|
||||
QNetworkAccessManager *mgr = nullptr;
|
||||
};
|
100
api/logic/minecraft/auth-msa/main.cpp
Normal file
100
api/logic/minecraft/auth-msa/main.cpp
Normal file
@ -0,0 +1,100 @@
|
||||
#include <QApplication>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QSaveFile>
|
||||
|
||||
#include "context.h"
|
||||
#include "mainwindow.h"
|
||||
|
||||
void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
QByteArray localMsg = msg.toLocal8Bit();
|
||||
const char *file = context.file ? context.file : "";
|
||||
const char *function = context.function ? context.function : "";
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
class Helper : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Helper(Context * context) : QObject(), context_(context), msg_(QString()) {
|
||||
QFile tokenCache("usercache.dat");
|
||||
if(tokenCache.open(QIODevice::ReadOnly)) {
|
||||
context_->resumeFromState(tokenCache.readAll());
|
||||
}
|
||||
}
|
||||
|
||||
public slots:
|
||||
void run() {
|
||||
connect(context_, &Context::activityChanged, this, &Helper::onActivityChanged);
|
||||
context_->silentSignIn();
|
||||
}
|
||||
|
||||
void onFailed() {
|
||||
qDebug() << "Login failed";
|
||||
}
|
||||
|
||||
void onActivityChanged(Katabasis::Activity activity) {
|
||||
if(activity == Katabasis::Activity::Idle) {
|
||||
switch(context_->validity()) {
|
||||
case Katabasis::Validity::None: {
|
||||
// account is gone, remove it.
|
||||
QFile::remove("usercache.dat");
|
||||
}
|
||||
break;
|
||||
case Katabasis::Validity::Assumed: {
|
||||
// this is basically a soft-failed refresh. do nothing.
|
||||
}
|
||||
break;
|
||||
case Katabasis::Validity::Certain: {
|
||||
// stuff got refreshed / signed in. Save.
|
||||
auto data = context_->saveState();
|
||||
QSaveFile tokenCache("usercache.dat");
|
||||
if(tokenCache.open(QIODevice::WriteOnly)) {
|
||||
tokenCache.write(context_->saveState());
|
||||
tokenCache.commit();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Context *context_;
|
||||
QString msg_;
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
qInstallMessageHandler(myMessageOutput);
|
||||
QApplication a(argc, argv);
|
||||
QCoreApplication::setOrganizationName("MultiMC");
|
||||
QCoreApplication::setApplicationName("MultiMC");
|
||||
Context c;
|
||||
Helper helper(&c);
|
||||
MainWindow window(&c);
|
||||
window.show();
|
||||
QTimer::singleShot(0, &helper, &Helper::run);
|
||||
return a.exec();
|
||||
}
|
||||
|
||||
#include "main.moc"
|
97
api/logic/minecraft/auth-msa/mainwindow.cpp
Normal file
97
api/logic/minecraft/auth-msa/mainwindow.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
#include "mainwindow.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include <QDebug>
|
||||
|
||||
#include <QDesktopServices>
|
||||
|
||||
#include "BuildConfig.h"
|
||||
|
||||
MainWindow::MainWindow(Context * context, QWidget *parent) :
|
||||
QMainWindow(parent),
|
||||
m_context(context),
|
||||
m_ui(new Ui::MainWindow)
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
connect(m_ui->signInButton_MSA, &QPushButton::clicked, this, &MainWindow::SignInMSAClicked);
|
||||
connect(m_ui->signInButton_Mojang, &QPushButton::clicked, this, &MainWindow::SignInMojangClicked);
|
||||
connect(m_ui->signOutButton, &QPushButton::clicked, this, &MainWindow::SignOutClicked);
|
||||
connect(m_ui->refreshButton, &QPushButton::clicked, this, &MainWindow::RefreshClicked);
|
||||
|
||||
// connect(m_context, &Context::linkingSucceeded, this, &MainWindow::SignInSucceeded);
|
||||
// connect(m_context, &Context::linkingFailed, this, &MainWindow::SignInFailed);
|
||||
connect(m_context, &Context::activityChanged, this, &MainWindow::ActivityChanged);
|
||||
ActivityChanged(Katabasis::Activity::Idle);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
|
||||
void MainWindow::ActivityChanged(Katabasis::Activity activity) {
|
||||
switch(activity) {
|
||||
case Katabasis::Activity::Idle: {
|
||||
if(m_context->validity() != Katabasis::Validity::None) {
|
||||
m_ui->signInButton_Mojang->setEnabled(false);
|
||||
m_ui->signInButton_MSA->setEnabled(false);
|
||||
m_ui->signOutButton->setEnabled(true);
|
||||
m_ui->refreshButton->setEnabled(true);
|
||||
m_ui->statusBar->showMessage(QString("Hello %1!").arg(m_context->userName()));
|
||||
}
|
||||
else {
|
||||
m_ui->signInButton_Mojang->setEnabled(true);
|
||||
m_ui->signInButton_MSA->setEnabled(true);
|
||||
m_ui->signOutButton->setEnabled(false);
|
||||
m_ui->refreshButton->setEnabled(false);
|
||||
m_ui->statusBar->showMessage("Press the login button to start.");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Katabasis::Activity::LoggingIn: {
|
||||
m_ui->signInButton_Mojang->setEnabled(false);
|
||||
m_ui->signInButton_MSA->setEnabled(false);
|
||||
m_ui->signOutButton->setEnabled(false);
|
||||
m_ui->refreshButton->setEnabled(false);
|
||||
m_ui->statusBar->showMessage("Logging in...");
|
||||
}
|
||||
break;
|
||||
case Katabasis::Activity::LoggingOut: {
|
||||
m_ui->signInButton_Mojang->setEnabled(false);
|
||||
m_ui->signInButton_MSA->setEnabled(false);
|
||||
m_ui->signOutButton->setEnabled(false);
|
||||
m_ui->refreshButton->setEnabled(false);
|
||||
m_ui->statusBar->showMessage("Logging out...");
|
||||
}
|
||||
break;
|
||||
case Katabasis::Activity::Refreshing: {
|
||||
m_ui->signInButton_Mojang->setEnabled(false);
|
||||
m_ui->signInButton_MSA->setEnabled(false);
|
||||
m_ui->signOutButton->setEnabled(false);
|
||||
m_ui->refreshButton->setEnabled(false);
|
||||
m_ui->statusBar->showMessage("Refreshing login...");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::SignInMSAClicked() {
|
||||
qDebug() << "Sign In MSA";
|
||||
// signIn({{"prompt", "select_account"}})
|
||||
// FIXME: wrong. very wrong. this should not be operating on the current context
|
||||
m_context->signIn();
|
||||
}
|
||||
|
||||
void MainWindow::SignInMojangClicked() {
|
||||
qDebug() << "Sign In Mojang";
|
||||
// signIn({{"prompt", "select_account"}})
|
||||
// FIXME: wrong. very wrong. this should not be operating on the current context
|
||||
m_context->signIn();
|
||||
}
|
||||
|
||||
|
||||
void MainWindow::SignOutClicked() {
|
||||
qDebug() << "Sign Out";
|
||||
m_context->signOut();
|
||||
}
|
||||
|
||||
void MainWindow::RefreshClicked() {
|
||||
qDebug() << "Refresh";
|
||||
m_context->silentSignIn();
|
||||
}
|
34
api/logic/minecraft/auth-msa/mainwindow.h
Normal file
34
api/logic/minecraft/auth-msa/mainwindow.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QScopedPointer>
|
||||
#include <QtNetwork>
|
||||
#include <katabasis/Bits.h>
|
||||
|
||||
#include "context.h"
|
||||
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(Context * context, QWidget *parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
private slots:
|
||||
void SignInMojangClicked();
|
||||
void SignInMSAClicked();
|
||||
|
||||
void SignOutClicked();
|
||||
void RefreshClicked();
|
||||
|
||||
void ActivityChanged(Katabasis::Activity activity);
|
||||
|
||||
private:
|
||||
Context* m_context;
|
||||
QScopedPointer<Ui::MainWindow> m_ui;
|
||||
};
|
||||
|
72
api/logic/minecraft/auth-msa/mainwindow.ui
Normal file
72
api/logic/minecraft/auth-msa/mainwindow.ui
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1037</width>
|
||||
<height>511</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>SmartMapsClient</string>
|
||||
</property>
|
||||
<property name="dockNestingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralWidget">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="3">
|
||||
<widget class="QPushButton" name="signInButton_Mojang">
|
||||
<property name="text">
|
||||
<string>SignIn Mojang</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="3">
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" rowspan="7" colspan="3">
|
||||
<widget class="QTreeView" name="accountView"/>
|
||||
</item>
|
||||
<item row="5" column="3">
|
||||
<widget class="QPushButton" name="refreshButton">
|
||||
<property name="text">
|
||||
<string>Refresh</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QPushButton" name="signInButton_MSA">
|
||||
<property name="text">
|
||||
<string>SignIn MSA</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="3">
|
||||
<widget class="QPushButton" name="signOutButton">
|
||||
<property name="text">
|
||||
<string>SignOut</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="3">
|
||||
<widget class="QPushButton" name="makeActiveButton">
|
||||
<property name="text">
|
||||
<string>Make Active</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusBar"/>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
Reference in New Issue
Block a user