Prevent almost all HTTP requests from blocking

The ones that remain blocking are the ones that run on different threads; see Task, yet another big mess to clean up.
This commit is contained in:
Tamás Bálint Misius 2023-06-10 12:28:30 +02:00
parent c2f8a7df25
commit c73fa1bcdd
No known key found for this signature in database
GPG Key ID: 5B472A12F6ECA9F2
105 changed files with 2196 additions and 1513 deletions

View File

@ -18,6 +18,12 @@ inline std::pair<Signed, Signed> floorDiv(Signed a, Signed b)
return { quo, rem };
}
template<class Signed>
inline std::pair<Signed, Signed> ceilDiv(Signed a, Signed b)
{
return floorDiv(a + b - Signed(1), b);
}
//Linear interpolation
template <typename T> inline T LinearInterpolate(T val1, T val2, T lowerCoord, T upperCoord, T coord)
{

View File

@ -7,6 +7,8 @@
#include "client/SaveFile.h"
#include "client/SaveInfo.h"
#include "client/http/requestmanager/RequestManager.h"
#include "client/http/GetSaveRequest.h"
#include "client/http/GetSaveDataRequest.h"
#include "common/platform/Platform.h"
#include "graphics/Graphics.h"
#include "simulation/SaveRenderer.h"
@ -462,15 +464,31 @@ int main(int argc, char * argv[])
}
int saveId = saveIdPart.ToNumber<int>();
auto newSave = Client::Ref().GetSave(saveId, 0);
if (!newSave)
throw std::runtime_error("Could not load save info");
auto saveData = Client::Ref().GetSaveData(saveId, 0);
if (!saveData.size())
throw std::runtime_error(("Could not load save\n" + Client::Ref().GetLastError()).ToUtf8());
auto newGameSave = std::make_unique<GameSave>(std::move(saveData));
newSave->SetGameSave(std::move(newGameSave));
auto getSave = std::make_unique<http::GetSaveRequest>(saveId, 0);
getSave->Start();
getSave->Wait();
std::unique_ptr<SaveInfo> newSave;
try
{
newSave = getSave->Finish();
}
catch (const http::RequestError &ex)
{
throw std::runtime_error("Could not load save info\n" + ByteString(ex.what()));
}
auto getSaveData = std::make_unique<http::GetSaveDataRequest>(saveId, 0);
getSaveData->Start();
getSaveData->Wait();
std::unique_ptr<GameSave> saveData;
try
{
saveData = std::make_unique<GameSave>(getSaveData->Finish());
}
catch (const http::RequestError &ex)
{
throw std::runtime_error("Could not load save\n" + ByteString(ex.what()));
}
newSave->SetGameSave(std::move(saveData));
gameController->LoadSave(std::move(newSave));
}
catch (std::exception & e)

View File

@ -1,6 +1,6 @@
#include "Client.h"
#include "prefs/GlobalPrefs.h"
#include "client/http/Request.h"
#include "client/http/StartupRequest.h"
#include "ClientListener.h"
#include "Format.h"
#include "MD5.h"
@ -13,7 +13,6 @@
#include "graphics/Graphics.h"
#include "prefs/Prefs.h"
#include "lua/CommandInterface.h"
#include "gui/preview/Comment.h"
#include "Config.h"
#include <cstring>
#include <cstdlib>
@ -29,8 +28,6 @@
Client::Client():
messageOfTheDay("Fetching the message of the day..."),
versionCheckRequest(nullptr),
alternateVersionCheckRequest(nullptr),
usingAltUpdateServer(false),
updateAvailable(false),
authUser(0, "")
@ -40,16 +37,7 @@ Client::Client():
authUser.Username = prefs.Get("User.Username", ByteString(""));
authUser.SessionID = prefs.Get("User.SessionID", ByteString(""));
authUser.SessionKey = prefs.Get("User.SessionKey", ByteString(""));
auto elevation = prefs.Get("User.Elevation", ByteString(""));
authUser.UserElevation = User::ElevationNone;
if (elevation == "Admin")
{
authUser.UserElevation = User::ElevationAdmin;
}
if (elevation == "Mod")
{
authUser.UserElevation = User::ElevationModerator;
}
authUser.UserElevation = prefs.Get("User.Elevation", User::ElevationNone);
firstRun = !prefs.BackedByFile();
}
@ -88,24 +76,14 @@ void Client::Initialize()
}
//Begin version check
versionCheckRequest = std::make_unique<http::Request>(ByteString::Build(SCHEME, SERVER, "/Startup.json"));
if (authUser.UserID)
{
versionCheckRequest->AuthHeaders(ByteString::Build(authUser.UserID), authUser.SessionID);
}
versionCheckRequest = std::make_unique<http::StartupRequest>(false);
versionCheckRequest->Start();
if constexpr (USE_UPDATESERVER)
{
// use an alternate update server
alternateVersionCheckRequest = std::make_unique<http::Request>(ByteString::Build(SCHEME, UPDATESERVER, "/Startup.json"));
usingAltUpdateServer = true;
if (authUser.UserID)
{
alternateVersionCheckRequest->AuthHeaders(authUser.Username, "");
}
alternateVersionCheckRequest = std::make_unique<http::StartupRequest>(true);
alternateVersionCheckRequest->Start();
usingAltUpdateServer = true;
}
}
@ -125,203 +103,82 @@ String Client::GetMessageOfTheDay()
return messageOfTheDay;
}
void Client::AddServerNotification(std::pair<String, ByteString> notification)
void Client::AddServerNotification(ServerNotification notification)
{
serverNotifications.push_back(notification);
notifyNewNotification(notification);
}
std::vector<std::pair<String, ByteString> > Client::GetServerNotifications()
std::vector<ServerNotification> Client::GetServerNotifications()
{
return serverNotifications;
}
RequestStatus Client::ParseServerReturn(ByteString &result, int status, bool json)
{
lastError = "";
// no server response, return "Malformed Response"
if (status == 200 && !result.size())
{
status = 603;
}
if (status == 302)
return RequestOkay;
if (status != 200)
{
lastError = String::Build("HTTP Error ", status, ": ", http::StatusText(status));
return RequestFailure;
}
if (json)
{
std::istringstream datastream(result);
Json::Value root;
try
{
datastream >> root;
// assume everything is fine if an empty [] is returned
if (root.size() == 0)
{
return RequestOkay;
}
int status = root.get("Status", 1).asInt();
if (status != 1)
{
lastError = ByteString(root.get("Error", "Unspecified Error").asString()).FromUtf8();
return RequestFailure;
}
}
catch (std::exception &e)
{
// sometimes the server returns a 200 with the text "Error: 401"
if (!strncmp(result.c_str(), "Error: ", 7))
{
status = ByteString(result.begin() + 7, result.end()).ToNumber<int>();
lastError = String::Build("HTTP Error ", status, ": ", http::StatusText(status));
return RequestFailure;
}
lastError = "Could not read response: " + ByteString(e.what()).FromUtf8();
return RequestFailure;
}
}
else
{
if (strncmp(result.c_str(), "OK", 2))
{
lastError = result.FromUtf8();
return RequestFailure;
}
}
return RequestOkay;
}
void Client::Tick()
{
CheckUpdate(versionCheckRequest, true);
CheckUpdate(alternateVersionCheckRequest, false);
}
void Client::CheckUpdate(std::unique_ptr<http::Request> &updateRequest, bool checkSession)
auto applyUpdateInfo = false;
if (versionCheckRequest && versionCheckRequest->CheckDone())
{
//Check status on version check request
if (updateRequest && updateRequest->CheckDone())
{
auto [ status, data ] = updateRequest->Finish();
if (checkSession && status == 618)
if (versionCheckRequest->StatusCode() == 618)
{
AddServerNotification({ "Failed to load SSL certificates", ByteString(SCHEME) + "powdertoy.co.uk/FAQ.html" });
}
if (status != 200)
{
//free(data);
if (usingAltUpdateServer && !checkSession)
this->messageOfTheDay = String::Build("HTTP Error ", status, " while checking for updates: ", http::StatusText(status));
else
this->messageOfTheDay = String::Build("HTTP Error ", status, " while fetching MotD");
}
else if(data.size())
{
std::istringstream dataStream(data);
try
{
Json::Value objDocument;
dataStream >> objDocument;
//Check session
if (checkSession)
{
if (!objDocument["Session"].asBool())
auto info = versionCheckRequest->Finish();
if (!info.sessionGood)
{
SetAuthUser(User(0, ""));
}
if (!usingAltUpdateServer)
{
updateInfo = info.updateInfo;
applyUpdateInfo = true;
messageOfTheDay = info.messageOfTheDay;
}
//Notifications from server
Json::Value notificationsArray = objDocument["Notifications"];
for (Json::UInt j = 0; j < notificationsArray.size(); j++)
for (auto &notification : info.notifications)
{
ByteString notificationLink = notificationsArray[j]["Link"].asString();
String notificationText = ByteString(notificationsArray[j]["Text"].asString()).FromUtf8();
std::pair<String, ByteString> item = std::pair<String, ByteString>(notificationText, notificationLink);
AddServerNotification(item);
}
//MOTD
if (!usingAltUpdateServer || !checkSession)
{
this->messageOfTheDay = ByteString(objDocument["MessageOfTheDay"].asString()).FromUtf8();
notifyMessageOfTheDay();
if constexpr (!IGNORE_UPDATES)
{
//Check for updates
Json::Value versions = objDocument["Updates"];
if constexpr (!SNAPSHOT)
{
Json::Value stableVersion = versions["Stable"];
int stableMajor = stableVersion["Major"].asInt();
int stableMinor = stableVersion["Minor"].asInt();
int stableBuild = stableVersion["Build"].asInt();
ByteString stableFile = stableVersion["File"].asString();
String stableChangelog = ByteString(stableVersion["Changelog"].asString()).FromUtf8();
if (stableBuild > BUILD_NUM)
{
updateAvailable = true;
updateInfo = UpdateInfo(stableMajor, stableMinor, stableBuild, stableFile, stableChangelog, UpdateInfo::Stable);
AddServerNotification(notification);
}
}
if (!updateAvailable)
catch (const http::RequestError &ex)
{
Json::Value betaVersion = versions["Beta"];
int betaMajor = betaVersion["Major"].asInt();
int betaMinor = betaVersion["Minor"].asInt();
int betaBuild = betaVersion["Build"].asInt();
ByteString betaFile = betaVersion["File"].asString();
String betaChangelog = ByteString(betaVersion["Changelog"].asString()).FromUtf8();
if (betaBuild > BUILD_NUM)
if (!usingAltUpdateServer)
{
updateAvailable = true;
updateInfo = UpdateInfo(betaMajor, betaMinor, betaBuild, betaFile, betaChangelog, UpdateInfo::Beta);
messageOfTheDay = ByteString::Build("Error while fetching MotD: ", ex.what()).FromUtf8();
}
}
if constexpr (SNAPSHOT || MOD)
versionCheckRequest.reset();
}
if (alternateVersionCheckRequest && alternateVersionCheckRequest->CheckDone())
{
Json::Value snapshotVersion = versions["Snapshot"];
int snapshotSnapshot = snapshotVersion["Snapshot"].asInt();
ByteString snapshotFile = snapshotVersion["File"].asString();
String snapshotChangelog = ByteString(snapshotVersion["Changelog"].asString()).FromUtf8();
if (snapshotSnapshot > SNAPSHOT_ID)
try
{
updateAvailable = true;
updateInfo = UpdateInfo(snapshotSnapshot, snapshotFile, snapshotChangelog, UpdateInfo::Snapshot);
auto info = alternateVersionCheckRequest->Finish();
updateInfo = info.updateInfo;
applyUpdateInfo = true;
messageOfTheDay = info.messageOfTheDay;
for (auto &notification : info.notifications)
{
AddServerNotification(notification);
}
}
if(updateAvailable)
catch (const http::RequestError &ex)
{
messageOfTheDay = ByteString::Build("Error while checking for updates: ", ex.what()).FromUtf8();
}
alternateVersionCheckRequest.reset();
}
if (applyUpdateInfo && !IGNORE_UPDATES)
{
if (updateInfo)
{
notifyUpdateAvailable();
}
}
}
}
catch (std::exception & e)
{
//Do nothing
}
}
updateRequest.reset();
}
}
UpdateInfo Client::GetUpdateInfo()
std::optional<UpdateInfo> Client::GetUpdateInfo()
{
return updateInfo;
}
@ -350,7 +207,7 @@ void Client::notifyAuthUserChanged()
}
}
void Client::notifyNewNotification(std::pair<String, ByteString> notification)
void Client::notifyNewNotification(ServerNotification notification)
{
for (std::vector<ClientListener*>::iterator iterator = listeners.begin(), end = listeners.end(); iterator != end; ++iterator)
{
@ -391,16 +248,7 @@ void Client::SetAuthUser(User user)
prefs.Set("User.SessionID", authUser.SessionID);
prefs.Set("User.SessionKey", authUser.SessionKey);
prefs.Set("User.Username", authUser.Username);
ByteString elevation = "None";
if (authUser.UserElevation == User::ElevationAdmin)
{
elevation = "Admin";
}
if (authUser.UserElevation == User::ElevationModerator)
{
elevation = "Mod";
}
prefs.Set("User.Elevation", elevation);
prefs.Set("User.Elevation", authUser.UserElevation);
}
else
{
@ -415,65 +263,6 @@ User Client::GetAuthUser()
return authUser;
}
RequestStatus Client::UploadSave(SaveInfo & save)
{
lastError = "";
int dataStatus;
ByteString data;
ByteString userID = ByteString::Build(authUser.UserID);
if (authUser.UserID)
{
if (!save.GetGameSave())
{
lastError = "Empty game save";
return RequestFailure;
}
save.SetID(0);
auto [ fromNewerVersion, gameData ] = save.GetGameSave()->Serialise();
(void)fromNewerVersion;
if (!gameData.size())
{
lastError = "Cannot serialize game save";
return RequestFailure;
}
else if (ALLOW_FAKE_NEWER_VERSION && fromNewerVersion && save.GetPublished())
{
lastError = "Cannot publish save, incompatible with latest release version.";
return RequestFailure;
}
std::tie(dataStatus, data) = http::Request::SimpleAuth(ByteString::Build(SCHEME, SERVER, "/Save.api"), userID, authUser.SessionID, {
{ "Name", save.GetName().ToUtf8() },
{ "Description", save.GetDescription().ToUtf8() },
{ "Data:save.bin", ByteString(gameData.begin(), gameData.end()) },
{ "Publish", save.GetPublished() ? "Public" : "Private" },
{ "Key", authUser.SessionKey }
});
}
else
{
lastError = "Not authenticated";
return RequestFailure;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, false);
if (ret == RequestOkay)
{
int saveID = ByteString(data.begin() + 3, data.end()).ToNumber<int>();
if (!saveID)
{
lastError = "Server did not return Save ID";
ret = RequestFailure;
}
else
save.SetID(saveID);
}
return ret;
}
void Client::MoveStampToFront(ByteString stampID)
{
auto it = std::find(stampIDs.begin(), stampIDs.end(), stampID);
@ -618,312 +407,6 @@ const std::vector<ByteString> &Client::GetStamps() const
return stampIDs;
}
RequestStatus Client::ExecVote(int saveID, int direction)
{
lastError = "";
int dataStatus;
ByteString data;
if (authUser.UserID)
{
ByteString saveIDText = ByteString::Build(saveID);
ByteString userIDText = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(ByteString::Build(SCHEME, SERVER, "/Vote.api"), userIDText, authUser.SessionID, {
{ "ID", saveIDText },
{ "Action", direction ? (direction == 1 ? "Up" : "Down") : "Reset" },
{ "Key", authUser.SessionKey }
});
}
else
{
lastError = "Not authenticated";
return RequestFailure;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, false);
return ret;
}
std::vector<char> Client::GetSaveData(int saveID, int saveDate)
{
lastError = "";
ByteString urlStr;
if (saveDate)
urlStr = ByteString::Build(STATICSCHEME, STATICSERVER, "/", saveID, "_", saveDate, ".cps");
else
urlStr = ByteString::Build(STATICSCHEME, STATICSERVER, "/", saveID, ".cps");
auto [ dataStatus, data ] = http::Request::Simple(urlStr);
// will always return failure
ParseServerReturn(data, dataStatus, false);
if (data.size() && dataStatus == 200)
{
return std::vector<char>(data.begin(), data.end());
}
return {};
}
LoginStatus Client::Login(ByteString username, ByteString password, User & user)
{
lastError = "";
user.UserID = 0;
user.Username = "";
user.SessionID = "";
user.SessionKey = "";
auto [ dataStatus, data ] = http::Request::Simple(ByteString::Build("https://", SERVER, "/Login.json"), {
{ "name", username },
{ "pass", password },
});
RequestStatus ret = ParseServerReturn(data, dataStatus, true);
if (ret == RequestOkay)
{
try
{
std::istringstream dataStream(data);
Json::Value objDocument;
dataStream >> objDocument;
ByteString usernameTemp = objDocument["Username"].asString();
int userIDTemp = objDocument["UserID"].asInt();
ByteString sessionIDTemp = objDocument["SessionID"].asString();
ByteString sessionKeyTemp = objDocument["SessionKey"].asString();
ByteString userElevationTemp = objDocument["Elevation"].asString();
Json::Value notificationsArray = objDocument["Notifications"];
for (Json::UInt j = 0; j < notificationsArray.size(); j++)
{
ByteString notificationLink = notificationsArray[j]["Link"].asString();
String notificationText = ByteString(notificationsArray[j]["Text"].asString()).FromUtf8();
std::pair<String, ByteString> item = std::pair<String, ByteString>(notificationText, notificationLink);
AddServerNotification(item);
}
user.Username = usernameTemp;
user.UserID = userIDTemp;
user.SessionID = sessionIDTemp;
user.SessionKey = sessionKeyTemp;
ByteString userElevation = userElevationTemp;
if(userElevation == "Admin")
user.UserElevation = User::ElevationAdmin;
else if(userElevation == "Mod")
user.UserElevation = User::ElevationModerator;
else
user.UserElevation= User::ElevationNone;
return LoginOkay;
}
catch (std::exception &e)
{
lastError = "Could not read response: " + ByteString(e.what()).FromUtf8();
return LoginError;
}
}
return LoginError;
}
RequestStatus Client::DeleteSave(int saveID)
{
lastError = "";
ByteString data;
ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/Delete.json?ID=", saveID, "&Mode=Delete&Key=", authUser.SessionKey);
int dataStatus;
if(authUser.UserID)
{
ByteString userID = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(url, userID, authUser.SessionID);
}
else
{
lastError = "Not authenticated";
return RequestFailure;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, true);
return ret;
}
RequestStatus Client::AddComment(int saveID, String comment)
{
lastError = "";
ByteString data;
int dataStatus;
ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/Comments.json?ID=", saveID);
if(authUser.UserID)
{
ByteString userID = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(url, userID, authUser.SessionID, {
{ "Comment", comment.ToUtf8() },
{ "Key", authUser.SessionKey }
});
}
else
{
lastError = "Not authenticated";
return RequestFailure;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, true);
return ret;
}
RequestStatus Client::FavouriteSave(int saveID, bool favourite)
{
lastError = "";
ByteStringBuilder urlStream;
ByteString data;
int dataStatus;
urlStream << SCHEME << SERVER << "/Browse/Favourite.json?ID=" << saveID << "&Key=" << authUser.SessionKey;
if(!favourite)
urlStream << "&Mode=Remove";
if(authUser.UserID)
{
ByteString userID = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(urlStream.Build(), userID, authUser.SessionID);
}
else
{
lastError = "Not authenticated";
return RequestFailure;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, true);
return ret;
}
RequestStatus Client::ReportSave(int saveID, String message)
{
lastError = "";
ByteString data;
int dataStatus;
ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/Report.json?ID=", saveID, "&Key=", authUser.SessionKey);
if(authUser.UserID)
{
ByteString userID = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(url, userID, authUser.SessionID, {
{ "Reason", message.ToUtf8() },
});
}
else
{
lastError = "Not authenticated";
return RequestFailure;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, true);
return ret;
}
RequestStatus Client::UnpublishSave(int saveID)
{
lastError = "";
ByteString data;
int dataStatus;
ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/Delete.json?ID=", saveID, "&Mode=Unpublish&Key=", authUser.SessionKey);
if(authUser.UserID)
{
ByteString userID = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(url, userID, authUser.SessionID);
}
else
{
lastError = "Not authenticated";
return RequestFailure;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, true);
return ret;
}
RequestStatus Client::PublishSave(int saveID)
{
lastError = "";
ByteString data;
int dataStatus;
ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/View.json?ID=", saveID, "&Key=", authUser.SessionKey);
if (authUser.UserID)
{
ByteString userID = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(url, userID, authUser.SessionID, {
{ "ActionPublish", "bagels" },
});
}
else
{
lastError = "Not authenticated";
return RequestFailure;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, true);
return ret;
}
std::unique_ptr<SaveInfo> Client::GetSave(int saveID, int saveDate)
{
lastError = "";
ByteStringBuilder urlStream;
urlStream << SCHEME << SERVER << "/Browse/View.json?ID=" << saveID;
if(saveDate)
{
urlStream << "&Date=" << saveDate;
}
ByteString data;
int dataStatus;
if(authUser.UserID)
{
ByteString userID = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(urlStream.Build(), userID, authUser.SessionID);
}
else
{
std::tie(dataStatus, data) = http::Request::Simple(urlStream.Build());
}
if(dataStatus == 200 && data.size())
{
try
{
std::istringstream dataStream(data);
Json::Value objDocument;
dataStream >> objDocument;
int tempID = objDocument["ID"].asInt();
int tempScoreUp = objDocument["ScoreUp"].asInt();
int tempScoreDown = objDocument["ScoreDown"].asInt();
int tempMyScore = objDocument["ScoreMine"].asInt();
ByteString tempUsername = objDocument["Username"].asString();
String tempName = ByteString(objDocument["Name"].asString()).FromUtf8();
String tempDescription = ByteString(objDocument["Description"].asString()).FromUtf8();
int tempCreatedDate = objDocument["DateCreated"].asInt();
int tempUpdatedDate = objDocument["Date"].asInt();
bool tempPublished = objDocument["Published"].asBool();
bool tempFavourite = objDocument["Favourite"].asBool();
int tempComments = objDocument["Comments"].asInt();
int tempViews = objDocument["Views"].asInt();
int tempVersion = objDocument["Version"].asInt();
Json::Value tagsArray = objDocument["Tags"];
std::list<ByteString> tempTags;
for (Json::UInt j = 0; j < tagsArray.size(); j++)
tempTags.push_back(tagsArray[j].asString());
auto tempSave = std::make_unique<SaveInfo>(tempID, tempCreatedDate, tempUpdatedDate, tempScoreUp,
tempScoreDown, tempMyScore, tempUsername, tempName,
tempDescription, tempPublished, tempTags);
tempSave->Comments = tempComments;
tempSave->Favourite = tempFavourite;
tempSave->Views = tempViews;
tempSave->Version = tempVersion;
return tempSave;
}
catch (std::exception & e)
{
lastError = "Could not read response: " + ByteString(e.what()).FromUtf8();
return nullptr;
}
}
else
{
lastError = http::StatusText(dataStatus);
}
return nullptr;
}
std::unique_ptr<SaveFile> Client::LoadSaveFile(ByteString filename)
{
ByteString err;
@ -964,84 +447,6 @@ std::unique_ptr<SaveFile> Client::LoadSaveFile(ByteString filename)
return file;
}
std::list<ByteString> * Client::RemoveTag(int saveID, ByteString tag)
{
lastError = "";
std::list<ByteString> * tags = NULL;
ByteString data;
int dataStatus;
ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/EditTag.json?Op=delete&ID=", saveID, "&Tag=", tag, "&Key=", authUser.SessionKey);
if(authUser.UserID)
{
ByteString userID = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(url, userID, authUser.SessionID);
}
else
{
lastError = "Not authenticated";
return NULL;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, true);
if (ret == RequestOkay)
{
try
{
std::istringstream dataStream(data);
Json::Value responseObject;
dataStream >> responseObject;
Json::Value tagsArray = responseObject["Tags"];
tags = new std::list<ByteString>();
for (Json::UInt j = 0; j < tagsArray.size(); j++)
tags->push_back(tagsArray[j].asString());
}
catch (std::exception &e)
{
lastError = "Could not read response: " + ByteString(e.what()).FromUtf8();
}
}
return tags;
}
std::list<ByteString> * Client::AddTag(int saveID, ByteString tag)
{
lastError = "";
std::list<ByteString> * tags = NULL;
ByteString data;
int dataStatus;
ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/EditTag.json?Op=add&ID=", saveID, "&Tag=", tag, "&Key=", authUser.SessionKey);
if(authUser.UserID)
{
ByteString userID = ByteString::Build(authUser.UserID);
std::tie(dataStatus, data) = http::Request::SimpleAuth(url, userID, authUser.SessionID);
}
else
{
lastError = "Not authenticated";
return NULL;
}
RequestStatus ret = ParseServerReturn(data, dataStatus, true);
if (ret == RequestOkay)
{
try
{
std::istringstream dataStream(data);
Json::Value responseObject;
dataStream >> responseObject;
Json::Value tagsArray = responseObject["Tags"];
tags = new std::list<ByteString>();
for (Json::UInt j = 0; j < tagsArray.size(); j++)
tags->push_back(tagsArray[j].asString());
}
catch (std::exception & e)
{
lastError = "Could not read response: " + ByteString(e.what()).FromUtf8();
}
}
return tags;
}
// stamp-specific wrapper for MergeAuthorInfo
// also used for clipboard and lua stamps
void Client::MergeStampAuthorInfo(Json::Value stampAuthors)

View File

@ -1,6 +1,7 @@
#pragma once
#include "common/String.h"
#include "common/ExplicitSingleton.h"
#include "StartupInfo.h"
#include "User.h"
#include <vector>
#include <cstdint>
@ -10,53 +11,27 @@
class SaveInfo;
class SaveFile;
class SaveComment;
class GameSave;
class VideoBuffer;
enum LoginStatus {
LoginOkay, LoginError
};
enum RequestStatus {
RequestOkay, RequestFailure
};
class UpdateInfo
{
public:
enum BuildType { Stable, Beta, Snapshot };
ByteString File;
String Changelog;
int Major;
int Minor;
int Build;
int Time;
BuildType Type;
UpdateInfo() : File(""), Changelog(""), Major(0), Minor(0), Build(0), Time(0), Type(Stable) {}
UpdateInfo(int major, int minor, int build, ByteString file, String changelog, BuildType type) : File(file), Changelog(changelog), Major(major), Minor(minor), Build(build), Time(0), Type(type) {}
UpdateInfo(int time, ByteString file, String changelog, BuildType type) : File(file), Changelog(changelog), Major(0), Minor(0), Build(0), Time(time), Type(type) {}
};
class Prefs;
class RequestListener;
class ClientListener;
namespace http
{
class Request;
class StartupRequest;
}
class Client: public ExplicitSingleton<Client> {
private:
String messageOfTheDay;
std::vector<std::pair<String, ByteString> > serverNotifications;
std::vector<ServerNotification> serverNotifications;
std::unique_ptr<http::Request> versionCheckRequest;
std::unique_ptr<http::Request> alternateVersionCheckRequest;
std::unique_ptr<http::StartupRequest> versionCheckRequest;
std::unique_ptr<http::StartupRequest> alternateVersionCheckRequest;
bool usingAltUpdateServer;
bool updateAvailable;
UpdateInfo updateInfo;
std::optional<UpdateInfo> updateInfo;
String lastError;
bool firstRun;
std::vector<ByteString> stampIDs;
@ -69,7 +44,7 @@ private:
void notifyUpdateAvailable();
void notifyAuthUserChanged();
void notifyMessageOfTheDay();
void notifyNewNotification(std::pair<String, ByteString> notification);
void notifyNewNotification(ServerNotification notification);
// Save stealing info
Json::Value authors;
@ -91,7 +66,7 @@ public:
void ClearAuthorInfo() { authors.clear(); }
bool IsAuthorsEmpty() { return authors.size() == 0; }
UpdateInfo GetUpdateInfo();
std::optional<UpdateInfo> GetUpdateInfo();
Client();
~Client();
@ -99,8 +74,8 @@ public:
ByteString FileOpenDialogue();
//std::string FileSaveDialogue();
void AddServerNotification(std::pair<String, ByteString> notification);
std::vector<std::pair<String, ByteString> > GetServerNotifications();
void AddServerNotification(ServerNotification notification);
std::vector<ServerNotification> GetServerNotifications();
void SetMessageOfTheDay(String message);
String GetMessageOfTheDay();
@ -111,9 +86,6 @@ public:
void AddListener(ClientListener * listener);
void RemoveListener(ClientListener * listener);
RequestStatus ExecVote(int saveID, int direction);
RequestStatus UploadSave(SaveInfo & save);
std::unique_ptr<SaveFile> GetStamp(ByteString stampID);
void DeleteStamp(ByteString stampID);
ByteString AddStamp(std::unique_ptr<GameSave> saveData);
@ -121,30 +93,11 @@ public:
const std::vector<ByteString> &GetStamps() const;
void MoveStampToFront(ByteString stampID);
RequestStatus AddComment(int saveID, String comment);
std::vector<char> GetSaveData(int saveID, int saveDate);
LoginStatus Login(ByteString username, ByteString password, User & user);
std::unique_ptr<SaveInfo> GetSave(int saveID, int saveDate);
std::unique_ptr<SaveFile> LoadSaveFile(ByteString filename);
RequestStatus DeleteSave(int saveID);
RequestStatus ReportSave(int saveID, String message);
RequestStatus UnpublishSave(int saveID);
RequestStatus PublishSave(int saveID);
RequestStatus FavouriteSave(int saveID, bool favourite);
void SetAuthUser(User user);
User GetAuthUser();
std::list<ByteString> * RemoveTag(int saveID, ByteString tag); //TODO RequestStatus
std::list<ByteString> * AddTag(int saveID, ByteString tag);
String GetLastError() {
return lastError;
}
RequestStatus ParseServerReturn(ByteString &result, int status, bool json);
void Tick();
void CheckUpdate(std::unique_ptr<http::Request> &updateRequest, bool checkSession);
String DoMigration(ByteString fromDir, ByteString toDir);
};

View File

@ -1,5 +1,6 @@
#pragma once
#include "common/String.h"
#include "client/ServerNotification.h"
class Client;
class ClientListener
@ -11,6 +12,6 @@ public:
virtual void NotifyUpdateAvailable(Client * sender) {}
virtual void NotifyAuthUserChanged(Client * sender) {}
virtual void NotifyMessageOfTheDay(Client * sender) {}
virtual void NotifyNewNotification(Client * sender, std::pair<String, ByteString> notification) {}
virtual void NotifyNewNotification(Client * sender, ServerNotification notification) {}
};

11
src/client/Comment.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include "User.h"
struct Comment
{
ByteString authorName;
User::Elevation authorElevation;
bool authorIsSelf;
bool authorIsBanned;
String content;
};

10
src/client/LoginInfo.h Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include "User.h"
#include "ServerNotification.h"
#include <vector>
struct LoginInfo
{
User user;
std::vector<ServerNotification> notifications;
};

View File

@ -45,7 +45,7 @@ void SaveInfo::SetName(String name)
{
this->name = name;
}
String SaveInfo::GetName()
const String &SaveInfo::GetName() const
{
return name;
}
@ -54,7 +54,7 @@ void SaveInfo::SetDescription(String description)
{
Description = description;
}
String SaveInfo::GetDescription()
const String &SaveInfo::GetDescription() const
{
return Description;
}

View File

@ -37,10 +37,10 @@ public:
SaveInfo(int _id, int _createdDate, int _updatedDate, int _votesUp, int _votesDown, int _vote, ByteString _userName, String _name, String description_, bool published_, std::list<ByteString> tags);
void SetName(String name);
String GetName();
const String &GetName() const;
void SetDescription(String description);
String GetDescription();
const String &GetDescription() const;
void SetPublished(bool published);
bool GetPublished() const;

17
src/client/Search.h Normal file
View File

@ -0,0 +1,17 @@
#pragma once
namespace http
{
enum Category
{
categoryNone,
categoryMyOwn,
categoryFavourites,
};
enum Sort
{
sortByVotes,
sortByDate,
};
}

View File

@ -0,0 +1,8 @@
#pragma once
#include "common/String.h"
struct ServerNotification
{
String text;
ByteString link;
};

29
src/client/StartupInfo.h Normal file
View File

@ -0,0 +1,29 @@
#pragma once
#include "common/String.h"
#include "ServerNotification.h"
#include <vector>
#include <optional>
struct UpdateInfo
{
enum Channel
{
channelStable,
channelBeta,
channelSnapshot,
};
Channel channel;
ByteString file;
String changeLog;
int major = 0;
int minor = 0;
int build = 0;
};
struct StartupInfo
{
bool sessionGood = false;
String messageOfTheDay;
std::vector<ServerNotification> notifications;
std::optional<UpdateInfo> updateInfo;
};

32
src/client/User.cpp Normal file
View File

@ -0,0 +1,32 @@
#include "User.h"
static const std::vector<std::pair<User::Elevation, ByteString>> elevationStrings = {
{ User::ElevationAdmin , "Admin" },
{ User::ElevationMod , "Mod" },
{ User::ElevationHalfMod, "HalfMod" },
{ User::ElevationNone , "None" },
};
User::Elevation User::ElevationFromString(ByteString str)
{
auto it = std::find_if(elevationStrings.begin(), elevationStrings.end(), [&str](auto &item) {
return item.second == str;
});
if (it != elevationStrings.end())
{
return it->first;
}
return ElevationNone;
}
ByteString User::ElevationToString(Elevation elevation)
{
auto it = std::find_if(elevationStrings.begin(), elevationStrings.end(), [elevation](auto &item) {
return item.first == elevation;
});
if (it != elevationStrings.end())
{
return it->second;
}
return "None";
}

View File

@ -7,8 +7,14 @@ class User
public:
enum Elevation
{
ElevationAdmin, ElevationModerator, ElevationNone
ElevationNone,
ElevationHalfMod,
ElevationMod,
ElevationAdmin,
};
static Elevation ElevationFromString(ByteString str);
static ByteString ElevationToString(Elevation elevation);
int UserID;
ByteString Username;
ByteString SessionID;

View File

@ -1,35 +1,36 @@
#include "APIRequest.h"
#include "client/Client.h"
namespace http
{
APIRequest::APIRequest(ByteString url) : Request(url)
APIRequest::APIRequest(ByteString url, AuthMode authMode, bool newCheckStatus) : Request(url), checkStatus(newCheckStatus)
{
auto user = Client::Ref().GetAuthUser();
if (authMode == authRequire && !user.UserID)
{
FailEarly("Not authenticated");
return;
}
if (authMode != authOmit && user.UserID)
{
User user = Client::Ref().GetAuthUser();
AuthHeaders(ByteString::Build(user.UserID), user.SessionID);
}
}
APIRequest::Result APIRequest::Finish()
Json::Value APIRequest::Finish()
{
Result result;
auto [ status, data ] = Request::Finish();
ParseResponse(data, status, checkStatus ? responseJson : responseData);
Json::Value document;
try
{
ByteString data;
std::tie(result.status, data) = Request::Finish();
Client::Ref().ParseServerReturn(data, result.status, true);
if (result.status == 200 && data.size())
std::istringstream ss(data);
ss >> document;
}
catch (const std::exception &ex)
{
std::istringstream dataStream(data);
Json::Value objDocument;
dataStream >> objDocument;
result.document = std::unique_ptr<Json::Value>(new Json::Value(objDocument));
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return document;
}
}
catch (std::exception & e)
{
}
return result;
}
}

View File

@ -2,22 +2,22 @@
#include "Request.h"
#include "common/String.h"
#include <json/json.h>
#include <memory>
#include <map>
namespace http
{
class APIRequest : public Request
{
bool checkStatus;
public:
struct Result
enum AuthMode
{
int status;
std::unique_ptr<Json::Value> document;
authRequire,
authUse,
authOmit,
};
APIRequest(ByteString url, AuthMode authMode, bool newCheckStatus);
APIRequest(ByteString url);
Result Finish();
Json::Value Finish();
};
}

View File

@ -0,0 +1,21 @@
#include "AddCommentRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
AddCommentRequest::AddCommentRequest(int saveID, String comment) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Browse/Comments.json?ID=", saveID), authRequire, true)
{
auto user = Client::Ref().GetAuthUser();
AddPostData(FormData{
{ "Comment", comment.ToUtf8() },
{ "Key", user.SessionKey },
});
}
void AddCommentRequest::Finish()
{
APIRequest::Finish();
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "APIRequest.h"
namespace http
{
class AddCommentRequest : public APIRequest
{
public:
AddCommentRequest(int saveID, String comment);
void Finish();
};
}

View File

@ -0,0 +1,29 @@
#include "AddTagRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
AddTagRequest::AddTagRequest(int saveID, ByteString tag) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Browse/EditTag.json?Op=add&ID=", saveID, "&Tag=", tag, "&Key=", Client::Ref().GetAuthUser().SessionKey), authRequire, true)
{
}
std::list<ByteString> AddTagRequest::Finish()
{
auto result = APIRequest::Finish();
std::list<ByteString> tags;
try
{
for (auto &tag : result["Tags"])
{
tags.push_back(tag.asString());
}
}
catch (const std::exception &ex)
{
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return tags;
}
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "APIRequest.h"
#include <list>
namespace http
{
class AddTagRequest : public APIRequest
{
public:
AddTagRequest(int saveID, ByteString tag);
std::list<ByteString> Finish();
};
}

View File

@ -0,0 +1,16 @@
#include "DeleteSaveRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
DeleteSaveRequest::DeleteSaveRequest(int saveID) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Browse/Delete.json?ID=", saveID, "&Mode=Delete&Key=", Client::Ref().GetAuthUser().SessionKey), authRequire, true)
{
}
void DeleteSaveRequest::Finish()
{
APIRequest::Finish();
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "APIRequest.h"
namespace http
{
class DeleteSaveRequest : public APIRequest
{
public:
DeleteSaveRequest(int saveID);
void Finish();
};
}

View File

@ -0,0 +1,30 @@
#include "ExecVoteRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
ExecVoteRequest::ExecVoteRequest(int saveID, int newDirection) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Vote.api"), authRequire, false),
direction(newDirection)
{
auto user = Client::Ref().GetAuthUser();
if (!user.UserID)
{
FailEarly("Not authenticated");
return;
}
AuthHeaders(ByteString::Build(user.UserID), user.SessionID);
AddPostData(FormData{
{ "ID", ByteString::Build(saveID) },
{ "Action", direction ? (direction == 1 ? "Up" : "Down") : "Reset" },
{ "Key", user.SessionKey },
});
}
void ExecVoteRequest::Finish()
{
auto [ status, data ] = Request::Finish();
ParseResponse(data, status, responseOk);
}
}

View File

@ -0,0 +1,20 @@
#pragma once
#include "APIRequest.h"
namespace http
{
class ExecVoteRequest : public APIRequest
{
int direction;
public:
ExecVoteRequest(int saveID, int newDirection);
void Finish();
int Direction() const
{
return direction;
}
};
}

View File

@ -0,0 +1,28 @@
#include "FavouriteSaveRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
static ByteString Url(int saveID, bool favourite)
{
ByteStringBuilder builder;
builder << SCHEME << SERVER << "/Browse/Favourite.json?ID=" << saveID << "&Key=" << Client::Ref().GetAuthUser().SessionKey;
if (!favourite)
{
builder << "&Mode=Remove";
}
return builder.Build();
}
FavouriteSaveRequest::FavouriteSaveRequest(int saveID, bool newFavourite) :
APIRequest(Url(saveID, newFavourite), authRequire, true),
favourite(newFavourite)
{
}
void FavouriteSaveRequest::Finish()
{
APIRequest::Finish();
}
}

View File

@ -0,0 +1,20 @@
#pragma once
#include "APIRequest.h"
namespace http
{
class FavouriteSaveRequest : public APIRequest
{
bool favourite;
public:
FavouriteSaveRequest(int saveID, bool newFavourite);
void Finish();
bool Favourite() const
{
return favourite;
}
};
}

View File

@ -0,0 +1,36 @@
#include "GetCommentsRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
GetCommentsRequest::GetCommentsRequest(int saveID, int start, int count) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Browse/Comments.json?ID=", saveID, "&Start=", start, "&Count=", count), authOmit, false)
{
}
std::vector<Comment> GetCommentsRequest::Finish()
{
auto result = APIRequest::Finish();
std::vector<Comment> comments;
auto user = Client::Ref().GetAuthUser();
try
{
for (auto &comment : result)
{
comments.push_back({
comment["Username"].asString(),
User::ElevationFromString(comment["Elevation"].asString()),
ByteString(comment["UserID"].asString()).ToNumber<int>() == user.UserID,
comment["IsBanned"].asBool(),
ByteString(comment["Text"].asString()).FromUtf8(),
});
}
}
catch (const std::exception &ex)
{
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return comments;
}
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "APIRequest.h"
#include "client/Comment.h"
namespace http
{
class GetCommentsRequest : public APIRequest
{
public:
GetCommentsRequest(int saveID, int start, int count);
std::vector<Comment> Finish();
};
}

View File

@ -0,0 +1,28 @@
#include "GetSaveDataRequest.h"
#include "Config.h"
namespace http
{
static ByteString Url(int saveID, int saveDate)
{
ByteStringBuilder builder;
builder << STATICSCHEME << STATICSERVER << "/" << saveID;
if (saveDate)
{
builder << "_" << saveDate;
}
builder << ".cps";
return builder.Build();
}
GetSaveDataRequest::GetSaveDataRequest(int saveID, int saveDate) : Request(Url(saveID, saveDate))
{
}
std::vector<char> GetSaveDataRequest::Finish()
{
auto [ status, data ] = Request::Finish();
ParseResponse(data, status, responseData);
return std::vector<char>(data.begin(), data.end());
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "Request.h"
namespace http
{
class GetSaveDataRequest : public Request
{
public:
GetSaveDataRequest(int saveID, int saveDate);
std::vector<char> Finish();
};
}

View File

@ -0,0 +1,69 @@
#include "GetSaveRequest.h"
#include "client/Client.h"
#include "client/SaveInfo.h"
#include "client/GameSave.h"
#include "Config.h"
namespace http
{
static ByteString Url(int saveID, int saveDate)
{
ByteStringBuilder builder;
builder << SCHEME << SERVER << "/Browse/View.json?ID=" << saveID;
if (saveDate)
{
builder << "&Date=" << saveDate;
}
return builder.Build();
}
GetSaveRequest::GetSaveRequest(int saveID, int saveDate) : Request(Url(saveID, saveDate))
{
auto user = Client::Ref().GetAuthUser();
if (user.UserID)
{
// This is needed so we know how we rated this save.
AuthHeaders(ByteString::Build(user.UserID), user.SessionID);
}
}
std::unique_ptr<SaveInfo> GetSaveRequest::Finish()
{
auto [ status, data ] = Request::Finish();
ParseResponse(data, status, responseData);
std::unique_ptr<SaveInfo> saveInfo;
try
{
Json::Value document;
std::istringstream ss(data);
ss >> document;
std::list<ByteString> tags;
for (auto &tag : document["Tags"])
{
tags.push_back(tag.asString());
}
saveInfo = std::make_unique<SaveInfo>(
document["ID"].asInt(),
document["DateCreated"].asInt(),
document["Date"].asInt(),
document["ScoreUp"].asInt(),
document["ScoreDown"].asInt(),
document["ScoreMine"].asInt(),
document["Username"].asString(),
ByteString(document["Name"].asString()).FromUtf8(),
ByteString(document["Description"].asString()).FromUtf8(),
document["Published"].asBool(),
tags
);
saveInfo->Comments = document["Comments"].asInt();
saveInfo->Favourite = document["Favourite"].asBool();
saveInfo->Views = document["Views"].asInt();
saveInfo->Version = document["Version"].asInt();
}
catch (const std::exception &ex)
{
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return saveInfo;
}
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "Request.h"
#include <memory>
class SaveInfo;
namespace http
{
class GetSaveRequest : public Request
{
public:
GetSaveRequest(int saveID, int saveDate);
std::unique_ptr<SaveInfo> Finish();
};
}

View File

@ -5,18 +5,18 @@
namespace http
{
GetUserInfoRequest::GetUserInfoRequest(ByteString username) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/User.json?Name=", username))
APIRequest(ByteString::Build(SCHEME, SERVER, "/User.json?Name=", username), authOmit, false)
{
}
std::unique_ptr<UserInfo> GetUserInfoRequest::Finish()
UserInfo GetUserInfoRequest::Finish()
{
std::unique_ptr<UserInfo> user_info;
auto result = APIRequest::Finish();
if (result.document)
UserInfo userInfo;
try
{
auto &user = (*result.document)["User"];
user_info = std::unique_ptr<UserInfo>(new UserInfo(
auto &user = result["User"];
userInfo = UserInfo(
user["ID"].asInt(),
user["Age"].asInt(),
user["Username"].asString(),
@ -29,9 +29,13 @@ namespace http
user["Forum"]["Topics"].asInt(),
user["Forum"]["Replies"].asInt(),
user["Forum"]["Reputation"].asInt()
));
);
}
return user_info;
catch (const std::exception &ex)
{
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return userInfo;
}
}

View File

@ -1,7 +1,6 @@
#pragma once
#include "APIRequest.h"
class UserInfo;
#include "client/UserInfo.h"
namespace http
{
@ -10,6 +9,6 @@ namespace http
public:
GetUserInfoRequest(ByteString username);
std::unique_ptr<UserInfo> Finish();
UserInfo Finish();
};
}

View File

@ -1,30 +1,28 @@
#include "ImageRequest.h"
#include "graphics/Graphics.h"
#include "client/Client.h"
#include <iostream>
namespace http
{
ImageRequest::ImageRequest(ByteString url, Vec2<int> size):
Request(std::move(url)),
size(size)
{}
ImageRequest::ImageRequest(ByteString url, Vec2<int> newRequestedSize) : Request(url), requestedSize(newRequestedSize)
{
}
std::unique_ptr<VideoBuffer> ImageRequest::Finish()
{
auto [ status, data ] = Request::Finish();
(void)status; // We don't use this for anything, not ideal >_>
std::unique_ptr<VideoBuffer> vb;
if (data.size())
{
vb = VideoBuffer::FromPNG(std::vector<char>(data.begin(), data.end()));
ParseResponse(data, status, responseData);
auto vb = VideoBuffer::FromPNG(std::vector<char>(data.begin(), data.end()));
if (vb)
vb->Resize(size, true);
{
vb->Resize(requestedSize, true);
}
else
{
vb = std::make_unique<VideoBuffer>(Vec2(15, 16));
vb->BlendChar(Vec2(2, 4), 0xE06E, 0xFFFFFF_rgb .WithAlpha(0xFF));
}
}
return vb;
}
}

View File

@ -2,7 +2,6 @@
#include "common/String.h"
#include "common/Vec2.h"
#include "Request.h"
#include <memory>
class VideoBuffer;
@ -11,10 +10,10 @@ namespace http
{
class ImageRequest : public Request
{
Vec2<int> size;
Vec2<int> requestedSize;
public:
ImageRequest(ByteString url, Vec2<int> size);
ImageRequest(ByteString url, Vec2<int> newRequestedSize);
std::unique_ptr<VideoBuffer> Finish();
};

View File

@ -0,0 +1,45 @@
#include "LoginRequest.h"
#include "Config.h"
#include "client/Client.h"
#include <json/json.h>
namespace http
{
LoginRequest::LoginRequest(ByteString username, ByteString password) : Request(ByteString::Build("https://", SERVER, "/Login.json"))
{
AddPostData(FormData{
{ "name", username },
{ "pass", password },
});
}
LoginInfo LoginRequest::Finish()
{
auto [ status, data ] = Request::Finish();
ParseResponse(data, status, responseJson);
LoginInfo loginInfo = { { 0, "" }, {} };
try
{
Json::Value document;
std::istringstream ss(data);
ss >> document;
loginInfo.user.Username = document["Username"].asString();
loginInfo.user.UserID = document["UserID"].asInt();
loginInfo.user.SessionID = document["SessionID"].asString();
loginInfo.user.SessionKey = document["SessionKey"].asString();
loginInfo.user.UserElevation = User::ElevationFromString(document["Elevation"].asString());
for (auto &item : document["Notifications"])
{
loginInfo.notifications.push_back({
ByteString(item["Text"].asString()).FromUtf8(),
item["Link"].asString(),
});
}
}
catch (const std::exception &ex)
{
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return loginInfo;
}
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "Request.h"
#include "client/LoginInfo.h"
namespace http
{
class LoginRequest : public Request
{
public:
LoginRequest(ByteString username, ByteString password);
LoginInfo Finish();
};
}

View File

@ -0,0 +1,20 @@
#include "LogoutRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
LogoutRequest::LogoutRequest() :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Logout.json"), authRequire, false)
{
auto user = Client::Ref().GetAuthUser();
AddPostData(FormData{
{ "Key", user.SessionKey },
});
}
void LogoutRequest::Finish()
{
APIRequest::Finish();
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "APIRequest.h"
namespace http
{
class LogoutRequest : public APIRequest
{
public:
LogoutRequest();
void Finish();
};
}

View File

@ -0,0 +1,19 @@
#include "PublishSaveRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
PublishSaveRequest::PublishSaveRequest(int saveID) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Browse/View.json?ID=", saveID, "&Key=", Client::Ref().GetAuthUser().SessionKey), authRequire, true)
{
AddPostData(FormData{
{ "ActionPublish", "bagels" },
});
}
void PublishSaveRequest::Finish()
{
APIRequest::Finish();
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "APIRequest.h"
namespace http
{
class PublishSaveRequest : public APIRequest
{
public:
PublishSaveRequest(int saveID);
void Finish();
};
}

View File

@ -0,0 +1,29 @@
#include "RemoveTagRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
RemoveTagRequest::RemoveTagRequest(int saveID, ByteString tag) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Browse/EditTag.json?Op=delete&ID=", saveID, "&Tag=", tag, "&Key=", Client::Ref().GetAuthUser().SessionKey), authRequire, true)
{
}
std::list<ByteString> RemoveTagRequest::Finish()
{
auto result = APIRequest::Finish();
std::list<ByteString> tags;
try
{
for (auto &tag : result["Tags"])
{
tags.push_back(tag.asString());
}
}
catch (const std::exception &ex)
{
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return tags;
}
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "APIRequest.h"
#include <list>
namespace http
{
class RemoveTagRequest : public APIRequest
{
public:
RemoveTagRequest(int saveID, ByteString tag);
std::list<ByteString> Finish();
};
}

View File

@ -0,0 +1,19 @@
#include "ReportSaveRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
ReportSaveRequest::ReportSaveRequest(int saveID, String message) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Browse/Report.json?ID=", saveID, "&Key=", Client::Ref().GetAuthUser().SessionKey), authRequire, true)
{
AddPostData(FormData{
{ "Reason", message.ToUtf8() },
});
}
void ReportSaveRequest::Finish()
{
APIRequest::Finish();
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "APIRequest.h"
namespace http
{
class ReportSaveRequest : public APIRequest
{
public:
ReportSaveRequest(int saveID, String message);
void Finish();
};
}

View File

@ -2,6 +2,8 @@
#include "requestmanager/RequestManager.h"
#include <memory>
#include <iostream>
#include <cstring>
#include <json/json.h>
namespace http
{
@ -13,12 +15,28 @@ namespace http
Request::~Request()
{
if (handle->state != RequestHandle::ready)
bool tryUnregister;
{
std::lock_guard lk(handle->stateMx);
tryUnregister = handle->state == RequestHandle::running;
}
if (tryUnregister)
{
// At this point it may have already finished and been unregistered but that's ok,
// attempting to unregister a request multiple times is allowed. We only do the
// state-checking dance so we don't wake up RequestManager if we don't have to.
// In fact, we could just not unregister requests here at all, they'd just run to
// completion and be unregistered later. All this does is cancel them early.
RequestManager::Ref().UnregisterRequest(*this);
}
}
void Request::FailEarly(ByteString error)
{
assert(handle->state == RequestHandle::ready);
handle->failEarly = error;
}
void Request::Verb(ByteString newVerb)
{
assert(handle->state == RequestHandle::ready);
@ -84,18 +102,36 @@ namespace http
return handle->responseHeaders;
}
std::pair<int, ByteString> Request::Finish()
void Request::Wait()
{
std::unique_lock lk(handle->stateMx);
if (handle->state == RequestHandle::running)
{
assert(handle->state == RequestHandle::running);
handle->stateCv.wait(lk, [this]() {
return handle->state == RequestHandle::done;
});
}
int Request::StatusCode() const
{
{
std::unique_lock lk(handle->stateMx);
assert(handle->state == RequestHandle::done);
}
return handle->statusCode;
}
std::pair<int, ByteString> Request::Finish()
{
{
std::unique_lock lk(handle->stateMx);
assert(handle->state == RequestHandle::done);
}
handle->state = RequestHandle::dead;
return { handle->statusCode, std::move(handle->responseData) };
if (handle->error)
{
throw RequestError(*handle->error);
}
return std::pair{ handle->statusCode, std::move(handle->responseData) };
}
void RequestHandle::MarkDone()
@ -106,30 +142,13 @@ namespace http
state = RequestHandle::done;
}
stateCv.notify_one();
if (error.size())
if (error)
{
std::cerr << error << std::endl;
std::cerr << *error << std::endl;
}
}
std::pair<int, ByteString> Request::Simple(ByteString uri, FormData postData)
{
return SimpleAuth(uri, "", "", postData);
}
std::pair<int, ByteString> Request::SimpleAuth(ByteString uri, ByteString ID, ByteString session, FormData postData)
{
auto request = std::make_unique<Request>(uri);
if (!postData.empty())
{
request->AddPostData(postData);
}
request->AuthHeaders(ID, session);
request->Start();
return request->Finish();
}
String StatusText(int ret)
const char *StatusText(int ret)
{
switch (ret)
{
@ -211,7 +230,69 @@ namespace http
case 619: return "SSL: Failed to Load CRL File";
case 620: return "SSL: Issuer Check Failed";
case 621: return "SSL: Pinned Public Key Mismatch";
default: return "Unknown Status Code";
}
return "Unknown Status Code";
}
void Request::ParseResponse(const ByteString &result, int status, ResponseType responseType)
{
// no server response, return "Malformed Response"
if (status == 200 && !result.size())
{
status = 603;
}
if (status == 302)
{
return;
}
if (status != 200)
{
throw RequestError(ByteString::Build("HTTP Error ", status, ": ", http::StatusText(status)));
}
switch (responseType)
{
case responseOk:
if (strncmp(result.c_str(), "OK", 2))
{
throw RequestError(result);
}
break;
case responseJson:
{
std::istringstream ss(result);
Json::Value root;
try
{
ss >> root;
// assume everything is fine if an empty [] is returned
if (root.size() == 0)
{
return;
}
int status = root.get("Status", 1).asInt();
if (status != 1)
{
throw RequestError(ByteString(root.get("Error", "Unspecified Error").asString()));
}
}
catch (const std::exception &ex)
{
// sometimes the server returns a 200 with the text "Error: 401"
if (!strncmp(result.c_str(), "Error: ", 7))
{
status = ByteString(result.begin() + 7, result.end()).ToNumber<int>();
throw RequestError(ByteString::Build("HTTP Error ", status, ": ", http::StatusText(status)));
}
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
}
break;
case responseData:
// no further processing required
break;
}
}
}

View File

@ -6,11 +6,18 @@
#include <vector>
#include <mutex>
#include <condition_variable>
#include <optional>
namespace http
{
struct RequestHandle;
// Thrown by Finish and ParseResponse
struct RequestError : public std::runtime_error
{
using runtime_error::runtime_error;
};
class Request
{
std::shared_ptr<RequestHandle> handle;
@ -21,6 +28,8 @@ namespace http
Request &operator =(const Request &) = delete;
~Request();
void FailEarly(ByteString error);
void Verb(ByteString newVerb);
void AddHeader(ByteString header);
@ -32,13 +41,21 @@ namespace http
std::pair<int, int> CheckProgress() const; // total, done
const std::vector<ByteString> &ResponseHeaders() const;
void Wait();
int StatusCode() const; // status
std::pair<int, ByteString> Finish(); // status, data
static std::pair<int, ByteString> Simple(ByteString uri, FormData postData = {});
static std::pair<int, ByteString> SimpleAuth(ByteString uri, ByteString ID, ByteString session, FormData postData = {});
enum ResponseType
{
responseOk,
responseJson,
responseData,
};
static void ParseResponse(const ByteString &result, int status, ResponseType responseType);
friend class RequestManager;
};
String StatusText(int code);
const char *StatusText(int code);
}

View File

@ -1,29 +1,20 @@
#include "SaveUserInfoRequest.h"
#include "client/UserInfo.h"
#include "Config.h"
namespace http
{
SaveUserInfoRequest::SaveUserInfoRequest(UserInfo &info) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Profile.json"))
SaveUserInfoRequest::SaveUserInfoRequest(UserInfo info) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Profile.json"), authRequire, true)
{
AddPostData(FormData{
{ "Location", info.location.ToUtf8() },
{ "Biography", info.biography.ToUtf8() }
{ "Biography", info.biography.ToUtf8() },
});
}
bool SaveUserInfoRequest::Finish()
void SaveUserInfoRequest::Finish()
{
auto result = APIRequest::Finish();
if (result.document)
{
return (*result.document)["Status"].asInt() == 1;
}
else
{
return false;
}
APIRequest::Finish();
}
}

View File

@ -1,15 +1,14 @@
#pragma once
#include "APIRequest.h"
class UserInfo;
#include "client/UserInfo.h"
namespace http
{
class SaveUserInfoRequest : public APIRequest
{
public:
SaveUserInfoRequest(UserInfo &info);
SaveUserInfoRequest(UserInfo info);
bool Finish();
void Finish();
};
}

View File

@ -0,0 +1,85 @@
#include "SearchSavesRequest.h"
#include "Config.h"
#include "client/Client.h"
#include "client/GameSave.h"
#include "Format.h"
namespace http
{
static ByteString Url(int start, int count, ByteString query, Sort sort, Category category)
{
ByteStringBuilder builder;
builder << SCHEME << SERVER << "/Browse.json?Start=" << start << "&Count=" << count;
auto appendToQuery = [&query](ByteString str) {
if (query.size())
{
query += " ";
}
query += str;
};
switch (sort)
{
case sortByDate:
appendToQuery("sort:date");
break;
default:
break;
}
auto user = Client::Ref().GetAuthUser();
switch (category)
{
case categoryFavourites:
builder << "&Category=Favourites";
break;
case categoryMyOwn:
assert(user.UserID);
appendToQuery("user:" + user.Username);
break;
default:
break;
}
if (query.size())
{
builder << "&Search_Query=" << format::URLEncode(query);
}
return builder.Build();
}
SearchSavesRequest::SearchSavesRequest(int start, int count, ByteString query, Sort sort, Category category) : APIRequest(Url(start, count, query, sort, category), authUse, false)
{
}
std::pair<int, std::vector<std::unique_ptr<SaveInfo>>> SearchSavesRequest::Finish()
{
std::vector<std::unique_ptr<SaveInfo>> saves;
auto result = APIRequest::Finish();
int count;
try
{
count = result["Count"].asInt();
for (auto &save : result["Saves"])
{
auto saveInfo = std::make_unique<SaveInfo>(
save["ID"].asInt(),
save["Created"].asInt(),
save["Updated"].asInt(),
save["ScoreUp"].asInt(),
save["ScoreDown"].asInt(),
save["Username"].asString(),
ByteString(save["Name"].asString()).FromUtf8()
);
saveInfo->Version = save["Version"].asInt();
saveInfo->SetPublished(save["Published"].asBool());
saves.push_back(std::move(saveInfo));
}
}
catch (const std::exception &ex)
{
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return std::pair{ count, std::move(saves) };
}
}

View File

@ -0,0 +1,15 @@
#pragma once
#include "APIRequest.h"
#include "client/SaveInfo.h"
#include "client/Search.h"
namespace http
{
class SearchSavesRequest : public APIRequest
{
public:
SearchSavesRequest(int start, int count, ByteString query, Sort sort, Category category);
std::pair<int, std::vector<std::unique_ptr<SaveInfo>>> Finish();
};
}

View File

@ -0,0 +1,42 @@
#include "SearchTagsRequest.h"
#include "Config.h"
#include "Format.h"
namespace http
{
static ByteString Url(int start, int count, ByteString query)
{
ByteStringBuilder builder;
builder << SCHEME << SERVER << "/Browse/Tags.json?Start=" << start << "&Count=" << count;
if (query.size())
{
builder << "&Search_Query=" << format::URLEncode(query);
}
return builder.Build();
}
SearchTagsRequest::SearchTagsRequest(int start, int count, ByteString query) : APIRequest(Url(start, count, query), authOmit, false)
{
}
std::vector<std::pair<ByteString, int>> SearchTagsRequest::Finish()
{
std::vector<std::pair<ByteString, int>> tags;
auto result = APIRequest::Finish();
try
{
for (auto &tag : result["Tags"])
{
tags.push_back({
tag["Tag"].asString(),
tag["Count"].asInt(),
});
}
}
catch (const std::exception &ex)
{
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return tags;
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "APIRequest.h"
namespace http
{
class SearchTagsRequest : public APIRequest
{
public:
SearchTagsRequest(int start, int count, ByteString query);
std::vector<std::pair<ByteString, int>> Finish();
};
}

View File

@ -0,0 +1,102 @@
#include "StartupRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
// TODO: update Client::messageOfTheDay
StartupRequest::StartupRequest(bool newAlternate) :
Request(ByteString::Build(SCHEME, newAlternate ? UPDATESERVER : SERVER, "/Startup.json")),
alternate(newAlternate)
{
auto user = Client::Ref().GetAuthUser();
if (user.UserID)
{
if (alternate)
{
// Cursed
AuthHeaders(user.Username, "");
}
else
{
AuthHeaders(ByteString::Build(user.UserID), user.SessionID);
}
}
}
StartupInfo StartupRequest::Finish()
{
auto [ status, data ] = Request::Finish();
ParseResponse(data, status, responseJson);
StartupInfo startupInfo;
try
{
Json::Value document;
std::istringstream ss(data);
ss >> document;
startupInfo.sessionGood = document["Session"].asBool();
startupInfo.messageOfTheDay = ByteString(document["MessageOfTheDay"].asString()).FromUtf8();
for (auto &notification : document["Notifications"])
{
startupInfo.notifications.push_back({
ByteString(notification["Text"].asString()).FromUtf8(),
notification["Link"].asString()
});
}
if constexpr (!IGNORE_UPDATES)
{
auto &versions = document["Updates"];
auto parseUpdate = [this, &versions, &startupInfo](ByteString key, UpdateInfo::Channel channel, std::function<bool (int)> updateAvailableFunc) {
if (!versions.isMember(key))
{
return;
}
auto &info = versions[key];
auto getOr = [&info](ByteString key, int defaultValue) {
if (!info.isMember(key))
{
return defaultValue;
}
return info[key].asInt();
};
auto build = getOr(key == "Snapshot" ? "Snapshot" : "Build", -1);
if (!updateAvailableFunc(build))
{
return;
}
startupInfo.updateInfo = UpdateInfo{
channel,
ByteString::Build(SCHEME, alternate ? UPDATESERVER : SERVER, info["File"].asString()),
ByteString(info["Changelog"].asString()).FromUtf8(),
getOr("Major", -1),
getOr("Minor", -1),
build,
};
};
if constexpr (SNAPSHOT || MOD)
{
parseUpdate("Snapshot", UpdateInfo::channelSnapshot, [](int build) {
return build > SNAPSHOT_ID;
});
}
else
{
parseUpdate("Stable", UpdateInfo::channelStable, [](int build) {
return build > BUILD_NUM;
});
if (!startupInfo.updateInfo.has_value())
{
parseUpdate("Beta", UpdateInfo::channelBeta, [](int build) {
return build > BUILD_NUM;
});
}
}
}
}
catch (const std::exception &ex)
{
throw RequestError("Could not read response: " + ByteString(ex.what()));
}
return startupInfo;
}
}

View File

@ -0,0 +1,16 @@
#pragma once
#include "APIRequest.h"
#include "client/StartupInfo.h"
namespace http
{
class StartupRequest : public Request
{
bool alternate;
public:
StartupRequest(bool newAlternate);
StartupInfo Finish();
};
}

View File

@ -0,0 +1,17 @@
#include "UnpublishSaveRequest.h"
#include "client/Client.h"
#include "Config.h"
namespace http
{
UnpublishSaveRequest::UnpublishSaveRequest(int saveID) :
APIRequest(ByteString::Build(SCHEME, SERVER, "/Browse/Delete.json?ID=", saveID, "&Mode=Unpublish&Key=", Client::Ref().GetAuthUser().SessionKey), authRequire, true)
{
}
void UnpublishSaveRequest::Finish()
{
APIRequest::Finish();
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "APIRequest.h"
namespace http
{
class UnpublishSaveRequest : public APIRequest
{
public:
UnpublishSaveRequest(int saveID);
void Finish();
};
}

View File

View File

View File

@ -0,0 +1,49 @@
#include "UploadSaveRequest.h"
#include "client/SaveInfo.h"
#include "client/Client.h"
#include "client/GameSave.h"
#include "Config.h"
namespace http
{
UploadSaveRequest::UploadSaveRequest(const SaveInfo &saveInfo) : Request(ByteString::Build(SCHEME, SERVER, "/Save.api"))
{
auto [ fromNewerVersion, gameData ] = saveInfo.GetGameSave()->Serialise();
if (!gameData.size())
{
FailEarly("Cannot serialize game save");
return;
}
else if (ALLOW_FAKE_NEWER_VERSION && fromNewerVersion && saveInfo.GetPublished())
{
FailEarly("Cannot publish save, incompatible with latest release version");
return;
}
auto user = Client::Ref().GetAuthUser();
if (!user.UserID)
{
FailEarly("Not authenticated");
return;
}
AuthHeaders(ByteString::Build(user.UserID), user.SessionID);
AddPostData(FormData{
{ "Name", saveInfo.GetName().ToUtf8() },
{ "Description", saveInfo.GetDescription().ToUtf8() },
{ "Data:save.bin", ByteString(gameData.begin(), gameData.end()) },
{ "Publish", saveInfo.GetPublished() ? "Public" : "Private" },
{ "Key", user.SessionKey },
});
}
int UploadSaveRequest::Finish()
{
auto [ status, data ] = Request::Finish();
ParseResponse(data, status, responseOk);
int saveID = ByteString(data.begin() + 3, data.end()).ToNumber<int>();
if (!saveID)
{
throw RequestError("Server did not return Save ID");
}
return saveID;
}
}

View File

@ -0,0 +1,15 @@
#pragma once
#include "Request.h"
class SaveInfo;
namespace http
{
class UploadSaveRequest : public Request
{
public:
UploadSaveRequest(const SaveInfo &saveInfo);
int Finish();
};
}

View File

@ -5,6 +5,25 @@ client_files += files(
'Request.cpp',
'SaveUserInfoRequest.cpp',
'ThumbnailRequest.cpp',
'AddTagRequest.cpp',
'RemoveTagRequest.cpp',
'GetSaveRequest.cpp',
'PublishSaveRequest.cpp',
'UnpublishSaveRequest.cpp',
'ReportSaveRequest.cpp',
'FavouriteSaveRequest.cpp',
'AddCommentRequest.cpp',
'DeleteSaveRequest.cpp',
'LoginRequest.cpp',
'GetSaveDataRequest.cpp',
'ExecVoteRequest.cpp',
'UploadSaveRequest.cpp',
'StartupRequest.cpp',
'UpdateRequest.cpp',
'SearchSavesRequest.cpp',
'SearchTagsRequest.cpp',
'GetCommentsRequest.cpp',
'LogoutRequest.cpp',
)
subdir('requestmanager')

View File

@ -22,6 +22,13 @@ namespace http
void RequestManager::RegisterRequest(Request &request)
{
if (request.handle->failEarly)
{
request.handle->error = request.handle->failEarly.value();
request.handle->statusCode = 600;
request.handle->MarkDone();
return;
}
if (disableNetwork)
{
request.handle->statusCode = 604;

View File

@ -285,16 +285,20 @@ namespace http
void RequestManager::RegisterRequestImpl(Request &request)
{
auto manager = static_cast<RequestManagerImpl *>(this);
{
std::lock_guard lk(manager->sharedStateMx);
manager->requestHandlesToRegister.push_back(request.handle);
}
curl_multi_wakeup(manager->curlMulti);
}
void RequestManager::UnregisterRequestImpl(Request &request)
{
auto manager = static_cast<RequestManagerImpl *>(this);
{
std::lock_guard lk(manager->sharedStateMx);
manager->requestHandlesToUnregister.push_back(request.handle);
}
curl_multi_wakeup(manager->curlMulti);
}

View File

@ -8,6 +8,7 @@
#include <memory>
#include <mutex>
#include <condition_variable>
#include <optional>
namespace http
{
@ -42,7 +43,8 @@ namespace http
int statusCode = 0;
ByteString responseData;
std::vector<ByteString> responseHeaders;
ByteString error;
std::optional<ByteString> error;
std::optional<ByteString> failEarly;
RequestHandle(CtorTag)
{

View File

@ -5,6 +5,7 @@ client_files = files(
'ThumbnailRendererTask.cpp',
'Client.cpp',
'GameSave.cpp',
'User.cpp',
)
subdir('http')

View File

@ -63,6 +63,7 @@
#include "Config.h"
#include <SDL.h>
#include <iostream>
#ifdef GetUserName
# undef GetUserName // dammit windows
@ -699,6 +700,7 @@ bool GameController::KeyRelease(int key, int scan, bool repeat, bool shift, bool
void GameController::Tick()
{
gameModel->Tick();
if(firstTick)
{
commandInterface->Init();
@ -1441,16 +1443,9 @@ void GameController::FrameStep()
void GameController::Vote(int direction)
{
if (gameModel->GetSave() && gameModel->GetUser().UserID && gameModel->GetSave()->GetID())
{
try
{
gameModel->SetVote(direction);
}
catch(GameModelException & ex)
{
new ErrorMessage("Error while voting", ByteString(ex.what()).FromUtf8());
}
}
}
void GameController::ChangeBrush()
@ -1536,7 +1531,7 @@ void GameController::NotifyAuthUserChanged(Client * sender)
gameModel->SetUser(newUser);
}
void GameController::NotifyNewNotification(Client * sender, std::pair<String, ByteString> notification)
void GameController::NotifyNewNotification(Client * sender, ServerNotification notification)
{
class LinkNotification : public Notification
{
@ -1550,7 +1545,7 @@ void GameController::NotifyNewNotification(Client * sender, std::pair<String, By
Platform::OpenURI(link);
}
};
gameModel->AddNotification(new LinkNotification(notification.second, notification.first));
gameModel->AddNotification(new LinkNotification(notification.link, notification.text));
}
void GameController::NotifyUpdateAvailable(Client * sender)
@ -1564,7 +1559,13 @@ void GameController::NotifyUpdateAvailable(Client * sender)
void Action() override
{
UpdateInfo info = Client::Ref().GetUpdateInfo();
auto optinfo = Client::Ref().GetUpdateInfo();
if (!optinfo.has_value())
{
std::cerr << "odd, the update has disappeared" << std::endl;
return;
}
UpdateInfo info = optinfo.value();
StringBuilder updateMessage;
if (Platform::CanUpdate())
{
@ -1593,36 +1594,41 @@ void GameController::NotifyUpdateAvailable(Client * sender)
}
updateMessage << "\nNew version:\n ";
if (info.Type == UpdateInfo::Beta)
if (info.channel == UpdateInfo::channelBeta)
{
updateMessage << info.Major << "." << info.Minor << " Beta, Build " << info.Build;
updateMessage << info.major << "." << info.minor << " Beta, Build " << info.build;
}
else if (info.Type == UpdateInfo::Snapshot)
else if (info.channel == UpdateInfo::channelSnapshot)
{
if constexpr (MOD)
{
updateMessage << "Mod version " << info.Time;
updateMessage << "Mod version " << info.build;
}
else
{
updateMessage << "Snapshot " << info.Time;
updateMessage << "Snapshot " << info.build;
}
}
else if(info.Type == UpdateInfo::Stable)
else if(info.channel == UpdateInfo::channelStable)
{
updateMessage << info.Major << "." << info.Minor << " Stable, Build " << info.Build;
updateMessage << info.major << "." << info.minor << " Stable, Build " << info.build;
}
if (info.Changelog.length())
updateMessage << "\n\nChangelog:\n" << info.Changelog;
if (info.changeLog.length())
updateMessage << "\n\nChangelog:\n" << info.changeLog;
new ConfirmPrompt("Run Updater", updateMessage.Build(), { [this] { c->RunUpdater(); } });
new ConfirmPrompt("Run Updater", updateMessage.Build(), { [this, info] { c->RunUpdater(info); } });
}
};
switch(sender->GetUpdateInfo().Type)
auto optinfo = sender->GetUpdateInfo();
if (!optinfo.has_value())
{
case UpdateInfo::Snapshot:
return;
}
switch(optinfo.value().channel)
{
case UpdateInfo::channelSnapshot:
if constexpr (MOD)
{
gameModel->AddNotification(new UpdateNotification(this, "A new mod update is available - click here to update"));
@ -1632,10 +1638,10 @@ void GameController::NotifyUpdateAvailable(Client * sender)
gameModel->AddNotification(new UpdateNotification(this, "A new snapshot is available - click here to update"));
}
break;
case UpdateInfo::Stable:
case UpdateInfo::channelStable:
gameModel->AddNotification(new UpdateNotification(this, "A new version is available - click here to update"));
break;
case UpdateInfo::Beta:
case UpdateInfo::channelBeta:
gameModel->AddNotification(new UpdateNotification(this, "A new beta is available - click here to update"));
break;
}
@ -1646,25 +1652,16 @@ void GameController::RemoveNotification(Notification * notification)
gameModel->RemoveNotification(notification);
}
void GameController::RunUpdater()
void GameController::RunUpdater(UpdateInfo info)
{
if (Platform::CanUpdate())
{
Exit();
new UpdateActivity();
new UpdateActivity(info);
}
else
{
ByteString file;
if constexpr (USE_UPDATESERVER)
{
file = ByteString::Build(SCHEME, UPDATESERVER, Client::Ref().GetUpdateInfo().File);
}
else
{
file = ByteString::Build(SCHEME, SERVER, Client::Ref().GetUpdateInfo().File);
}
Platform::OpenURI(file);
Platform::OpenURI(info.file);
}
}

View File

@ -1,5 +1,6 @@
#pragma once
#include "client/ClientListener.h"
#include "client/StartupInfo.h"
#include "gui/interface/Point.h"
#include "gui/interface/Colour.h"
#include "simulation/Sign.h"
@ -181,8 +182,8 @@ public:
void NotifyUpdateAvailable(Client * sender) override;
void NotifyAuthUserChanged(Client * sender) override;
void NotifyNewNotification(Client * sender, std::pair<String, ByteString> notification) override;
void RunUpdater();
void NotifyNewNotification(Client * sender, ServerNotification notification) override;
void RunUpdater(UpdateInfo info);
bool GetMouseClickRequired();
void RemoveCustomGOLType(const ByteString &identifier);

View File

@ -17,6 +17,7 @@
#include "client/GameSave.h"
#include "client/SaveFile.h"
#include "client/SaveInfo.h"
#include "client/http/ExecVoteRequest.h"
#include "common/platform/Platform.h"
#include "graphics/Renderer.h"
#include "simulation/Air.h"
@ -30,6 +31,7 @@
#include "simulation/ToolClasses.h"
#include "gui/game/DecorationTool.h"
#include "gui/interface/Engine.h"
#include "gui/dialogues/ErrorMessage.h"
#include <iostream>
#include <algorithm>
#include <optional>
@ -780,18 +782,33 @@ void GameModel::SetUndoHistoryLimit(unsigned int undoHistoryLimit_)
void GameModel::SetVote(int direction)
{
if(currentSave)
queuedVote = direction;
}
void GameModel::Tick()
{
RequestStatus status = Client::Ref().ExecVote(currentSave->GetID(), direction);
if(status == RequestOkay)
if (execVoteRequest && execVoteRequest->CheckDone())
{
currentSave->vote = direction;
try
{
execVoteRequest->Finish();
currentSave->vote = execVoteRequest->Direction();
notifySaveChanged();
}
else
catch (const http::RequestError &ex)
{
throw GameModelException("Could not vote: "+Client::Ref().GetLastError());
new ErrorMessage("Error while voting", ByteString(ex.what()).FromUtf8());
}
execVoteRequest.reset();
}
if (!execVoteRequest && queuedVote)
{
if (currentSave)
{
execVoteRequest = std::make_unique<http::ExecVoteRequest>(currentSave->GetID(), *queuedVote);
execVoteRequest->Start();
}
queuedVote.reset();
}
}

View File

@ -5,6 +5,7 @@
#include <vector>
#include <deque>
#include <memory>
#include <optional>
class Menu;
class Tool;
@ -21,6 +22,11 @@ class Snapshot;
struct SnapshotDelta;
class GameSave;
namespace http
{
class ExecVoteRequest;
};
class ToolSelection
{
public:
@ -40,6 +46,8 @@ struct HistoryEntry
class GameModel
{
std::unique_ptr<http::ExecVoteRequest> execVoteRequest;
private:
std::vector<Notification*> notifications;
//int clipboardSize;
@ -119,10 +127,14 @@ private:
void SaveToSimParameters(const GameSave &saveData);
std::optional<int> queuedVote;
public:
GameModel();
~GameModel();
void Tick();
void SetEdgeMode(int edgeMode);
int GetEdgeMode();
void SetTemperatureScale(int temperatureScale);

View File

@ -1426,8 +1426,7 @@ void GameView::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl,
c->ReloadSim();
break;
case SDL_SCANCODE_A:
if ((Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator
|| Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin) && ctrl)
if (Client::Ref().GetAuthUser().UserElevation != User::ElevationNone && ctrl)
{
ByteString authorString = Client::Ref().GetAuthorInfo().toStyledString();
new InformationMessage("Save authorship info", authorString.FromUtf8(), true);

View File

@ -1,7 +1,7 @@
#include "LoginController.h"
#include "client/Client.h"
#include "client/http/LoginRequest.h"
#include "client/http/LogoutRequest.h"
#include "LoginView.h"
#include "LoginModel.h"
#include "Controller.h"
@ -23,15 +23,19 @@ void LoginController::Login(ByteString username, ByteString password)
loginModel->Login(username, password);
}
User LoginController::GetUser()
void LoginController::Logout()
{
return loginModel->GetUser();
loginModel->Logout();
}
void LoginController::Tick()
{
loginModel->Tick();
}
void LoginController::Exit()
{
loginView->CloseActiveWindow();
Client::Ref().SetAuthUser(loginModel->GetUser());
if (onDone)
onDone();
HasExited = true;

View File

@ -14,8 +14,9 @@ public:
bool HasExited;
LoginController(std::function<void ()> onDone = nullptr);
void Login(ByteString username, ByteString password);
void Logout();
void Tick();
void Exit();
LoginView * GetView() { return loginView; }
User GetUser();
virtual ~LoginController();
~LoginController();
};

View File

@ -1,39 +1,32 @@
#include "LoginModel.h"
#include "LoginView.h"
#include "client/Client.h"
LoginModel::LoginModel():
currentUser(0, "")
{
}
#include "client/http/LoginRequest.h"
#include "client/http/LogoutRequest.h"
void LoginModel::Login(ByteString username, ByteString password)
{
if (username.Contains("@"))
{
statusText = "Use your Powder Toy account to log in, not your email. If you don't have a Powder Toy account, you can create one at https://powdertoy.co.uk/Register.html";
loginStatus = false;
loginStatus = loginIdle;
notifyStatusChanged();
return;
}
statusText = "Logging in...";
loginStatus = false;
loginStatus = loginWorking;
notifyStatusChanged();
LoginStatus status = Client::Ref().Login(username, password, currentUser);
switch(status)
{
case LoginOkay:
statusText = "Logged in";
loginStatus = true;
break;
case LoginError:
statusText = Client::Ref().GetLastError();
break;
loginRequest = std::make_unique<http::LoginRequest>(username, password);
loginRequest->Start();
}
void LoginModel::Logout()
{
statusText = "Logging out...";
loginStatus = loginWorking;
notifyStatusChanged();
logoutRequest = std::make_unique<http::LogoutRequest>();
logoutRequest->Start();
}
void LoginModel::AddObserver(LoginView * observer)
@ -46,14 +39,47 @@ String LoginModel::GetStatusText()
return statusText;
}
User LoginModel::GetUser()
void LoginModel::Tick()
{
return currentUser;
if (loginRequest && loginRequest->CheckDone())
{
try
{
auto info = loginRequest->Finish();
auto &client = Client::Ref();
client.SetAuthUser(info.user);
for (auto &item : info.notifications)
{
client.AddServerNotification(item);
}
bool LoginModel::GetStatus()
statusText = "Logged in";
loginStatus = loginSucceeded;
}
catch (const http::RequestError &ex)
{
return loginStatus;
statusText = ByteString(ex.what()).FromUtf8();
loginStatus = loginIdle;
}
notifyStatusChanged();
loginRequest.reset();
}
if (logoutRequest && logoutRequest->CheckDone())
{
try
{
logoutRequest->Finish();
auto &client = Client::Ref();
client.SetAuthUser(User(0, ""));
statusText = "Logged out";
}
catch (const http::RequestError &ex)
{
statusText = ByteString(ex.what()).FromUtf8();
}
loginStatus = loginIdle;
notifyStatusChanged();
logoutRequest.reset();
}
}
void LoginModel::notifyStatusChanged()
@ -64,6 +90,7 @@ void LoginModel::notifyStatusChanged()
}
}
LoginModel::~LoginModel() {
LoginModel::~LoginModel()
{
// Satisfy std::unique_ptr
}

View File

@ -2,21 +2,41 @@
#include "common/String.h"
#include "client/User.h"
#include <vector>
#include <memory>
namespace http
{
class LoginRequest;
class LogoutRequest;
}
enum LoginStatus
{
loginIdle,
loginWorking,
loginSucceeded,
};
class LoginView;
class LoginModel
{
std::unique_ptr<http::LoginRequest> loginRequest;
std::unique_ptr<http::LogoutRequest> logoutRequest;
std::vector<LoginView*> observers;
String statusText;
bool loginStatus;
LoginStatus loginStatus = loginIdle;
void notifyStatusChanged();
User currentUser;
public:
LoginModel();
void Login(ByteString username, ByteString password);
void Logout();
void AddObserver(LoginView * observer);
String GetStatusText();
bool GetStatus();
LoginStatus GetStatus() const
{
return loginStatus;
}
void Tick();
User GetUser();
virtual ~LoginModel();
~LoginModel();
};

View File

@ -1,18 +1,13 @@
#include "LoginView.h"
#include "LoginModel.h"
#include "LoginController.h"
#include "graphics/Graphics.h"
#include "gui/interface/Button.h"
#include "gui/interface/Label.h"
#include "gui/interface/Textbox.h"
#include "gui/Style.h"
#include "client/Client.h"
#include "Misc.h"
#include <SDL.h>
LoginView::LoginView():
@ -39,11 +34,15 @@ LoginView::LoginView():
loginButton->Appearance.HorizontalAlign = ui::Appearance::AlignRight;
loginButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
loginButton->Appearance.TextInactive = style::Colour::ConfirmButton;
loginButton->SetActionCallback({ [this] { c->Login(usernameField->GetText().ToUtf8(), passwordField->GetText().ToUtf8()); } });
loginButton->SetActionCallback({ [this] {
c->Login(usernameField->GetText().ToUtf8(), passwordField->GetText().ToUtf8());
} });
AddComponent(cancelButton);
cancelButton->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
cancelButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
cancelButton->SetActionCallback({ [this] { c->Exit(); } });
cancelButton->SetActionCallback({ [this] {
c->Logout();
} });
AddComponent(titleLabel);
titleLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
titleLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
@ -85,12 +84,17 @@ void LoginView::NotifyStatusChanged(LoginModel * sender)
targetSize.Y = 87;
infoLabel->SetText(sender->GetStatusText());
infoLabel->AutoHeight();
auto notWorking = sender->GetStatus() != loginWorking;
loginButton->Enabled = notWorking;
cancelButton->Enabled = notWorking && Client::Ref().GetAuthUser().UserID;
usernameField->Enabled = notWorking;
passwordField->Enabled = notWorking;
if (sender->GetStatusText().length())
{
targetSize.Y += infoLabel->Size.Y+2;
infoLabel->Visible = true;
}
if(sender->GetStatus())
if (sender->GetStatus() == loginSucceeded)
{
c->Exit();
}
@ -98,6 +102,7 @@ void LoginView::NotifyStatusChanged(LoginModel * sender)
void LoginView::OnTick(float dt)
{
c->Tick();
//if(targetSize != Size)
{
ui::Point difference = targetSize-Size;

View File

@ -1,24 +0,0 @@
#pragma once
#include "common/String.h"
class SaveComment
{
public:
int authorID;
ByteString authorName;
ByteString authorNameFormatted;
String comment;
SaveComment(int userID, ByteString username, ByteString usernameFormatted, String commentText):
authorID(userID), authorName(username), authorNameFormatted(usernameFormatted), comment(commentText)
{
}
SaveComment(const SaveComment & comment):
authorID(comment.authorID), authorName(comment.authorName), authorNameFormatted(comment.authorNameFormatted), comment(comment.comment)
{
}
SaveComment(const SaveComment * comment):
authorID(comment->authorID), authorName(comment->authorName), authorNameFormatted(comment->authorNameFormatted), comment(comment->comment)
{
}
};

View File

@ -6,6 +6,10 @@
#include "client/Client.h"
#include "client/SaveInfo.h"
#include "client/GameSave.h"
#include "client/http/GetSaveRequest.h"
#include "client/http/GetSaveDataRequest.h"
#include "client/http/GetCommentsRequest.h"
#include "client/http/FavouriteSaveRequest.h"
#include "common/platform/Platform.h"
#include "graphics/Graphics.h"
#include "gui/dialogues/ErrorMessage.h"
@ -51,30 +55,6 @@ void PreviewController::Update()
}
}
bool PreviewController::SubmitComment(String comment)
{
if(comment.length() < 4)
{
new ErrorMessage("Error", "Comment is too short");
return false;
}
else
{
RequestStatus status = Client::Ref().AddComment(saveId, comment);
if(status != RequestOkay)
{
new ErrorMessage("Error submitting comment", Client::Ref().GetLastError());
return false;
}
else
{
previewModel->CommentAdded();
previewModel->UpdateComments(1);
}
}
return true;
}
void PreviewController::ShowLogin()
{
loginWindow = new LoginController();
@ -106,32 +86,11 @@ void PreviewController::DoOpen()
previewModel->SetDoOpen(true);
}
void PreviewController::Report(String message)
{
if(Client::Ref().ReportSave(saveId, message) == RequestOkay)
{
Exit();
new InformationMessage("Information", "Report submitted", false);
}
else
new ErrorMessage("Error", "Unable to file report: " + Client::Ref().GetLastError());
}
void PreviewController::FavouriteSave()
{
if (previewModel->GetSaveInfo() && Client::Ref().GetAuthUser().UserID)
{
try
{
if(previewModel->GetSaveInfo()->Favourite)
previewModel->SetFavourite(false);
else
previewModel->SetFavourite(true);
}
catch (PreviewModelException & e)
{
new ErrorMessage("Error", ByteString(e.what()).FromUtf8());
}
previewModel->SetFavourite(!previewModel->GetSaveInfo()->Favourite);
}
}
@ -161,6 +120,12 @@ bool PreviewController::PrevCommentPage()
return false;
}
void PreviewController::CommentAdded()
{
previewModel->CommentAdded();
previewModel->UpdateComments(1);
}
void PreviewController::Exit()
{
previewView->CloseActiveWindow();

View File

@ -23,7 +23,6 @@ public:
void Exit();
void DoOpen();
void OpenInBrowser();
void Report(String message);
void ShowLogin();
bool GetDoOpen();
const SaveInfo *GetSaveInfo() const;
@ -31,10 +30,10 @@ public:
PreviewView * GetView() { return previewView; }
void Update();
void FavouriteSave();
bool SubmitComment(String comment);
bool NextCommentPage();
bool PrevCommentPage();
void CommentAdded();
virtual ~PreviewController();
};

View File

@ -1,46 +1,27 @@
#include "PreviewModel.h"
#include "client/http/Request.h"
#include "client/http/GetSaveDataRequest.h"
#include "client/http/GetSaveRequest.h"
#include "client/http/GetCommentsRequest.h"
#include "client/http/FavouriteSaveRequest.h"
#include "Format.h"
#include "Misc.h"
#include "client/Client.h"
#include "client/GameSave.h"
#include "client/SaveInfo.h"
#include "gui/dialogues/ErrorMessage.h"
#include "gui/preview/Comment.h"
#include "PreviewModelException.h"
#include "PreviewView.h"
#include "Config.h"
#include <cmath>
#include <iostream>
PreviewModel::PreviewModel():
doOpen(false),
canOpen(true),
saveData(NULL),
saveComments(NULL),
commentBoxEnabled(false),
commentsLoaded(false),
commentsTotal(0),
commentsPageNumber(1)
{
}
PreviewModel::~PreviewModel()
{
delete saveData;
ClearComments();
}
constexpr auto commentsPerPage = 20;
void PreviewModel::SetFavourite(bool favourite)
{
if (saveInfo)
{
if (Client::Ref().FavouriteSave(saveInfo->id, favourite) == RequestOkay)
saveInfo->Favourite = favourite;
else if (favourite)
throw PreviewModelException("Error, could not fav. the save: " + Client::Ref().GetLastError());
else
throw PreviewModelException("Error, could not unfav. the save: " + Client::Ref().GetLastError());
notifySaveChanged();
queuedFavourite = favourite;
}
}
@ -64,37 +45,22 @@ void PreviewModel::UpdateSave(int saveID, int saveDate)
this->saveDate = saveDate;
saveInfo.reset();
if (saveData)
{
delete saveData;
saveData = NULL;
}
ClearComments();
saveData.reset();
saveComments.reset();
notifySaveChanged();
notifySaveCommentsChanged();
ByteString url;
if (saveDate)
url = ByteString::Build(STATICSCHEME, STATICSERVER, "/", saveID, "_", saveDate, ".cps");
else
url = ByteString::Build(STATICSCHEME, STATICSERVER, "/", saveID, ".cps");
saveDataDownload = std::make_unique<http::Request>(url);
saveDataDownload = std::make_unique<http::GetSaveDataRequest>(saveID, saveDate);
saveDataDownload->Start();
url = ByteString::Build(SCHEME, SERVER , "/Browse/View.json?ID=", saveID);
if (saveDate)
url += ByteString::Build("&Date=", saveDate);
saveInfoDownload = std::make_unique<http::Request>(url);
saveInfoDownload->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID);
saveInfoDownload = std::make_unique<http::GetSaveRequest>(saveID, saveDate);
saveInfoDownload->Start();
if (!GetDoOpen())
{
commentsLoaded = false;
url = ByteString::Build(SCHEME, SERVER, "/Browse/Comments.json?ID=", saveID, "&Start=", (commentsPageNumber-1)*20, "&Count=20");
commentsDownload = std::make_unique<http::Request>(url);
commentsDownload->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID);
commentsDownload = std::make_unique<http::GetCommentsRequest>(saveID, (commentsPageNumber - 1) * commentsPerPage, commentsPerPage);
commentsDownload->Start();
}
}
@ -131,7 +97,7 @@ int PreviewModel::GetCommentsPageNum()
int PreviewModel::GetCommentsPageCount()
{
return std::max(1, (int)(ceil(commentsTotal/20.0f)));
return std::max(1, ceilDiv(commentsTotal, commentsPerPage).first);
}
bool PreviewModel::GetCommentsLoaded()
@ -144,14 +110,12 @@ void PreviewModel::UpdateComments(int pageNumber)
if (commentsLoaded)
{
commentsLoaded = false;
ClearComments();
saveComments.reset();
commentsPageNumber = pageNumber;
if (!GetDoOpen())
{
ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/Comments.json?ID=", saveID, "&Start=", (commentsPageNumber-1)*20, "&Count=20");
commentsDownload = std::make_unique<http::Request>(url);
commentsDownload->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID);
commentsDownload = std::make_unique<http::GetCommentsRequest>(saveID, (commentsPageNumber - 1) * commentsPerPage, commentsPerPage);
commentsDownload->Start();
}
@ -189,175 +153,105 @@ void PreviewModel::OnSaveReady()
notifySaveCommentsChanged();
}
void PreviewModel::ClearComments()
{
if (saveComments)
{
for (size_t i = 0; i < saveComments->size(); i++)
delete saveComments->at(i);
saveComments->clear();
delete saveComments;
saveComments = NULL;
}
}
bool PreviewModel::ParseSaveInfo(ByteString &saveInfoResponse)
{
saveInfo.reset();
try // how does this differ from Client::GetSave?
{
std::istringstream dataStream(saveInfoResponse);
Json::Value objDocument;
dataStream >> objDocument;
int tempID = objDocument["ID"].asInt();
int tempScoreUp = objDocument["ScoreUp"].asInt();
int tempScoreDown = objDocument["ScoreDown"].asInt();
int tempMyScore = objDocument["ScoreMine"].asInt();
ByteString tempUsername = objDocument["Username"].asString();
String tempName = ByteString(objDocument["Name"].asString()).FromUtf8();
String tempDescription = ByteString(objDocument["Description"].asString()).FromUtf8();
int tempCreatedDate = objDocument["DateCreated"].asInt();
int tempUpdatedDate = objDocument["Date"].asInt();
bool tempPublished = objDocument["Published"].asBool();
bool tempFavourite = objDocument["Favourite"].asBool();
int tempComments = objDocument["Comments"].asInt();
int tempViews = objDocument["Views"].asInt();
int tempVersion = objDocument["Version"].asInt();
Json::Value tagsArray = objDocument["Tags"];
std::list<ByteString> tempTags;
for (Json::UInt j = 0; j < tagsArray.size(); j++)
tempTags.push_back(tagsArray[j].asString());
auto newSaveInfo = std::make_unique<SaveInfo>(tempID, tempCreatedDate, tempUpdatedDate, tempScoreUp,
tempScoreDown, tempMyScore, tempUsername, tempName,
tempDescription, tempPublished, tempTags);
newSaveInfo->Comments = tempComments;
newSaveInfo->Favourite = tempFavourite;
newSaveInfo->Views = tempViews;
newSaveInfo->Version = tempVersion;
// This is a workaround for a bug on the TPT server where the wrong 404 save is returned
// Redownload the .cps file for a fixed version of the 404 save
if (tempID == 404 && this->saveID != 404)
{
delete saveData;
saveData = NULL;
saveDataDownload = std::make_unique<http::Request>(ByteString::Build(STATICSCHEME, STATICSERVER, "/2157797.cps"));
saveDataDownload->Start();
}
saveInfo = std::move(newSaveInfo);
}
catch (std::exception &e)
{
return false;
}
return true;
}
bool PreviewModel::ParseComments(ByteString &commentsResponse)
{
ClearComments();
saveComments = new std::vector<SaveComment*>();
try
{
std::istringstream dataStream(commentsResponse);
Json::Value commentsArray;
dataStream >> commentsArray;
for (Json::UInt j = 0; j < commentsArray.size(); j++)
{
int userID = ByteString(commentsArray[j]["UserID"].asString()).ToNumber<int>();
ByteString username = commentsArray[j]["Username"].asString();
ByteString formattedUsername = commentsArray[j]["FormattedUsername"].asString();
if (formattedUsername == "jacobot")
formattedUsername = "\bt" + formattedUsername;
String comment = ByteString(commentsArray[j]["Text"].asString()).FromUtf8();
saveComments->push_back(new SaveComment(userID, username, formattedUsername, comment));
}
return true;
}
catch (std::exception &e)
{
return false;
}
}
void PreviewModel::Update()
{
auto triggerOnSaveReady = false;
if (saveDataDownload && saveDataDownload->CheckDone())
{
auto [ status, ret ] = saveDataDownload->Finish();
ByteString nothing;
Client::Ref().ParseServerReturn(nothing, status, true);
if (status == 200 && ret.size())
try
{
delete saveData;
saveData = new std::vector<char>(ret.begin(), ret.end());
if (saveInfo && saveData)
OnSaveReady();
saveData = saveDataDownload->Finish();
triggerOnSaveReady = true;
}
else
catch (const http::RequestError &ex)
{
auto why = ByteString(ex.what()).FromUtf8();
for (size_t i = 0; i < observers.size(); i++)
{
observers[i]->SaveLoadingError(Client::Ref().GetLastError());
observers[i]->SaveLoadingError(why);
}
}
saveDataDownload.reset();
}
if (saveInfoDownload && saveInfoDownload->CheckDone())
{
auto [ status, ret ] = saveInfoDownload->Finish();
ByteString nothing;
Client::Ref().ParseServerReturn(nothing, status, true);
if (status == 200 && ret.size())
try
{
if (ParseSaveInfo(ret))
saveInfo = saveInfoDownload->Finish();
triggerOnSaveReady = true;
// This is a workaround for a bug on the TPT server where the wrong 404 save is returned
// Redownload the .cps file for a fixed version of the 404 save
if (saveInfo->GetID() == 404 && saveID != 404)
{
if (saveInfo && saveData)
OnSaveReady();
saveData.reset();
saveDataDownload = std::make_unique<http::GetSaveDataRequest>(2157797, 0);
saveDataDownload->Start();
}
else
}
catch (const http::RequestError &ex)
{
auto why = ByteString(ex.what()).FromUtf8();
for (size_t i = 0; i < observers.size(); i++)
observers[i]->SaveLoadingError("Could not parse save info");
}
}
else
{
for (size_t i = 0; i < observers.size(); i++)
observers[i]->SaveLoadingError(Client::Ref().GetLastError());
observers[i]->SaveLoadingError(why);
}
}
saveInfoDownload.reset();
}
if (triggerOnSaveReady && saveInfo && saveData)
{
OnSaveReady();
}
if (commentsDownload && commentsDownload->CheckDone())
{
auto [ status, ret ] = commentsDownload->Finish();
ClearComments();
ByteString nothing;
Client::Ref().ParseServerReturn(nothing, status, true);
if (status == 200 && ret.size())
ParseComments(ret);
try
{
saveComments = commentsDownload->Finish();
}
catch (const http::RequestError &ex)
{
// TODO: handle
}
commentsLoaded = true;
notifySaveCommentsChanged();
notifyCommentsPageChanged();
commentsDownload.reset();
}
}
std::vector<SaveComment*> * PreviewModel::GetComments()
if (favouriteSaveRequest && favouriteSaveRequest->CheckDone())
{
return saveComments;
try
{
favouriteSaveRequest->Finish();
if (saveInfo)
{
saveInfo->Favourite = favouriteSaveRequest->Favourite();
notifySaveChanged();
}
}
catch (const http::RequestError &ex)
{
if (favouriteSaveRequest->Favourite())
{
throw PreviewModelException("Error, could not fav. the save: " + ByteString(ex.what()).FromUtf8());
}
else
{
throw PreviewModelException("Error, could not unfav. the save: " + ByteString(ex.what()).FromUtf8());
}
}
favouriteSaveRequest.reset();
}
if (!favouriteSaveRequest && queuedFavourite)
{
if (saveInfo)
{
favouriteSaveRequest = std::make_unique<http::FavouriteSaveRequest>(saveInfo->id, *queuedFavourite);
favouriteSaveRequest->Start();
}
queuedFavourite.reset();
}
}
void PreviewModel::notifySaveChanged()

View File

@ -1,47 +1,54 @@
#pragma once
#include "common/String.h"
#include "client/Comment.h"
#include <vector>
#include <memory>
#include <optional>
namespace http
{
class Request;
class GetSaveDataRequest;
class GetSaveRequest;
class GetCommentsRequest;
class FavouriteSaveRequest;
}
class PreviewView;
class SaveInfo;
class SaveComment;
class PreviewModel
{
bool doOpen;
bool canOpen;
bool doOpen = false;
bool canOpen = true;
std::vector<PreviewView*> observers;
std::unique_ptr<SaveInfo> saveInfo;
std::vector<char> * saveData;
std::vector<SaveComment*> * saveComments;
std::optional<std::vector<char>> saveData;
std::optional<std::vector<Comment>> saveComments;
void notifySaveChanged();
void notifySaveCommentsChanged();
void notifyCommentsPageChanged();
void notifyCommentBoxEnabledChanged();
std::unique_ptr<http::Request> saveDataDownload;
std::unique_ptr<http::Request> saveInfoDownload;
std::unique_ptr<http::Request> commentsDownload;
std::unique_ptr<http::GetSaveDataRequest> saveDataDownload;
std::unique_ptr<http::GetSaveRequest> saveInfoDownload;
std::unique_ptr<http::GetCommentsRequest> commentsDownload;
std::unique_ptr<http::FavouriteSaveRequest> favouriteSaveRequest;
int saveID;
int saveDate;
bool commentBoxEnabled;
bool commentsLoaded;
int commentsTotal;
int commentsPageNumber;
bool commentBoxEnabled = false;
bool commentsLoaded = false;
int commentsTotal = 0;
int commentsPageNumber = 1;
std::optional<bool> queuedFavourite;
public:
PreviewModel();
~PreviewModel();
const SaveInfo *GetSaveInfo() const;
std::unique_ptr<SaveInfo> TakeSaveInfo();
std::vector<SaveComment*> * GetComments();
const std::vector<Comment> *GetComments() const
{
return saveComments ? &*saveComments : nullptr;
}
bool GetCommentBoxEnabled();
void SetCommentBoxEnabled(bool enabledState);
@ -59,7 +66,6 @@ public:
bool GetCanOpen();
void SetDoOpen(bool doOpen);
void Update();
void ClearComments();
void OnSaveReady();
bool ParseSaveInfo(ByteString &saveInfoResponse);
bool ParseComments(ByteString &commentsResponse);

View File

@ -4,6 +4,8 @@
#include "client/Client.h"
#include "client/SaveInfo.h"
#include "client/http/AddCommentRequest.h"
#include "client/http/ReportSaveRequest.h"
#include "gui/dialogues/TextPrompt.h"
#include "gui/profile/ProfileActivity.h"
@ -17,12 +19,12 @@
#include "gui/interface/Textbox.h"
#include "gui/interface/Engine.h"
#include "gui/dialogues/ErrorMessage.h"
#include "gui/dialogues/InformationMessage.h"
#include "gui/interface/Point.h"
#include "gui/interface/Window.h"
#include "gui/Style.h"
#include "common/tpt-rand.h"
#include "Comment.h"
#include "Format.h"
#include "Misc.h"
@ -57,6 +59,7 @@ PreviewView::PreviewView(std::unique_ptr<VideoBuffer> newSavePreview):
favButton = new ui::Button(ui::Point(50, Size.Y-19), ui::Point(51, 19), "Fav");
favButton->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
favButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
favButton->SetTogglable(true);
favButton->SetIcon(IconFavourite);
favButton->SetActionCallback({ [this] { c->FavouriteSave(); } });
favButton->Enabled = Client::Ref().GetAuthUser().UserID?true:false;
@ -68,7 +71,12 @@ PreviewView::PreviewView(std::unique_ptr<VideoBuffer> newSavePreview):
reportButton->SetIcon(IconReport);
reportButton->SetActionCallback({ [this] {
new TextPrompt("Report Save", "Things to consider when reporting:\n\bw1)\bg When reporting stolen saves, please include the ID of the original save.\n\bw2)\bg Do not ask for saves to be removed from front page unless they break the rules.\n\bw3)\bg You may report saves for comments or tags too (including your own saves)", "", "[reason]", true, { [this](String const &resultText) {
c->Report(resultText);
if (reportSaveRequest)
{
return;
}
reportSaveRequest = std::make_unique<http::ReportSaveRequest>(c->SaveID(), resultText);
reportSaveRequest->Start();
} });
} });
reportButton->Enabled = Client::Ref().GetAuthUser().UserID?true:false;
@ -222,7 +230,12 @@ void PreviewView::CheckComment()
if (!commentWarningLabel)
return;
String text = addCommentBox->GetText().ToLower();
if (!userIsAuthor && (text.Contains("stolen") || text.Contains("copied")))
if (addCommentRequest)
{
commentWarningLabel->SetText("Submitting comment...");
commentHelpText = true;
}
else if (!userIsAuthor && (text.Contains("stolen") || text.Contains("copied")))
{
if (!commentHelpText)
{
@ -375,6 +388,37 @@ void PreviewView::OnTick(float dt)
ErrorMessage::Blocking("Error loading save", doErrorMessage);
c->Exit();
}
if (reportSaveRequest && reportSaveRequest->CheckDone())
{
try
{
reportSaveRequest->Finish();
c->Exit();
new InformationMessage("Information", "Report submitted", false);
}
catch (const http::RequestError &ex)
{
new ErrorMessage("Error", "Unable to file report: " + ByteString(ex.what()).FromUtf8());
}
reportSaveRequest.reset();
}
if (addCommentRequest && addCommentRequest->CheckDone())
{
try
{
addCommentBox->SetText("");
c->CommentAdded();
}
catch (const http::RequestError &ex)
{
new ErrorMessage("Error submitting comment", ByteString(ex.what()).FromUtf8());
}
submitCommentButton->Enabled = true;
commentBoxAutoHeight();
addCommentRequest.reset();
CheckComment();
}
}
void PreviewView::OnTryExit(ExitMethod method)
@ -448,16 +492,16 @@ void PreviewView::NotifySaveChanged(PreviewModel * sender)
if(save->Favourite)
{
favButton->Enabled = true;
favButton->SetText("Unfav");
favButton->SetToggleState(true);
}
else if(Client::Ref().GetAuthUser().UserID)
{
favButton->Enabled = true;
favButton->SetText("Fav");
favButton->SetToggleState(false);
}
else
{
favButton->SetText("Fav");
favButton->SetToggleState(false);
favButton->Enabled = false;
}
@ -477,6 +521,7 @@ void PreviewView::NotifySaveChanged(PreviewModel * sender)
saveNameLabel->SetText("");
authorDateLabel->SetText("");
saveDescriptionLabel->SetText("");
favButton->SetToggleState(false);
favButton->Enabled = false;
if (!sender->GetCanOpen())
openButton->Enabled = false;
@ -488,18 +533,19 @@ void PreviewView::submitComment()
if (addCommentBox)
{
String comment = addCommentBox->GetText();
if (comment.length() < 4)
{
new ErrorMessage("Error", "Comment is too short");
return;
}
submitCommentButton->Enabled = false;
addCommentBox->SetText("");
addCommentBox->SetPlaceholder("Submitting comment"); //This doesn't appear to ever show since no separate thread is created
FocusComponent(NULL);
if (!c->SubmitComment(comment))
addCommentBox->SetText(comment);
addCommentRequest = std::make_unique<http::AddCommentRequest>(c->SaveID(), comment);
addCommentRequest->Start();
addCommentBox->SetPlaceholder("Add comment");
submitCommentButton->Enabled = true;
commentBoxAutoHeight();
CheckComment();
}
}
@ -564,7 +610,7 @@ void PreviewView::NotifyCommentsPageChanged(PreviewModel * sender)
void PreviewView::NotifyCommentsChanged(PreviewModel * sender)
{
std::vector<SaveComment*> * comments = sender->GetComments();
auto commentsPtr = sender->GetComments();
for (size_t i = 0; i < commentComponents.size(); i++)
{
@ -575,8 +621,9 @@ void PreviewView::NotifyCommentsChanged(PreviewModel * sender)
commentTextComponents.clear();
commentsPanel->InnerSize = ui::Point(0, 0);
if (comments)
if (commentsPtr)
{
auto &comments = *commentsPtr;
for (size_t i = 0; i < commentComponents.size(); i++)
{
commentsPanel->RemoveChild(commentComponents[i]);
@ -589,11 +636,11 @@ void PreviewView::NotifyCommentsChanged(PreviewModel * sender)
ui::Label * tempUsername;
ui::Label * tempComment;
ui::AvatarButton * tempAvatar;
for (size_t i = 0; i < comments->size(); i++)
for (size_t i = 0; i < comments.size(); i++)
{
if (showAvatars)
{
tempAvatar = new ui::AvatarButton(ui::Point(2, currentY+7), ui::Point(26, 26), comments->at(i)->authorName);
tempAvatar = new ui::AvatarButton(ui::Point(2, currentY+7), ui::Point(26, 26), comments[i].authorName);
tempAvatar->SetActionCallback({ [tempAvatar] {
if (tempAvatar->GetUsername().size() > 0)
{
@ -604,25 +651,38 @@ void PreviewView::NotifyCommentsChanged(PreviewModel * sender)
commentsPanel->AddChild(tempAvatar);
}
auto authorNameFormatted = comments[i].authorName.FromUtf8();
if (comments[i].authorElevation != User::ElevationNone || comments[i].authorName == "jacobot")
{
authorNameFormatted = "\bt" + authorNameFormatted;
}
else if (comments[i].authorIsBanned)
{
authorNameFormatted = "\bg" + authorNameFormatted;
}
else if (Client::Ref().GetAuthUser().UserID && Client::Ref().GetAuthUser().Username == comments[i].authorName)
{
authorNameFormatted = "\bo" + authorNameFormatted;
}
else if (sender->GetSaveInfo() && sender->GetSaveInfo()->GetUserName() == comments[i].authorName)
{
authorNameFormatted = "\bl" + authorNameFormatted;
}
if (showAvatars)
tempUsername = new ui::Label(ui::Point(31, currentY+3), ui::Point(Size.X-((XRES/2) + 13 + 26), 16), comments->at(i)->authorNameFormatted.FromUtf8());
tempUsername = new ui::Label(ui::Point(31, currentY+3), ui::Point(Size.X-((XRES/2) + 13 + 26), 16), authorNameFormatted);
else
tempUsername = new ui::Label(ui::Point(5, currentY+3), ui::Point(Size.X-((XRES/2) + 13), 16), comments->at(i)->authorNameFormatted.FromUtf8());
tempUsername = new ui::Label(ui::Point(5, currentY+3), ui::Point(Size.X-((XRES/2) + 13), 16), authorNameFormatted);
tempUsername->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
tempUsername->Appearance.VerticalAlign = ui::Appearance::AlignBottom;
if (Client::Ref().GetAuthUser().UserID && Client::Ref().GetAuthUser().Username == comments->at(i)->authorName)
tempUsername->SetTextColour(ui::Colour(255, 255, 100));
else if (sender->GetSaveInfo() && sender->GetSaveInfo()->GetUserName() == comments->at(i)->authorName)
tempUsername->SetTextColour(ui::Colour(255, 100, 100));
currentY += 16;
commentComponents.push_back(tempUsername);
commentsPanel->AddChild(tempUsername);
if (showAvatars)
tempComment = new ui::Label(ui::Point(31, currentY+5), ui::Point(Size.X-((XRES/2) + 13 + 26), -1), comments->at(i)->comment);
tempComment = new ui::Label(ui::Point(31, currentY+5), ui::Point(Size.X-((XRES/2) + 13 + 26), -1), comments[i].content);
else
tempComment = new ui::Label(ui::Point(5, currentY+5), ui::Point(Size.X-((XRES/2) + 13), -1), comments->at(i)->comment);
tempComment = new ui::Label(ui::Point(5, currentY+5), ui::Point(Size.X-((XRES/2) + 13), -1), comments[i].content);
tempComment->SetMultiline(true);
tempComment->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
tempComment->Appearance.VerticalAlign = ui::Appearance::AlignTop;

View File

@ -5,6 +5,12 @@
#include "common/String.h"
#include "gui/interface/Window.h"
namespace http
{
class AddCommentRequest;
class ReportSaveRequest;
}
namespace ui
{
class Button;
@ -64,6 +70,10 @@ class PreviewView: public ui::Window
void submitComment();
bool CheckSwearing(String text);
void CheckComment();
std::unique_ptr<http::AddCommentRequest> addCommentRequest;
std::unique_ptr<http::ReportSaveRequest> reportSaveRequest;
public:
void AttachController(PreviewController * controller);
PreviewView(std::unique_ptr<VideoBuffer> newSavePreviev);

View File

@ -1,5 +1,7 @@
#include "ProfileActivity.h"
#include "client/Client.h"
#include "client/http/SaveUserInfoRequest.h"
#include "client/http/GetUserInfoRequest.h"
#include "common/platform/Platform.h"
#include "gui/Style.h"
#include "gui/interface/AvatarButton.h"
@ -200,30 +202,30 @@ void ProfileActivity::OnTick(float dt)
if (saveUserInfoRequest && saveUserInfoRequest->CheckDone())
{
auto SaveUserInfoStatus = saveUserInfoRequest->Finish();
if (SaveUserInfoStatus)
try
{
saveUserInfoRequest->Finish();
Exit();
}
else
catch (const http::RequestError &ex)
{
doError = true;
doErrorMessage = "Could not save user info: " + Client::Ref().GetLastError();
doErrorMessage = "Could not save user info: " + ByteString(ex.what()).FromUtf8();
}
saveUserInfoRequest.reset();
}
if (getUserInfoRequest && getUserInfoRequest->CheckDone())
{
auto getUserInfoResult = getUserInfoRequest->Finish();
if (getUserInfoResult)
try
{
auto userInfo = getUserInfoRequest->Finish();
loading = false;
setUserInfo(*getUserInfoResult);
setUserInfo(userInfo);
}
else
catch (const http::RequestError &ex)
{
doError = true;
doErrorMessage = "Could not load user info: " + Client::Ref().GetLastError();
doErrorMessage = "Could not load user info: " + ByteString(ex.what()).FromUtf8();
}
getUserInfoRequest.reset();
}

View File

@ -2,10 +2,14 @@
#include "common/String.h"
#include "Activity.h"
#include "client/UserInfo.h"
#include "client/http/SaveUserInfoRequest.h"
#include "client/http/GetUserInfoRequest.h"
#include <memory>
namespace http
{
class SaveUserInfoRequest;
class GetUserInfoRequest;
}
namespace ui
{
class Label;

View File

@ -1,7 +1,5 @@
#include "ServerSaveActivity.h"
#include "graphics/Graphics.h"
#include "gui/interface/Label.h"
#include "gui/interface/Textbox.h"
#include "gui/interface/Button.h"
@ -10,13 +8,11 @@
#include "gui/dialogues/SaveIDMessage.h"
#include "gui/dialogues/ConfirmPrompt.h"
#include "gui/dialogues/InformationMessage.h"
#include "client/Client.h"
#include "client/ThumbnailRendererTask.h"
#include "client/GameSave.h"
#include "client/http/UploadSaveRequest.h"
#include "tasks/Task.h"
#include "gui/Style.h"
class SaveUploadTask: public Task
@ -36,7 +32,19 @@ class SaveUploadTask: public Task
bool doWork() override
{
notifyProgress(-1);
return Client::Ref().UploadSave(save) == RequestOkay;
auto uploadSaveRequest = std::make_unique<http::UploadSaveRequest>(save);
uploadSaveRequest->Start();
uploadSaveRequest->Wait();
try
{
save.SetID(uploadSaveRequest->Finish());
}
catch (const http::RequestError &ex)
{
notifyError(ByteString(ex.what()).FromUtf8());
return false;
}
return true;
}
public:
@ -168,7 +176,7 @@ void ServerSaveActivity::NotifyDone(Task * task)
if(!task->GetSuccess())
{
Exit();
new ErrorMessage("Error", Client::Ref().GetLastError());
new ErrorMessage("Error", task->GetError());
}
else
{
@ -182,26 +190,22 @@ void ServerSaveActivity::NotifyDone(Task * task)
void ServerSaveActivity::Save()
{
if(nameField->GetText().length())
if (!nameField->GetText().length())
{
new ErrorMessage("Error", "You must specify a save name.");
return;
}
if(Client::Ref().GetAuthUser().Username != save->GetUserName() && publishedCheckbox->GetChecked())
{
new ConfirmPrompt("Publish", "This save was created by " + save->GetUserName().FromUtf8() + ", you're about to publish this under your own name; If you haven't been given permission by the author to do so, please uncheck the publish box, otherwise continue", { [this] {
Exit();
saveUpload();
} });
}
else
{
Exit();
saveUpload();
}
}
else
{
new ErrorMessage("Error", "You must specify a save name.");
}
}
void ServerSaveActivity::AddAuthorInfo()
{
@ -223,6 +227,7 @@ void ServerSaveActivity::AddAuthorInfo()
void ServerSaveActivity::saveUpload()
{
okayButton->Enabled = false;
save->SetName(nameField->GetText());
save->SetDescription(descriptionField->GetText());
save->SetPublished(publishedCheckbox->GetChecked());
@ -234,16 +239,8 @@ void ServerSaveActivity::saveUpload()
save->SetGameSave(std::move(gameSave));
}
AddAuthorInfo();
if(Client::Ref().UploadSave(*save) != RequestOkay)
{
new ErrorMessage("Error", "Upload failed with error:\n"+Client::Ref().GetLastError());
}
else if (onUploaded)
{
new SaveIDMessage(save->GetID());
onUploaded(std::move(save));
}
uploadSaveRequest = std::make_unique<http::UploadSaveRequest>(*save);
uploadSaveRequest->Start();
}
void ServerSaveActivity::Exit()
@ -364,6 +361,26 @@ void ServerSaveActivity::OnTick(float dt)
}
}
if (uploadSaveRequest && uploadSaveRequest->CheckDone())
{
okayButton->Enabled = true;
try
{
save->SetID(uploadSaveRequest->Finish());
Exit();
new SaveIDMessage(save->GetID());
if (onUploaded)
{
onUploaded(std::move(save));
}
}
catch (const http::RequestError &ex)
{
new ErrorMessage("Error", "Upload failed with error:\n" + ByteString(ex.what()).FromUtf8());
}
uploadSaveRequest.reset();
}
if(saveUploadTask)
saveUploadTask->Poll();
}

View File

@ -13,6 +13,11 @@
#include "save_online.png.h"
namespace http
{
class UploadSaveRequest;
}
namespace ui
{
class Label;
@ -25,6 +30,8 @@ class Task;
class VideoBuffer;
class ServerSaveActivity: public WindowActivity, public TaskListener
{
std::unique_ptr<http::UploadSaveRequest> uploadSaveRequest;
using OnUploaded = std::function<void (std::unique_ptr<SaveInfo>)>;
std::unique_ptr<PlaneAdapter<std::vector<pixel_rgba>>> saveToServerImage = format::PixelsFromPNG(
std::vector<char>(save_online_png, save_online_png + save_online_png_size)

View File

@ -7,6 +7,12 @@
#include "client/Client.h"
#include "client/SaveInfo.h"
#include "client/GameSave.h"
#include "client/http/DeleteSaveRequest.h"
#include "client/http/PublishSaveRequest.h"
#include "client/http/UnpublishSaveRequest.h"
#include "client/http/FavouriteSaveRequest.h"
#include "client/http/SearchSavesRequest.h"
#include "client/http/SearchTagsRequest.h"
#include "common/platform/Platform.h"
#include "common/tpt-minmax.h"
#include "graphics/Graphics.h"
@ -132,13 +138,13 @@ void SearchController::SetPageRelative(int offset)
void SearchController::ChangeSort()
{
if(searchModel->GetSort() == "new")
if(searchModel->GetSort() == http::sortByDate)
{
searchModel->SetSort("best");
searchModel->SetSort(http::sortByVotes);
}
else
{
searchModel->SetSort("new");
searchModel->SetSort(http::sortByDate);
}
searchModel->UpdateSaveList(1, searchModel->GetLastQuery());
}
@ -183,7 +189,7 @@ void SearchController::SelectAllSaves()
if (!Client::Ref().GetAuthUser().UserID)
return;
if (searchModel->GetShowOwn() ||
Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator ||
Client::Ref().GetAuthUser().UserElevation == User::ElevationMod ||
Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin)
searchModel->SelectAllSaves();
@ -245,9 +251,16 @@ void SearchController::removeSelectedC()
for (size_t i = 0; i < saves.size(); i++)
{
notifyStatus(String::Build("Deleting save [", saves[i], "] ..."));
if (Client::Ref().DeleteSave(saves[i])!=RequestOkay)
auto deleteSaveRequest = std::make_unique<http::DeleteSaveRequest>(saves[i]);
deleteSaveRequest->Start();
deleteSaveRequest->Wait();
try
{
notifyError(String::Build("Failed to delete [", saves[i], "]: ", Client::Ref().GetLastError()));
deleteSaveRequest->Finish();
}
catch (const http::RequestError &ex)
{
notifyError(String::Build("Failed to delete [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
c->Refresh();
return false;
}
@ -286,37 +299,49 @@ void SearchController::unpublishSelectedC(bool publish)
public:
UnpublishSavesTask(std::vector<int> saves_, SearchController *c_, bool publish_) { saves = saves_; c = c_; publish = publish_; }
bool PublishSave(int saveID)
void PublishSave(int saveID)
{
notifyStatus(String::Build("Publishing save [", saveID, "]"));
if (Client::Ref().PublishSave(saveID) != RequestOkay)
return false;
return true;
auto publishSaveRequest = std::make_unique<http::PublishSaveRequest>(saveID);
publishSaveRequest->Start();
publishSaveRequest->Wait();
publishSaveRequest->Finish();
}
bool UnpublishSave(int saveID)
void UnpublishSave(int saveID)
{
notifyStatus(String::Build("Unpublishing save [", saveID, "]"));
if (Client::Ref().UnpublishSave(saveID) != RequestOkay)
return false;
return true;
auto unpublishSaveRequest = std::make_unique<http::UnpublishSaveRequest>(saveID);
unpublishSaveRequest->Start();
unpublishSaveRequest->Wait();
unpublishSaveRequest->Finish();
}
bool doWork() override
{
bool ret;
for (size_t i = 0; i < saves.size(); i++)
{
try
{
if (publish)
ret = PublishSave(saves[i]);
{
PublishSave(saves[i]);
}
else
ret = UnpublishSave(saves[i]);
if (!ret)
{
UnpublishSave(saves[i]);
}
}
catch (const http::RequestError &ex)
{
if (publish) // uses html page so error message will be spam
{
notifyError(String::Build("Failed to publish [", saves[i], "], is this save yours?"));
}
else
notifyError(String::Build("Failed to unpublish [", saves[i], "]: " + Client::Ref().GetLastError()));
{
notifyError(String::Build("Failed to unpublish [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
}
c->Refresh();
return false;
}
@ -343,9 +368,16 @@ void SearchController::FavouriteSelected()
for (size_t i = 0; i < saves.size(); i++)
{
notifyStatus(String::Build("Favouring save [", saves[i], "]"));
if (Client::Ref().FavouriteSave(saves[i], true)!=RequestOkay)
auto favouriteSaveRequest = std::make_unique<http::FavouriteSaveRequest>(saves[i], true);
favouriteSaveRequest->Start();
favouriteSaveRequest->Wait();
try
{
notifyError(String::Build("Failed to favourite [", saves[i], "]: " + Client::Ref().GetLastError()));
favouriteSaveRequest->Finish();
}
catch (const http::RequestError &ex)
{
notifyError(String::Build("Failed to favourite [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
return false;
}
notifyProgress((i + 1) * 100 / saves.size());
@ -364,9 +396,16 @@ void SearchController::FavouriteSelected()
for (size_t i = 0; i < saves.size(); i++)
{
notifyStatus(String::Build("Unfavouring save [", saves[i], "]"));
if (Client::Ref().FavouriteSave(saves[i], false)!=RequestOkay)
auto unfavouriteSaveRequest = std::make_unique<http::FavouriteSaveRequest>(saves[i], false);
unfavouriteSaveRequest->Start();
unfavouriteSaveRequest->Wait();
try
{
notifyError(String::Build("Failed to unfavourite [", saves[i], "]: " + Client::Ref().GetLastError()));
unfavouriteSaveRequest->Finish();
}
catch (const http::RequestError &ex)
{
notifyError(String::Build("Failed to unfavourite [", saves[i], "]: ", ByteString(ex.what()).FromAscii()));
return false;
}
notifyProgress((i + 1) * 100 / saves.size());

View File

@ -4,12 +4,14 @@
#include "client/SaveInfo.h"
#include "client/GameSave.h"
#include "client/Client.h"
#include "client/http/SearchSavesRequest.h"
#include "client/http/SearchTagsRequest.h"
#include "common/tpt-minmax.h"
#include <thread>
#include <cmath>
SearchModel::SearchModel():
currentSort("best"),
currentSort(http::sortByVotes),
currentPage(1),
resultCount(0),
showOwn(false),
@ -28,81 +30,26 @@ bool SearchModel::GetShowTags()
return showTags;
}
void SearchModel::BeginSearchSaves(int start, int count, String query, ByteString sort, ByteString category)
void SearchModel::BeginSearchSaves(int start, int count, String query, http::Sort sort, http::Category category)
{
lastError = "";
resultCount = 0;
ByteStringBuilder urlStream;
ByteString data;
urlStream << SCHEME << SERVER << "/Browse.json?Start=" << start << "&Count=" << count;
if(query.length() || sort.length())
{
urlStream << "&Search_Query=";
if(query.length())
urlStream << format::URLEncode(query.ToUtf8());
if(sort == "date")
{
if(query.length())
urlStream << format::URLEncode(" ");
urlStream << format::URLEncode("sort:") << format::URLEncode(sort);
}
}
if(category.length())
{
urlStream << "&Category=" << format::URLEncode(category);
}
searchSaves = std::make_unique<http::Request>(urlStream.Build());
auto authUser = Client::Ref().GetAuthUser();
if (authUser.UserID)
{
searchSaves->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID);
}
searchSaves = std::make_unique<http::SearchSavesRequest>(start, count, query.ToUtf8(), sort, category);
searchSaves->Start();
}
std::vector<std::unique_ptr<SaveInfo>> SearchModel::EndSearchSaves()
{
std::vector<std::unique_ptr<SaveInfo>> saveArray;
auto [ dataStatus, data ] = searchSaves->Finish();
searchSaves.reset();
auto &client = Client::Ref();
client.ParseServerReturn(data, dataStatus, true);
if (dataStatus == 200 && data.size())
{
try
{
std::istringstream dataStream(data);
Json::Value objDocument;
dataStream >> objDocument;
resultCount = objDocument["Count"].asInt();
Json::Value savesArray = objDocument["Saves"];
for (Json::UInt j = 0; j < savesArray.size(); j++)
std::tie(resultCount, saveArray) = searchSaves->Finish();
}
catch (const http::RequestError &ex)
{
int tempID = savesArray[j]["ID"].asInt();
int tempCreatedDate = savesArray[j]["Created"].asInt();
int tempUpdatedDate = savesArray[j]["Updated"].asInt();
int tempScoreUp = savesArray[j]["ScoreUp"].asInt();
int tempScoreDown = savesArray[j]["ScoreDown"].asInt();
ByteString tempUsername = savesArray[j]["Username"].asString();
String tempName = ByteString(savesArray[j]["Name"].asString()).FromUtf8();
int tempVersion = savesArray[j]["Version"].asInt();
bool tempPublished = savesArray[j]["Published"].asBool();
auto tempSaveInfo = std::make_unique<SaveInfo>(tempID, tempCreatedDate, tempUpdatedDate, tempScoreUp, tempScoreDown, tempUsername, tempName);
tempSaveInfo->Version = tempVersion;
tempSaveInfo->SetPublished(tempPublished);
saveArray.push_back(std::move(tempSaveInfo));
}
}
catch (std::exception &e)
{
lastError = "Could not read response: " + ByteString(e.what()).FromUtf8();
}
}
else
{
lastError = client.GetLastError();
lastError = ByteString(ex.what()).FromUtf8();
}
searchSaves.reset();
return saveArray;
}
@ -117,40 +64,22 @@ void SearchModel::BeginGetTags(int start, int count, String query)
if(query.length())
urlStream << format::URLEncode(query.ToUtf8());
}
getTags = std::make_unique<http::Request>(urlStream.Build());
getTags = std::make_unique<http::SearchTagsRequest>(start, count, query.ToUtf8());
getTags->Start();
}
std::vector<std::pair<ByteString, int>> SearchModel::EndGetTags()
{
std::vector<std::pair<ByteString, int>> tagArray;
auto [ dataStatus, data ] = getTags->Finish();
getTags.reset();
if(dataStatus == 200 && data.size())
{
try
{
std::istringstream dataStream(data);
Json::Value objDocument;
dataStream >> objDocument;
Json::Value tagsArray = objDocument["Tags"];
for (Json::UInt j = 0; j < tagsArray.size(); j++)
tagArray = getTags->Finish();
}
catch (const http::RequestError &ex)
{
int tagCount = tagsArray[j]["Count"].asInt();
ByteString tag = tagsArray[j]["Tag"].asString();
tagArray.push_back(std::pair<ByteString, int>(tag, tagCount));
}
}
catch (std::exception & e)
{
lastError = "Could not read response: " + ByteString(e.what()).FromUtf8();
}
}
else
{
lastError = http::StatusText(dataStatus);
lastError = ByteString(ex.what()).FromUtf8();
}
getTags.reset();
return tagArray;
}
@ -166,7 +95,7 @@ bool SearchModel::UpdateSaveList(int pageNumber, String query)
//resultCount = 0;
currentPage = pageNumber;
if(pageNumber == 1 && !showOwn && !showFavourite && currentSort == "best" && query == "")
if(pageNumber == 1 && !showOwn && !showFavourite && currentSort == http::sortByVotes && query == "")
SetShowTags(true);
else
SetShowTags(false);
@ -182,12 +111,16 @@ bool SearchModel::UpdateSaveList(int pageNumber, String query)
BeginGetTags(0, 24, "");
}
ByteString category = "";
auto category = http::categoryNone;
if (showFavourite)
category = "Favourites";
{
category = http::categoryFavourites;
}
if (showOwn && Client::Ref().GetAuthUser().UserID)
category = "by:"+Client::Ref().GetAuthUser().Username;
BeginSearchSaves((currentPage-1)*20, 20, lastQuery, currentSort=="new"?"date":"votes", category);
{
category = http::categoryMyOwn;
}
BeginSearchSaves((currentPage-1)*20, 20, lastQuery, currentSort, category);
return true;
}
return false;
@ -363,7 +296,7 @@ void SearchModel::notifySelectedChanged()
int SearchModel::GetPageCount()
{
if (!showOwn && !showFavourite && currentSort == "best" && lastQuery == "")
if (!showOwn && !showFavourite && currentSort == http::sortByVotes && lastQuery == "")
return std::max(1, (int)(ceil(resultCount/20.0f))+1); //add one for front page (front page saves are repeated twice)
else
return std::max(1, (int)(ceil(resultCount/20.0f)));

View File

@ -1,26 +1,32 @@
#pragma once
#include "common/String.h"
#include "client/http/Request.h"
#include "client/Search.h"
#include "Config.h"
#include <vector>
#include <atomic>
#include <memory>
namespace http
{
class SearchSavesRequest;
class SearchTagsRequest;
}
class SaveInfo;
class SearchView;
class SearchModel
{
private:
std::unique_ptr<http::Request> searchSaves;
void BeginSearchSaves(int start, int count, String query, ByteString sort, ByteString category);
std::unique_ptr<http::SearchSavesRequest> searchSaves;
void BeginSearchSaves(int start, int count, String query, http::Sort sort, http::Category category);
std::vector<std::unique_ptr<SaveInfo>> EndSearchSaves();
void BeginGetTags(int start, int count, String query);
std::vector<std::pair<ByteString, int>> EndGetTags();
std::unique_ptr<http::Request> getTags;
std::unique_ptr<http::SearchTagsRequest> getTags;
std::unique_ptr<SaveInfo> loadedSave;
ByteString currentSort;
http::Sort currentSort;
String lastQuery;
String lastError;
std::vector<int> selected;
@ -55,8 +61,8 @@ public:
int GetPageCount();
int GetPageNum() { return currentPage; }
String GetLastQuery() { return lastQuery; }
void SetSort(ByteString sort) { if(!searchSaves) { currentSort = sort; } notifySortChanged(); }
ByteString GetSort() { return currentSort; }
void SetSort(http::Sort sort) { if(!searchSaves) { currentSort = sort; } notifySortChanged(); }
http::Sort GetSort() { return currentSort; }
void SetShowOwn(bool show) { if(!searchSaves) { if(show!=showOwn) { showOwn = show; } } notifyShowOwnChanged(); }
bool GetShowOwn() { return showOwn; }
void SetShowFavourite(bool show) { if(show!=showFavourite && !searchSaves) { showFavourite = show; } notifyShowFavouriteChanged(); }

View File

@ -211,7 +211,7 @@ void SearchView::Search(String query)
void SearchView::NotifySortChanged(SearchModel * sender)
{
if(sender->GetSort() == "best")
if(sender->GetSort() == http::sortByVotes)
{
sortButton->SetToggleState(false);
sortButton->SetText("By votes");
@ -228,7 +228,7 @@ void SearchView::NotifySortChanged(SearchModel * sender)
void SearchView::NotifyShowOwnChanged(SearchModel * sender)
{
ownButton->SetToggleState(sender->GetShowOwn());
if(sender->GetShowOwn() || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator)
if(sender->GetShowOwn() || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod)
{
unpublishSelected->Enabled = true;
removeSelected->Enabled = true;
@ -248,7 +248,7 @@ void SearchView::NotifyShowFavouriteChanged(SearchModel * sender)
unpublishSelected->Enabled = false;
removeSelected->Enabled = false;
}
else if(sender->GetShowOwn() || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator)
else if(sender->GetShowOwn() || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod)
{
unpublishSelected->Enabled = true;
removeSelected->Enabled = true;
@ -323,7 +323,7 @@ void SearchView::CheckAccess()
favButton->Enabled = true;
favouriteSelected->Enabled = true;
if (Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator)
if (Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod)
{
unpublishSelected->Enabled = true;
removeSelected->Enabled = true;
@ -569,7 +569,7 @@ void SearchView::NotifySaveListChanged(SearchModel * sender)
});
if(Client::Ref().GetAuthUser().UserID)
saveButton->SetSelectable(true);
if (saves[i]->GetUserName() == Client::Ref().GetAuthUser().Username || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator)
if (saves[i]->GetUserName() == Client::Ref().GetAuthUser().Username || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod)
saveButton->SetShowVotes(true);
saveButtons.push_back(saveButton);
AddComponent(saveButton);

View File

@ -1,8 +1,8 @@
#include "TagsController.h"
#include "TagsModel.h"
#include "TagsView.h"
#include "client/http/AddTagRequest.h"
#include "client/http/RemoveTagRequest.h"
#include "gui/interface/Engine.h"
#include "client/SaveInfo.h"
#include "Controller.h"
@ -36,6 +36,11 @@ void TagsController::AddTag(ByteString tag)
tagsModel->AddTag(tag);
}
void TagsController::Tick()
{
tagsModel->Tick();
}
void TagsController::Exit()
{
tagsView->CloseActiveWindow();

View File

@ -18,5 +18,6 @@ public:
void RemoveTag(ByteString tag);
void AddTag(ByteString tag);
void Exit();
void Tick();
virtual ~TagsController();
};

View File

@ -1,13 +1,15 @@
#include "TagsModel.h"
#include "TagsView.h"
#include "TagsModelException.h"
#include "client/Client.h"
#include "client/SaveInfo.h"
#include "client/http/AddTagRequest.h"
#include "client/http/RemoveTagRequest.h"
#include "gui/dialogues/ErrorMessage.h"
void TagsModel::SetSave(SaveInfo *newSave /* non-owning */)
{
queuedTags.clear();
this->save = newSave;
notifyTagsChanged();
}
@ -17,40 +19,73 @@ SaveInfo *TagsModel::GetSave() // non-owning
return save;
}
void TagsModel::RemoveTag(ByteString tag)
void TagsModel::Tick()
{
auto triggerTags = false;
std::list<ByteString> tags;
if (addTagRequest && addTagRequest->CheckDone())
{
try
{
tags = addTagRequest->Finish();
triggerTags = true;
}
catch (const http::RequestError &ex)
{
new ErrorMessage("Could not add tag", ByteString(ex.what()).FromUtf8());
}
addTagRequest.reset();
}
if (removeTagRequest && removeTagRequest->CheckDone())
{
try
{
tags = removeTagRequest->Finish();
triggerTags = true;
}
catch (const http::RequestError &ex)
{
new ErrorMessage("Could not remove tag", ByteString(ex.what()).FromUtf8());
}
removeTagRequest.reset();
}
if (triggerTags)
{
if (save)
{
std::list<ByteString> * tags = Client::Ref().RemoveTag(save->GetID(), tag);
if(tags)
{
save->SetTags(std::list<ByteString>(*tags));
save->SetTags(tags);
}
notifyTagsChanged();
delete tags;
}
if (!addTagRequest && !removeTagRequest && !queuedTags.empty())
{
auto it = queuedTags.begin();
auto [ tag, add ] = *it;
queuedTags.erase(it);
if (save)
{
if (add)
{
addTagRequest = std::make_unique<http::AddTagRequest>(save->GetID(), tag);
addTagRequest->Start();
}
else
{
throw TagsModelException(Client::Ref().GetLastError());
removeTagRequest = std::make_unique<http::RemoveTagRequest>(save->GetID(), tag);
removeTagRequest->Start();
}
}
}
}
void TagsModel::RemoveTag(ByteString tag)
{
queuedTags[tag] = false;
}
void TagsModel::AddTag(ByteString tag)
{
if(save)
{
std::list<ByteString> * tags = Client::Ref().AddTag(save->GetID(), tag);
if(tags)
{
save->SetTags(std::list<ByteString>(*tags));
notifyTagsChanged();
delete tags;
}
else
{
throw TagsModelException(Client::Ref().GetLastError());
}
}
queuedTags[tag] = true;
}
void TagsModel::AddObserver(TagsView * observer)

View File

@ -1,11 +1,22 @@
#pragma once
#include "common/String.h"
#include <vector>
#include <map>
#include <memory>
namespace http
{
class AddTagRequest;
class RemoveTagRequest;
}
class SaveInfo;
class TagsView;
class TagsModel {
std::unique_ptr<http::AddTagRequest> addTagRequest;
std::unique_ptr<http::RemoveTagRequest> removeTagRequest;
std::map<ByteString, bool> queuedTags;
SaveInfo *save = nullptr; // non-owning
std::vector<TagsView*> observers;
void notifyTagsChanged();
@ -15,4 +26,5 @@ public:
void RemoveTag(ByteString tag);
void AddTag(ByteString tag);
SaveInfo *GetSave(); // non-owning
void Tick();
};

View File

@ -49,6 +49,11 @@ TagsView::TagsView():
AddComponent(title);
}
void TagsView::OnTick(float dt)
{
c->Tick();
}
void TagsView::OnDraw()
{
Graphics * g = GetGraphics();
@ -76,7 +81,7 @@ void TagsView::NotifyTagsChanged(TagsModel * sender)
tags.push_back(tempLabel);
AddComponent(tempLabel);
if(sender->GetSave()->GetUserName() == Client::Ref().GetAuthUser().Username || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator)
if(sender->GetSave()->GetUserName() == Client::Ref().GetAuthUser().Username || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod)
{
ui::Button * tempButton = new ui::Button(ui::Point(15, 37+(16*i)), ui::Point(11, 12));
tempButton->Appearance.icon = IconDelete;
@ -85,14 +90,7 @@ void TagsView::NotifyTagsChanged(TagsModel * sender)
tempButton->Appearance.HorizontalAlign = ui::Appearance::AlignCentre;
tempButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
tempButton->SetActionCallback({ [this, tag] {
try
{
c->RemoveTag(tag);
}
catch(TagsModelException & ex)
{
new ErrorMessage("Could not remove tag", ByteString(ex.what()).FromUtf8());
}
} });
tags.push_back(tempButton);
AddComponent(tempButton);
@ -125,13 +123,6 @@ void TagsView::addTag()
new ErrorMessage("Tag not long enough", "Must be at least 4 letters");
return;
}
try
{
c->AddTag(tagInput->GetText().ToUtf8());
}
catch(TagsModelException & ex)
{
new ErrorMessage("Could not add tag", ByteString(ex.what()).FromUtf8());
}
tagInput->SetText("");
}

View File

@ -22,6 +22,7 @@ class TagsView: public ui::Window {
public:
TagsView();
void OnDraw() override;
void OnTick(float dt) override;
void AttachController(TagsController * c_) { c = c_; }
void OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) override;
void NotifyTagsChanged(TagsModel * sender);

View File

@ -1,7 +1,6 @@
#include "UpdateActivity.h"
#include "client/http/Request.h"
#include "prefs/GlobalPrefs.h"
#include "client/Client.h"
#include "common/platform/Platform.h"
#include "tasks/Task.h"
#include "tasks/TaskWindow.h"
@ -53,7 +52,16 @@ private:
Platform::Millisleep(1);
}
auto [ status, data ] = request->Finish();
int status;
ByteString data;
try
{
std::tie(status, data) = request->Finish();
}
catch (const http::RequestError &ex)
{
return niceNotifyError("Could not download update: " + String::Build("Server responded with Status ", ByteString(ex.what()).FromAscii()));
}
if (status!=200)
{
return niceNotifyError("Could not download update: " + String::Build("Server responded with Status ", status));
@ -107,9 +115,9 @@ private:
}
};
UpdateActivity::UpdateActivity() {
ByteString file = ByteString::Build(SCHEME, USE_UPDATESERVER ? UPDATESERVER : SERVER, Client::Ref().GetUpdateInfo().File);
updateDownloadTask = new UpdateDownloadTask(file, this);
UpdateActivity::UpdateActivity(UpdateInfo info)
{
updateDownloadTask = new UpdateDownloadTask(info.file, this);
updateWindow = new TaskWindow("Downloading update...", updateDownloadTask, true);
}

View File

@ -1,4 +1,5 @@
#pragma once
#include "client/StartupInfo.h"
class Task;
class TaskWindow;
@ -7,7 +8,7 @@ class UpdateActivity
Task * updateDownloadTask;
TaskWindow * updateWindow;
public:
UpdateActivity();
UpdateActivity(UpdateInfo info);
virtual ~UpdateActivity();
void Exit();
virtual void NotifyDone(Task * sender);

Some files were not shown because too many files have changed in this diff Show More