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 }; 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 //Linear interpolation
template <typename T> inline T LinearInterpolate(T val1, T val2, T lowerCoord, T upperCoord, T coord) 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/SaveFile.h"
#include "client/SaveInfo.h" #include "client/SaveInfo.h"
#include "client/http/requestmanager/RequestManager.h" #include "client/http/requestmanager/RequestManager.h"
#include "client/http/GetSaveRequest.h"
#include "client/http/GetSaveDataRequest.h"
#include "common/platform/Platform.h" #include "common/platform/Platform.h"
#include "graphics/Graphics.h" #include "graphics/Graphics.h"
#include "simulation/SaveRenderer.h" #include "simulation/SaveRenderer.h"
@ -462,15 +464,31 @@ int main(int argc, char * argv[])
} }
int saveId = saveIdPart.ToNumber<int>(); int saveId = saveIdPart.ToNumber<int>();
auto newSave = Client::Ref().GetSave(saveId, 0); auto getSave = std::make_unique<http::GetSaveRequest>(saveId, 0);
if (!newSave) getSave->Start();
throw std::runtime_error("Could not load save info"); getSave->Wait();
auto saveData = Client::Ref().GetSaveData(saveId, 0); std::unique_ptr<SaveInfo> newSave;
if (!saveData.size()) try
throw std::runtime_error(("Could not load save\n" + Client::Ref().GetLastError()).ToUtf8()); {
auto newGameSave = std::make_unique<GameSave>(std::move(saveData)); newSave = getSave->Finish();
newSave->SetGameSave(std::move(newGameSave)); }
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)); gameController->LoadSave(std::move(newSave));
} }
catch (std::exception & e) catch (std::exception & e)

View File

@ -1,6 +1,6 @@
#include "Client.h" #include "Client.h"
#include "prefs/GlobalPrefs.h" #include "prefs/GlobalPrefs.h"
#include "client/http/Request.h" #include "client/http/StartupRequest.h"
#include "ClientListener.h" #include "ClientListener.h"
#include "Format.h" #include "Format.h"
#include "MD5.h" #include "MD5.h"
@ -13,7 +13,6 @@
#include "graphics/Graphics.h" #include "graphics/Graphics.h"
#include "prefs/Prefs.h" #include "prefs/Prefs.h"
#include "lua/CommandInterface.h" #include "lua/CommandInterface.h"
#include "gui/preview/Comment.h"
#include "Config.h" #include "Config.h"
#include <cstring> #include <cstring>
#include <cstdlib> #include <cstdlib>
@ -29,8 +28,6 @@
Client::Client(): Client::Client():
messageOfTheDay("Fetching the message of the day..."), messageOfTheDay("Fetching the message of the day..."),
versionCheckRequest(nullptr),
alternateVersionCheckRequest(nullptr),
usingAltUpdateServer(false), usingAltUpdateServer(false),
updateAvailable(false), updateAvailable(false),
authUser(0, "") authUser(0, "")
@ -40,16 +37,7 @@ Client::Client():
authUser.Username = prefs.Get("User.Username", ByteString("")); authUser.Username = prefs.Get("User.Username", ByteString(""));
authUser.SessionID = prefs.Get("User.SessionID", ByteString("")); authUser.SessionID = prefs.Get("User.SessionID", ByteString(""));
authUser.SessionKey = prefs.Get("User.SessionKey", ByteString("")); authUser.SessionKey = prefs.Get("User.SessionKey", ByteString(""));
auto elevation = prefs.Get("User.Elevation", ByteString("")); authUser.UserElevation = prefs.Get("User.Elevation", User::ElevationNone);
authUser.UserElevation = User::ElevationNone;
if (elevation == "Admin")
{
authUser.UserElevation = User::ElevationAdmin;
}
if (elevation == "Mod")
{
authUser.UserElevation = User::ElevationModerator;
}
firstRun = !prefs.BackedByFile(); firstRun = !prefs.BackedByFile();
} }
@ -88,24 +76,14 @@ void Client::Initialize()
} }
//Begin version check //Begin version check
versionCheckRequest = std::make_unique<http::Request>(ByteString::Build(SCHEME, SERVER, "/Startup.json")); versionCheckRequest = std::make_unique<http::StartupRequest>(false);
if (authUser.UserID)
{
versionCheckRequest->AuthHeaders(ByteString::Build(authUser.UserID), authUser.SessionID);
}
versionCheckRequest->Start(); versionCheckRequest->Start();
if constexpr (USE_UPDATESERVER) if constexpr (USE_UPDATESERVER)
{ {
// use an alternate update server // use an alternate update server
alternateVersionCheckRequest = std::make_unique<http::Request>(ByteString::Build(SCHEME, UPDATESERVER, "/Startup.json")); alternateVersionCheckRequest = std::make_unique<http::StartupRequest>(true);
usingAltUpdateServer = true;
if (authUser.UserID)
{
alternateVersionCheckRequest->AuthHeaders(authUser.Username, "");
}
alternateVersionCheckRequest->Start(); alternateVersionCheckRequest->Start();
usingAltUpdateServer = true;
} }
} }
@ -125,203 +103,82 @@ String Client::GetMessageOfTheDay()
return messageOfTheDay; return messageOfTheDay;
} }
void Client::AddServerNotification(std::pair<String, ByteString> notification) void Client::AddServerNotification(ServerNotification notification)
{ {
serverNotifications.push_back(notification); serverNotifications.push_back(notification);
notifyNewNotification(notification); notifyNewNotification(notification);
} }
std::vector<std::pair<String, ByteString> > Client::GetServerNotifications() std::vector<ServerNotification> Client::GetServerNotifications()
{ {
return serverNotifications; 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() void Client::Tick()
{ {
CheckUpdate(versionCheckRequest, true); auto applyUpdateInfo = false;
CheckUpdate(alternateVersionCheckRequest, false); if (versionCheckRequest && versionCheckRequest->CheckDone())
}
void Client::CheckUpdate(std::unique_ptr<http::Request> &updateRequest, bool checkSession)
{ {
//Check status on version check request if (versionCheckRequest->StatusCode() == 618)
if (updateRequest && updateRequest->CheckDone())
{
auto [ status, data ] = updateRequest->Finish();
if (checkSession && status == 618)
{ {
AddServerNotification({ "Failed to load SSL certificates", ByteString(SCHEME) + "powdertoy.co.uk/FAQ.html" }); 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 try
{ {
Json::Value objDocument; auto info = versionCheckRequest->Finish();
dataStream >> objDocument; if (!info.sessionGood)
//Check session
if (checkSession)
{
if (!objDocument["Session"].asBool())
{ {
SetAuthUser(User(0, "")); SetAuthUser(User(0, ""));
} }
if (!usingAltUpdateServer)
{
updateInfo = info.updateInfo;
applyUpdateInfo = true;
messageOfTheDay = info.messageOfTheDay;
} }
for (auto &notification : info.notifications)
//Notifications from server
Json::Value notificationsArray = objDocument["Notifications"];
for (Json::UInt j = 0; j < notificationsArray.size(); j++)
{ {
ByteString notificationLink = notificationsArray[j]["Link"].asString(); AddServerNotification(notification);
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);
} }
} }
catch (const http::RequestError &ex)
if (!updateAvailable)
{ {
Json::Value betaVersion = versions["Beta"]; if (!usingAltUpdateServer)
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)
{ {
updateAvailable = true; messageOfTheDay = ByteString::Build("Error while fetching MotD: ", ex.what()).FromUtf8();
updateInfo = UpdateInfo(betaMajor, betaMinor, betaBuild, betaFile, betaChangelog, UpdateInfo::Beta);
} }
} }
versionCheckRequest.reset();
if constexpr (SNAPSHOT || MOD) }
if (alternateVersionCheckRequest && alternateVersionCheckRequest->CheckDone())
{ {
Json::Value snapshotVersion = versions["Snapshot"]; try
int snapshotSnapshot = snapshotVersion["Snapshot"].asInt();
ByteString snapshotFile = snapshotVersion["File"].asString();
String snapshotChangelog = ByteString(snapshotVersion["Changelog"].asString()).FromUtf8();
if (snapshotSnapshot > SNAPSHOT_ID)
{ {
updateAvailable = true; auto info = alternateVersionCheckRequest->Finish();
updateInfo = UpdateInfo(snapshotSnapshot, snapshotFile, snapshotChangelog, UpdateInfo::Snapshot); updateInfo = info.updateInfo;
applyUpdateInfo = true;
messageOfTheDay = info.messageOfTheDay;
for (auto &notification : info.notifications)
{
AddServerNotification(notification);
} }
} }
catch (const http::RequestError &ex)
if(updateAvailable) {
messageOfTheDay = ByteString::Build("Error while checking for updates: ", ex.what()).FromUtf8();
}
alternateVersionCheckRequest.reset();
}
if (applyUpdateInfo && !IGNORE_UPDATES)
{
if (updateInfo)
{ {
notifyUpdateAvailable(); notifyUpdateAvailable();
} }
} }
} }
}
catch (std::exception & e)
{
//Do nothing
}
}
updateRequest.reset();
}
}
UpdateInfo Client::GetUpdateInfo() std::optional<UpdateInfo> Client::GetUpdateInfo()
{ {
return updateInfo; 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) 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.SessionID", authUser.SessionID);
prefs.Set("User.SessionKey", authUser.SessionKey); prefs.Set("User.SessionKey", authUser.SessionKey);
prefs.Set("User.Username", authUser.Username); prefs.Set("User.Username", authUser.Username);
ByteString elevation = "None"; prefs.Set("User.Elevation", authUser.UserElevation);
if (authUser.UserElevation == User::ElevationAdmin)
{
elevation = "Admin";
}
if (authUser.UserElevation == User::ElevationModerator)
{
elevation = "Mod";
}
prefs.Set("User.Elevation", elevation);
} }
else else
{ {
@ -415,65 +263,6 @@ User Client::GetAuthUser()
return authUser; 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) void Client::MoveStampToFront(ByteString stampID)
{ {
auto it = std::find(stampIDs.begin(), stampIDs.end(), stampID); auto it = std::find(stampIDs.begin(), stampIDs.end(), stampID);
@ -618,312 +407,6 @@ const std::vector<ByteString> &Client::GetStamps() const
return stampIDs; 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) std::unique_ptr<SaveFile> Client::LoadSaveFile(ByteString filename)
{ {
ByteString err; ByteString err;
@ -964,84 +447,6 @@ std::unique_ptr<SaveFile> Client::LoadSaveFile(ByteString filename)
return file; 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 // stamp-specific wrapper for MergeAuthorInfo
// also used for clipboard and lua stamps // also used for clipboard and lua stamps
void Client::MergeStampAuthorInfo(Json::Value stampAuthors) void Client::MergeStampAuthorInfo(Json::Value stampAuthors)

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "common/String.h" #include "common/String.h"
#include "common/ExplicitSingleton.h" #include "common/ExplicitSingleton.h"
#include "StartupInfo.h"
#include "User.h" #include "User.h"
#include <vector> #include <vector>
#include <cstdint> #include <cstdint>
@ -10,53 +11,27 @@
class SaveInfo; class SaveInfo;
class SaveFile; class SaveFile;
class SaveComment;
class GameSave; class GameSave;
class VideoBuffer; 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 Prefs;
class RequestListener; class RequestListener;
class ClientListener; class ClientListener;
namespace http namespace http
{ {
class Request; class StartupRequest;
} }
class Client: public ExplicitSingleton<Client> { class Client: public ExplicitSingleton<Client> {
private: private:
String messageOfTheDay; String messageOfTheDay;
std::vector<std::pair<String, ByteString> > serverNotifications; std::vector<ServerNotification> serverNotifications;
std::unique_ptr<http::Request> versionCheckRequest; std::unique_ptr<http::StartupRequest> versionCheckRequest;
std::unique_ptr<http::Request> alternateVersionCheckRequest; std::unique_ptr<http::StartupRequest> alternateVersionCheckRequest;
bool usingAltUpdateServer; bool usingAltUpdateServer;
bool updateAvailable; bool updateAvailable;
UpdateInfo updateInfo; std::optional<UpdateInfo> updateInfo;
String lastError;
bool firstRun; bool firstRun;
std::vector<ByteString> stampIDs; std::vector<ByteString> stampIDs;
@ -69,7 +44,7 @@ private:
void notifyUpdateAvailable(); void notifyUpdateAvailable();
void notifyAuthUserChanged(); void notifyAuthUserChanged();
void notifyMessageOfTheDay(); void notifyMessageOfTheDay();
void notifyNewNotification(std::pair<String, ByteString> notification); void notifyNewNotification(ServerNotification notification);
// Save stealing info // Save stealing info
Json::Value authors; Json::Value authors;
@ -91,7 +66,7 @@ public:
void ClearAuthorInfo() { authors.clear(); } void ClearAuthorInfo() { authors.clear(); }
bool IsAuthorsEmpty() { return authors.size() == 0; } bool IsAuthorsEmpty() { return authors.size() == 0; }
UpdateInfo GetUpdateInfo(); std::optional<UpdateInfo> GetUpdateInfo();
Client(); Client();
~Client(); ~Client();
@ -99,8 +74,8 @@ public:
ByteString FileOpenDialogue(); ByteString FileOpenDialogue();
//std::string FileSaveDialogue(); //std::string FileSaveDialogue();
void AddServerNotification(std::pair<String, ByteString> notification); void AddServerNotification(ServerNotification notification);
std::vector<std::pair<String, ByteString> > GetServerNotifications(); std::vector<ServerNotification> GetServerNotifications();
void SetMessageOfTheDay(String message); void SetMessageOfTheDay(String message);
String GetMessageOfTheDay(); String GetMessageOfTheDay();
@ -111,9 +86,6 @@ public:
void AddListener(ClientListener * listener); void AddListener(ClientListener * listener);
void RemoveListener(ClientListener * listener); void RemoveListener(ClientListener * listener);
RequestStatus ExecVote(int saveID, int direction);
RequestStatus UploadSave(SaveInfo & save);
std::unique_ptr<SaveFile> GetStamp(ByteString stampID); std::unique_ptr<SaveFile> GetStamp(ByteString stampID);
void DeleteStamp(ByteString stampID); void DeleteStamp(ByteString stampID);
ByteString AddStamp(std::unique_ptr<GameSave> saveData); ByteString AddStamp(std::unique_ptr<GameSave> saveData);
@ -121,30 +93,11 @@ public:
const std::vector<ByteString> &GetStamps() const; const std::vector<ByteString> &GetStamps() const;
void MoveStampToFront(ByteString stampID); 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); 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); void SetAuthUser(User user);
User GetAuthUser(); 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 Tick();
void CheckUpdate(std::unique_ptr<http::Request> &updateRequest, bool checkSession);
String DoMigration(ByteString fromDir, ByteString toDir); String DoMigration(ByteString fromDir, ByteString toDir);
}; };

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include "common/String.h" #include "common/String.h"
#include "client/ServerNotification.h"
class Client; class Client;
class ClientListener class ClientListener
@ -11,6 +12,6 @@ public:
virtual void NotifyUpdateAvailable(Client * sender) {} virtual void NotifyUpdateAvailable(Client * sender) {}
virtual void NotifyAuthUserChanged(Client * sender) {} virtual void NotifyAuthUserChanged(Client * sender) {}
virtual void NotifyMessageOfTheDay(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; this->name = name;
} }
String SaveInfo::GetName() const String &SaveInfo::GetName() const
{ {
return name; return name;
} }
@ -54,7 +54,7 @@ void SaveInfo::SetDescription(String description)
{ {
Description = description; Description = description;
} }
String SaveInfo::GetDescription() const String &SaveInfo::GetDescription() const
{ {
return Description; 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); 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); void SetName(String name);
String GetName(); const String &GetName() const;
void SetDescription(String description); void SetDescription(String description);
String GetDescription(); const String &GetDescription() const;
void SetPublished(bool published); void SetPublished(bool published);
bool GetPublished() const; 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: public:
enum Elevation enum Elevation
{ {
ElevationAdmin, ElevationModerator, ElevationNone ElevationNone,
ElevationHalfMod,
ElevationMod,
ElevationAdmin,
}; };
static Elevation ElevationFromString(ByteString str);
static ByteString ElevationToString(Elevation elevation);
int UserID; int UserID;
ByteString Username; ByteString Username;
ByteString SessionID; ByteString SessionID;

View File

@ -1,35 +1,36 @@
#include "APIRequest.h" #include "APIRequest.h"
#include "client/Client.h" #include "client/Client.h"
namespace http 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); 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 try
{ {
ByteString data; std::istringstream ss(data);
std::tie(result.status, data) = Request::Finish(); ss >> document;
Client::Ref().ParseServerReturn(data, result.status, true); }
if (result.status == 200 && data.size()) catch (const std::exception &ex)
{ {
std::istringstream dataStream(data); throw RequestError("Could not read response: " + ByteString(ex.what()));
Json::Value objDocument; }
dataStream >> objDocument; return document;
result.document = std::unique_ptr<Json::Value>(new Json::Value(objDocument));
} }
} }
catch (std::exception & e)
{
}
return result;
}
}

View File

@ -2,22 +2,22 @@
#include "Request.h" #include "Request.h"
#include "common/String.h" #include "common/String.h"
#include <json/json.h> #include <json/json.h>
#include <memory>
#include <map>
namespace http namespace http
{ {
class APIRequest : public Request class APIRequest : public Request
{ {
bool checkStatus;
public: public:
struct Result enum AuthMode
{ {
int status; authRequire,
std::unique_ptr<Json::Value> document; authUse,
authOmit,
}; };
APIRequest(ByteString url, AuthMode authMode, bool newCheckStatus);
APIRequest(ByteString url); Json::Value Finish();
Result 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 namespace http
{ {
GetUserInfoRequest::GetUserInfoRequest(ByteString username) : 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(); auto result = APIRequest::Finish();
if (result.document) UserInfo userInfo;
try
{ {
auto &user = (*result.document)["User"]; auto &user = result["User"];
user_info = std::unique_ptr<UserInfo>(new UserInfo( userInfo = UserInfo(
user["ID"].asInt(), user["ID"].asInt(),
user["Age"].asInt(), user["Age"].asInt(),
user["Username"].asString(), user["Username"].asString(),
@ -29,9 +29,13 @@ namespace http
user["Forum"]["Topics"].asInt(), user["Forum"]["Topics"].asInt(),
user["Forum"]["Replies"].asInt(), user["Forum"]["Replies"].asInt(),
user["Forum"]["Reputation"].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 #pragma once
#include "APIRequest.h" #include "APIRequest.h"
#include "client/UserInfo.h"
class UserInfo;
namespace http namespace http
{ {
@ -10,6 +9,6 @@ namespace http
public: public:
GetUserInfoRequest(ByteString username); GetUserInfoRequest(ByteString username);
std::unique_ptr<UserInfo> Finish(); UserInfo Finish();
}; };
} }

View File

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

View File

@ -2,7 +2,6 @@
#include "common/String.h" #include "common/String.h"
#include "common/Vec2.h" #include "common/Vec2.h"
#include "Request.h" #include "Request.h"
#include <memory> #include <memory>
class VideoBuffer; class VideoBuffer;
@ -11,10 +10,10 @@ namespace http
{ {
class ImageRequest : public Request class ImageRequest : public Request
{ {
Vec2<int> size; Vec2<int> requestedSize;
public: public:
ImageRequest(ByteString url, Vec2<int> size); ImageRequest(ByteString url, Vec2<int> newRequestedSize);
std::unique_ptr<VideoBuffer> Finish(); 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 "requestmanager/RequestManager.h"
#include <memory> #include <memory>
#include <iostream> #include <iostream>
#include <cstring>
#include <json/json.h>
namespace http namespace http
{ {
@ -13,12 +15,28 @@ namespace http
Request::~Request() 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); RequestManager::Ref().UnregisterRequest(*this);
} }
} }
void Request::FailEarly(ByteString error)
{
assert(handle->state == RequestHandle::ready);
handle->failEarly = error;
}
void Request::Verb(ByteString newVerb) void Request::Verb(ByteString newVerb)
{ {
assert(handle->state == RequestHandle::ready); assert(handle->state == RequestHandle::ready);
@ -84,18 +102,36 @@ namespace http
return handle->responseHeaders; return handle->responseHeaders;
} }
std::pair<int, ByteString> Request::Finish() void Request::Wait()
{ {
std::unique_lock lk(handle->stateMx); std::unique_lock lk(handle->stateMx);
if (handle->state == RequestHandle::running) assert(handle->state == RequestHandle::running);
{
handle->stateCv.wait(lk, [this]() { handle->stateCv.wait(lk, [this]() {
return handle->state == RequestHandle::done; return handle->state == RequestHandle::done;
}); });
} }
int Request::StatusCode() const
{
{
std::unique_lock lk(handle->stateMx);
assert(handle->state == RequestHandle::done); 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; 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() void RequestHandle::MarkDone()
@ -106,30 +142,13 @@ namespace http
state = RequestHandle::done; state = RequestHandle::done;
} }
stateCv.notify_one(); 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) const char *StatusText(int ret)
{
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)
{ {
switch (ret) switch (ret)
{ {
@ -211,7 +230,69 @@ namespace http
case 619: return "SSL: Failed to Load CRL File"; case 619: return "SSL: Failed to Load CRL File";
case 620: return "SSL: Issuer Check Failed"; case 620: return "SSL: Issuer Check Failed";
case 621: return "SSL: Pinned Public Key Mismatch"; 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 <vector>
#include <mutex> #include <mutex>
#include <condition_variable> #include <condition_variable>
#include <optional>
namespace http namespace http
{ {
struct RequestHandle; struct RequestHandle;
// Thrown by Finish and ParseResponse
struct RequestError : public std::runtime_error
{
using runtime_error::runtime_error;
};
class Request class Request
{ {
std::shared_ptr<RequestHandle> handle; std::shared_ptr<RequestHandle> handle;
@ -21,6 +28,8 @@ namespace http
Request &operator =(const Request &) = delete; Request &operator =(const Request &) = delete;
~Request(); ~Request();
void FailEarly(ByteString error);
void Verb(ByteString newVerb); void Verb(ByteString newVerb);
void AddHeader(ByteString header); void AddHeader(ByteString header);
@ -32,13 +41,21 @@ namespace http
std::pair<int, int> CheckProgress() const; // total, done std::pair<int, int> CheckProgress() const; // total, done
const std::vector<ByteString> &ResponseHeaders() const; const std::vector<ByteString> &ResponseHeaders() const;
void Wait();
int StatusCode() const; // status
std::pair<int, ByteString> Finish(); // status, data std::pair<int, ByteString> Finish(); // status, data
static std::pair<int, ByteString> Simple(ByteString uri, FormData postData = {}); enum ResponseType
static std::pair<int, ByteString> SimpleAuth(ByteString uri, ByteString ID, ByteString session, FormData postData = {}); {
responseOk,
responseJson,
responseData,
};
static void ParseResponse(const ByteString &result, int status, ResponseType responseType);
friend class RequestManager; friend class RequestManager;
}; };
String StatusText(int code); const char *StatusText(int code);
} }

View File

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

View File

@ -1,15 +1,14 @@
#pragma once #pragma once
#include "APIRequest.h" #include "APIRequest.h"
#include "client/UserInfo.h"
class UserInfo;
namespace http namespace http
{ {
class SaveUserInfoRequest : public APIRequest class SaveUserInfoRequest : public APIRequest
{ {
public: 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', 'Request.cpp',
'SaveUserInfoRequest.cpp', 'SaveUserInfoRequest.cpp',
'ThumbnailRequest.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') subdir('requestmanager')

View File

@ -22,6 +22,13 @@ namespace http
void RequestManager::RegisterRequest(Request &request) 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) if (disableNetwork)
{ {
request.handle->statusCode = 604; request.handle->statusCode = 604;

View File

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

View File

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

View File

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

View File

@ -63,6 +63,7 @@
#include "Config.h" #include "Config.h"
#include <SDL.h> #include <SDL.h>
#include <iostream>
#ifdef GetUserName #ifdef GetUserName
# undef GetUserName // dammit windows # undef GetUserName // dammit windows
@ -699,6 +700,7 @@ bool GameController::KeyRelease(int key, int scan, bool repeat, bool shift, bool
void GameController::Tick() void GameController::Tick()
{ {
gameModel->Tick();
if(firstTick) if(firstTick)
{ {
commandInterface->Init(); commandInterface->Init();
@ -1441,16 +1443,9 @@ void GameController::FrameStep()
void GameController::Vote(int direction) void GameController::Vote(int direction)
{ {
if (gameModel->GetSave() && gameModel->GetUser().UserID && gameModel->GetSave()->GetID()) if (gameModel->GetSave() && gameModel->GetUser().UserID && gameModel->GetSave()->GetID())
{
try
{ {
gameModel->SetVote(direction); gameModel->SetVote(direction);
} }
catch(GameModelException & ex)
{
new ErrorMessage("Error while voting", ByteString(ex.what()).FromUtf8());
}
}
} }
void GameController::ChangeBrush() void GameController::ChangeBrush()
@ -1536,7 +1531,7 @@ void GameController::NotifyAuthUserChanged(Client * sender)
gameModel->SetUser(newUser); gameModel->SetUser(newUser);
} }
void GameController::NotifyNewNotification(Client * sender, std::pair<String, ByteString> notification) void GameController::NotifyNewNotification(Client * sender, ServerNotification notification)
{ {
class LinkNotification : public Notification class LinkNotification : public Notification
{ {
@ -1550,7 +1545,7 @@ void GameController::NotifyNewNotification(Client * sender, std::pair<String, By
Platform::OpenURI(link); Platform::OpenURI(link);
} }
}; };
gameModel->AddNotification(new LinkNotification(notification.second, notification.first)); gameModel->AddNotification(new LinkNotification(notification.link, notification.text));
} }
void GameController::NotifyUpdateAvailable(Client * sender) void GameController::NotifyUpdateAvailable(Client * sender)
@ -1564,7 +1559,13 @@ void GameController::NotifyUpdateAvailable(Client * sender)
void Action() override 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; StringBuilder updateMessage;
if (Platform::CanUpdate()) if (Platform::CanUpdate())
{ {
@ -1593,36 +1594,41 @@ void GameController::NotifyUpdateAvailable(Client * sender)
} }
updateMessage << "\nNew version:\n "; 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) if constexpr (MOD)
{ {
updateMessage << "Mod version " << info.Time; updateMessage << "Mod version " << info.build;
} }
else 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()) if (info.changeLog.length())
updateMessage << "\n\nChangelog:\n" << info.Changelog; 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) if constexpr (MOD)
{ {
gameModel->AddNotification(new UpdateNotification(this, "A new mod update is available - click here to update")); 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")); gameModel->AddNotification(new UpdateNotification(this, "A new snapshot is available - click here to update"));
} }
break; break;
case UpdateInfo::Stable: case UpdateInfo::channelStable:
gameModel->AddNotification(new UpdateNotification(this, "A new version is available - click here to update")); gameModel->AddNotification(new UpdateNotification(this, "A new version is available - click here to update"));
break; break;
case UpdateInfo::Beta: case UpdateInfo::channelBeta:
gameModel->AddNotification(new UpdateNotification(this, "A new beta is available - click here to update")); gameModel->AddNotification(new UpdateNotification(this, "A new beta is available - click here to update"));
break; break;
} }
@ -1646,25 +1652,16 @@ void GameController::RemoveNotification(Notification * notification)
gameModel->RemoveNotification(notification); gameModel->RemoveNotification(notification);
} }
void GameController::RunUpdater() void GameController::RunUpdater(UpdateInfo info)
{ {
if (Platform::CanUpdate()) if (Platform::CanUpdate())
{ {
Exit(); Exit();
new UpdateActivity(); new UpdateActivity(info);
} }
else else
{ {
ByteString file; Platform::OpenURI(info.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);
} }
} }

View File

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

View File

@ -17,6 +17,7 @@
#include "client/GameSave.h" #include "client/GameSave.h"
#include "client/SaveFile.h" #include "client/SaveFile.h"
#include "client/SaveInfo.h" #include "client/SaveInfo.h"
#include "client/http/ExecVoteRequest.h"
#include "common/platform/Platform.h" #include "common/platform/Platform.h"
#include "graphics/Renderer.h" #include "graphics/Renderer.h"
#include "simulation/Air.h" #include "simulation/Air.h"
@ -30,6 +31,7 @@
#include "simulation/ToolClasses.h" #include "simulation/ToolClasses.h"
#include "gui/game/DecorationTool.h" #include "gui/game/DecorationTool.h"
#include "gui/interface/Engine.h" #include "gui/interface/Engine.h"
#include "gui/dialogues/ErrorMessage.h"
#include <iostream> #include <iostream>
#include <algorithm> #include <algorithm>
#include <optional> #include <optional>
@ -780,18 +782,33 @@ void GameModel::SetUndoHistoryLimit(unsigned int undoHistoryLimit_)
void GameModel::SetVote(int direction) void GameModel::SetVote(int direction)
{ {
if(currentSave) queuedVote = direction;
}
void GameModel::Tick()
{ {
RequestStatus status = Client::Ref().ExecVote(currentSave->GetID(), direction); if (execVoteRequest && execVoteRequest->CheckDone())
if(status == RequestOkay)
{ {
currentSave->vote = direction; try
{
execVoteRequest->Finish();
currentSave->vote = execVoteRequest->Direction();
notifySaveChanged(); 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 <vector>
#include <deque> #include <deque>
#include <memory> #include <memory>
#include <optional>
class Menu; class Menu;
class Tool; class Tool;
@ -21,6 +22,11 @@ class Snapshot;
struct SnapshotDelta; struct SnapshotDelta;
class GameSave; class GameSave;
namespace http
{
class ExecVoteRequest;
};
class ToolSelection class ToolSelection
{ {
public: public:
@ -40,6 +46,8 @@ struct HistoryEntry
class GameModel class GameModel
{ {
std::unique_ptr<http::ExecVoteRequest> execVoteRequest;
private: private:
std::vector<Notification*> notifications; std::vector<Notification*> notifications;
//int clipboardSize; //int clipboardSize;
@ -119,10 +127,14 @@ private:
void SaveToSimParameters(const GameSave &saveData); void SaveToSimParameters(const GameSave &saveData);
std::optional<int> queuedVote;
public: public:
GameModel(); GameModel();
~GameModel(); ~GameModel();
void Tick();
void SetEdgeMode(int edgeMode); void SetEdgeMode(int edgeMode);
int GetEdgeMode(); int GetEdgeMode();
void SetTemperatureScale(int temperatureScale); 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(); c->ReloadSim();
break; break;
case SDL_SCANCODE_A: case SDL_SCANCODE_A:
if ((Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator if (Client::Ref().GetAuthUser().UserElevation != User::ElevationNone && ctrl)
|| Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin) && ctrl)
{ {
ByteString authorString = Client::Ref().GetAuthorInfo().toStyledString(); ByteString authorString = Client::Ref().GetAuthorInfo().toStyledString();
new InformationMessage("Save authorship info", authorString.FromUtf8(), true); new InformationMessage("Save authorship info", authorString.FromUtf8(), true);

View File

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

View File

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

View File

@ -1,39 +1,32 @@
#include "LoginModel.h" #include "LoginModel.h"
#include "LoginView.h" #include "LoginView.h"
#include "client/Client.h" #include "client/Client.h"
#include "client/http/LoginRequest.h"
LoginModel::LoginModel(): #include "client/http/LogoutRequest.h"
currentUser(0, "")
{
}
void LoginModel::Login(ByteString username, ByteString password) void LoginModel::Login(ByteString username, ByteString password)
{ {
if (username.Contains("@")) 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"; 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(); notifyStatusChanged();
return; return;
} }
statusText = "Logging in..."; statusText = "Logging in...";
loginStatus = false; loginStatus = loginWorking;
notifyStatusChanged(); notifyStatusChanged();
LoginStatus status = Client::Ref().Login(username, password, currentUser); loginRequest = std::make_unique<http::LoginRequest>(username, password);
switch(status) loginRequest->Start();
{
case LoginOkay:
statusText = "Logged in";
loginStatus = true;
break;
case LoginError:
statusText = Client::Ref().GetLastError();
break;
} }
void LoginModel::Logout()
{
statusText = "Logging out...";
loginStatus = loginWorking;
notifyStatusChanged(); notifyStatusChanged();
logoutRequest = std::make_unique<http::LogoutRequest>();
logoutRequest->Start();
} }
void LoginModel::AddObserver(LoginView * observer) void LoginModel::AddObserver(LoginView * observer)
@ -46,14 +39,47 @@ String LoginModel::GetStatusText()
return statusText; 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);
} }
statusText = "Logged in";
bool LoginModel::GetStatus() 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() 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 "common/String.h"
#include "client/User.h" #include "client/User.h"
#include <vector> #include <vector>
#include <memory>
namespace http
{
class LoginRequest;
class LogoutRequest;
}
enum LoginStatus
{
loginIdle,
loginWorking,
loginSucceeded,
};
class LoginView; class LoginView;
class LoginModel class LoginModel
{ {
std::unique_ptr<http::LoginRequest> loginRequest;
std::unique_ptr<http::LogoutRequest> logoutRequest;
std::vector<LoginView*> observers; std::vector<LoginView*> observers;
String statusText; String statusText;
bool loginStatus; LoginStatus loginStatus = loginIdle;
void notifyStatusChanged(); void notifyStatusChanged();
User currentUser;
public: public:
LoginModel();
void Login(ByteString username, ByteString password); void Login(ByteString username, ByteString password);
void Logout();
void AddObserver(LoginView * observer); void AddObserver(LoginView * observer);
String GetStatusText(); String GetStatusText();
bool GetStatus(); LoginStatus GetStatus() const
{
return loginStatus;
}
void Tick();
User GetUser(); User GetUser();
virtual ~LoginModel(); ~LoginModel();
}; };

View File

@ -1,18 +1,13 @@
#include "LoginView.h" #include "LoginView.h"
#include "LoginModel.h" #include "LoginModel.h"
#include "LoginController.h" #include "LoginController.h"
#include "graphics/Graphics.h" #include "graphics/Graphics.h"
#include "gui/interface/Button.h" #include "gui/interface/Button.h"
#include "gui/interface/Label.h" #include "gui/interface/Label.h"
#include "gui/interface/Textbox.h" #include "gui/interface/Textbox.h"
#include "gui/Style.h" #include "gui/Style.h"
#include "client/Client.h" #include "client/Client.h"
#include "Misc.h" #include "Misc.h"
#include <SDL.h> #include <SDL.h>
LoginView::LoginView(): LoginView::LoginView():
@ -39,11 +34,15 @@ LoginView::LoginView():
loginButton->Appearance.HorizontalAlign = ui::Appearance::AlignRight; loginButton->Appearance.HorizontalAlign = ui::Appearance::AlignRight;
loginButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; loginButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
loginButton->Appearance.TextInactive = style::Colour::ConfirmButton; 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); AddComponent(cancelButton);
cancelButton->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; cancelButton->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
cancelButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; cancelButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
cancelButton->SetActionCallback({ [this] { c->Exit(); } }); cancelButton->SetActionCallback({ [this] {
c->Logout();
} });
AddComponent(titleLabel); AddComponent(titleLabel);
titleLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; titleLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
titleLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; titleLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
@ -85,12 +84,17 @@ void LoginView::NotifyStatusChanged(LoginModel * sender)
targetSize.Y = 87; targetSize.Y = 87;
infoLabel->SetText(sender->GetStatusText()); infoLabel->SetText(sender->GetStatusText());
infoLabel->AutoHeight(); 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()) if (sender->GetStatusText().length())
{ {
targetSize.Y += infoLabel->Size.Y+2; targetSize.Y += infoLabel->Size.Y+2;
infoLabel->Visible = true; infoLabel->Visible = true;
} }
if(sender->GetStatus()) if (sender->GetStatus() == loginSucceeded)
{ {
c->Exit(); c->Exit();
} }
@ -98,6 +102,7 @@ void LoginView::NotifyStatusChanged(LoginModel * sender)
void LoginView::OnTick(float dt) void LoginView::OnTick(float dt)
{ {
c->Tick();
//if(targetSize != Size) //if(targetSize != Size)
{ {
ui::Point difference = 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/Client.h"
#include "client/SaveInfo.h" #include "client/SaveInfo.h"
#include "client/GameSave.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 "common/platform/Platform.h"
#include "graphics/Graphics.h" #include "graphics/Graphics.h"
#include "gui/dialogues/ErrorMessage.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() void PreviewController::ShowLogin()
{ {
loginWindow = new LoginController(); loginWindow = new LoginController();
@ -106,32 +86,11 @@ void PreviewController::DoOpen()
previewModel->SetDoOpen(true); 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() void PreviewController::FavouriteSave()
{ {
if (previewModel->GetSaveInfo() && Client::Ref().GetAuthUser().UserID) if (previewModel->GetSaveInfo() && Client::Ref().GetAuthUser().UserID)
{ {
try previewModel->SetFavourite(!previewModel->GetSaveInfo()->Favourite);
{
if(previewModel->GetSaveInfo()->Favourite)
previewModel->SetFavourite(false);
else
previewModel->SetFavourite(true);
}
catch (PreviewModelException & e)
{
new ErrorMessage("Error", ByteString(e.what()).FromUtf8());
}
} }
} }
@ -161,6 +120,12 @@ bool PreviewController::PrevCommentPage()
return false; return false;
} }
void PreviewController::CommentAdded()
{
previewModel->CommentAdded();
previewModel->UpdateComments(1);
}
void PreviewController::Exit() void PreviewController::Exit()
{ {
previewView->CloseActiveWindow(); previewView->CloseActiveWindow();

View File

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

View File

@ -1,46 +1,27 @@
#include "PreviewModel.h" #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 "Format.h"
#include "Misc.h"
#include "client/Client.h" #include "client/Client.h"
#include "client/GameSave.h" #include "client/GameSave.h"
#include "client/SaveInfo.h" #include "client/SaveInfo.h"
#include "gui/dialogues/ErrorMessage.h" #include "gui/dialogues/ErrorMessage.h"
#include "gui/preview/Comment.h"
#include "PreviewModelException.h" #include "PreviewModelException.h"
#include "PreviewView.h" #include "PreviewView.h"
#include "Config.h" #include "Config.h"
#include <cmath> #include <cmath>
#include <iostream> #include <iostream>
PreviewModel::PreviewModel(): constexpr auto commentsPerPage = 20;
doOpen(false),
canOpen(true),
saveData(NULL),
saveComments(NULL),
commentBoxEnabled(false),
commentsLoaded(false),
commentsTotal(0),
commentsPageNumber(1)
{
}
PreviewModel::~PreviewModel()
{
delete saveData;
ClearComments();
}
void PreviewModel::SetFavourite(bool favourite) void PreviewModel::SetFavourite(bool favourite)
{ {
if (saveInfo) if (saveInfo)
{ {
if (Client::Ref().FavouriteSave(saveInfo->id, favourite) == RequestOkay) queuedFavourite = favourite;
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();
} }
} }
@ -64,37 +45,22 @@ void PreviewModel::UpdateSave(int saveID, int saveDate)
this->saveDate = saveDate; this->saveDate = saveDate;
saveInfo.reset(); saveInfo.reset();
if (saveData) saveData.reset();
{ saveComments.reset();
delete saveData;
saveData = NULL;
}
ClearComments();
notifySaveChanged(); notifySaveChanged();
notifySaveCommentsChanged(); notifySaveCommentsChanged();
ByteString url; saveDataDownload = std::make_unique<http::GetSaveDataRequest>(saveID, saveDate);
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->Start(); saveDataDownload->Start();
url = ByteString::Build(SCHEME, SERVER , "/Browse/View.json?ID=", saveID); saveInfoDownload = std::make_unique<http::GetSaveRequest>(saveID, saveDate);
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->Start(); saveInfoDownload->Start();
if (!GetDoOpen()) if (!GetDoOpen())
{ {
commentsLoaded = false; commentsLoaded = false;
url = ByteString::Build(SCHEME, SERVER, "/Browse/Comments.json?ID=", saveID, "&Start=", (commentsPageNumber-1)*20, "&Count=20"); commentsDownload = std::make_unique<http::GetCommentsRequest>(saveID, (commentsPageNumber - 1) * commentsPerPage, commentsPerPage);
commentsDownload = std::make_unique<http::Request>(url);
commentsDownload->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID);
commentsDownload->Start(); commentsDownload->Start();
} }
} }
@ -131,7 +97,7 @@ int PreviewModel::GetCommentsPageNum()
int PreviewModel::GetCommentsPageCount() int PreviewModel::GetCommentsPageCount()
{ {
return std::max(1, (int)(ceil(commentsTotal/20.0f))); return std::max(1, ceilDiv(commentsTotal, commentsPerPage).first);
} }
bool PreviewModel::GetCommentsLoaded() bool PreviewModel::GetCommentsLoaded()
@ -144,14 +110,12 @@ void PreviewModel::UpdateComments(int pageNumber)
if (commentsLoaded) if (commentsLoaded)
{ {
commentsLoaded = false; commentsLoaded = false;
ClearComments(); saveComments.reset();
commentsPageNumber = pageNumber; commentsPageNumber = pageNumber;
if (!GetDoOpen()) if (!GetDoOpen())
{ {
ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/Comments.json?ID=", saveID, "&Start=", (commentsPageNumber-1)*20, "&Count=20"); commentsDownload = std::make_unique<http::GetCommentsRequest>(saveID, (commentsPageNumber - 1) * commentsPerPage, commentsPerPage);
commentsDownload = std::make_unique<http::Request>(url);
commentsDownload->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID);
commentsDownload->Start(); commentsDownload->Start();
} }
@ -189,175 +153,105 @@ void PreviewModel::OnSaveReady()
notifySaveCommentsChanged(); 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() void PreviewModel::Update()
{ {
auto triggerOnSaveReady = false;
if (saveDataDownload && saveDataDownload->CheckDone()) if (saveDataDownload && saveDataDownload->CheckDone())
{ {
auto [ status, ret ] = saveDataDownload->Finish(); try
ByteString nothing;
Client::Ref().ParseServerReturn(nothing, status, true);
if (status == 200 && ret.size())
{ {
delete saveData; saveData = saveDataDownload->Finish();
saveData = new std::vector<char>(ret.begin(), ret.end()); triggerOnSaveReady = true;
if (saveInfo && saveData)
OnSaveReady();
} }
else catch (const http::RequestError &ex)
{ {
auto why = ByteString(ex.what()).FromUtf8();
for (size_t i = 0; i < observers.size(); i++) for (size_t i = 0; i < observers.size(); i++)
{ {
observers[i]->SaveLoadingError(Client::Ref().GetLastError()); observers[i]->SaveLoadingError(why);
} }
} }
saveDataDownload.reset(); saveDataDownload.reset();
} }
if (saveInfoDownload && saveInfoDownload->CheckDone()) if (saveInfoDownload && saveInfoDownload->CheckDone())
{ {
auto [ status, ret ] = saveInfoDownload->Finish(); try
ByteString nothing;
Client::Ref().ParseServerReturn(nothing, status, true);
if (status == 200 && ret.size())
{ {
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) saveData.reset();
OnSaveReady(); 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++) 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(why);
observers[i]->SaveLoadingError(Client::Ref().GetLastError()); }
} }
saveInfoDownload.reset(); saveInfoDownload.reset();
} }
if (triggerOnSaveReady && saveInfo && saveData)
{
OnSaveReady();
}
if (commentsDownload && commentsDownload->CheckDone()) if (commentsDownload && commentsDownload->CheckDone())
{ {
auto [ status, ret ] = commentsDownload->Finish(); try
ClearComments(); {
saveComments = commentsDownload->Finish();
ByteString nothing; }
Client::Ref().ParseServerReturn(nothing, status, true); catch (const http::RequestError &ex)
if (status == 200 && ret.size()) {
ParseComments(ret); // TODO: handle
}
commentsLoaded = true; commentsLoaded = true;
notifySaveCommentsChanged(); notifySaveCommentsChanged();
notifyCommentsPageChanged(); notifyCommentsPageChanged();
commentsDownload.reset(); 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() void PreviewModel::notifySaveChanged()

View File

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

View File

@ -4,6 +4,8 @@
#include "client/Client.h" #include "client/Client.h"
#include "client/SaveInfo.h" #include "client/SaveInfo.h"
#include "client/http/AddCommentRequest.h"
#include "client/http/ReportSaveRequest.h"
#include "gui/dialogues/TextPrompt.h" #include "gui/dialogues/TextPrompt.h"
#include "gui/profile/ProfileActivity.h" #include "gui/profile/ProfileActivity.h"
@ -17,12 +19,12 @@
#include "gui/interface/Textbox.h" #include "gui/interface/Textbox.h"
#include "gui/interface/Engine.h" #include "gui/interface/Engine.h"
#include "gui/dialogues/ErrorMessage.h" #include "gui/dialogues/ErrorMessage.h"
#include "gui/dialogues/InformationMessage.h"
#include "gui/interface/Point.h" #include "gui/interface/Point.h"
#include "gui/interface/Window.h" #include "gui/interface/Window.h"
#include "gui/Style.h" #include "gui/Style.h"
#include "common/tpt-rand.h" #include "common/tpt-rand.h"
#include "Comment.h"
#include "Format.h" #include "Format.h"
#include "Misc.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 = new ui::Button(ui::Point(50, Size.Y-19), ui::Point(51, 19), "Fav");
favButton->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; favButton->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
favButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; favButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle;
favButton->SetTogglable(true);
favButton->SetIcon(IconFavourite); favButton->SetIcon(IconFavourite);
favButton->SetActionCallback({ [this] { c->FavouriteSave(); } }); favButton->SetActionCallback({ [this] { c->FavouriteSave(); } });
favButton->Enabled = Client::Ref().GetAuthUser().UserID?true:false; favButton->Enabled = Client::Ref().GetAuthUser().UserID?true:false;
@ -68,7 +71,12 @@ PreviewView::PreviewView(std::unique_ptr<VideoBuffer> newSavePreview):
reportButton->SetIcon(IconReport); reportButton->SetIcon(IconReport);
reportButton->SetActionCallback({ [this] { 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) { 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; reportButton->Enabled = Client::Ref().GetAuthUser().UserID?true:false;
@ -222,7 +230,12 @@ void PreviewView::CheckComment()
if (!commentWarningLabel) if (!commentWarningLabel)
return; return;
String text = addCommentBox->GetText().ToLower(); 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) if (!commentHelpText)
{ {
@ -375,6 +388,37 @@ void PreviewView::OnTick(float dt)
ErrorMessage::Blocking("Error loading save", doErrorMessage); ErrorMessage::Blocking("Error loading save", doErrorMessage);
c->Exit(); 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) void PreviewView::OnTryExit(ExitMethod method)
@ -448,16 +492,16 @@ void PreviewView::NotifySaveChanged(PreviewModel * sender)
if(save->Favourite) if(save->Favourite)
{ {
favButton->Enabled = true; favButton->Enabled = true;
favButton->SetText("Unfav"); favButton->SetToggleState(true);
} }
else if(Client::Ref().GetAuthUser().UserID) else if(Client::Ref().GetAuthUser().UserID)
{ {
favButton->Enabled = true; favButton->Enabled = true;
favButton->SetText("Fav"); favButton->SetToggleState(false);
} }
else else
{ {
favButton->SetText("Fav"); favButton->SetToggleState(false);
favButton->Enabled = false; favButton->Enabled = false;
} }
@ -477,6 +521,7 @@ void PreviewView::NotifySaveChanged(PreviewModel * sender)
saveNameLabel->SetText(""); saveNameLabel->SetText("");
authorDateLabel->SetText(""); authorDateLabel->SetText("");
saveDescriptionLabel->SetText(""); saveDescriptionLabel->SetText("");
favButton->SetToggleState(false);
favButton->Enabled = false; favButton->Enabled = false;
if (!sender->GetCanOpen()) if (!sender->GetCanOpen())
openButton->Enabled = false; openButton->Enabled = false;
@ -488,18 +533,19 @@ void PreviewView::submitComment()
if (addCommentBox) if (addCommentBox)
{ {
String comment = addCommentBox->GetText(); String comment = addCommentBox->GetText();
if (comment.length() < 4)
{
new ErrorMessage("Error", "Comment is too short");
return;
}
submitCommentButton->Enabled = false; submitCommentButton->Enabled = false;
addCommentBox->SetText("");
addCommentBox->SetPlaceholder("Submitting comment"); //This doesn't appear to ever show since no separate thread is created
FocusComponent(NULL); FocusComponent(NULL);
if (!c->SubmitComment(comment)) addCommentRequest = std::make_unique<http::AddCommentRequest>(c->SaveID(), comment);
addCommentBox->SetText(comment); addCommentRequest->Start();
addCommentBox->SetPlaceholder("Add comment"); CheckComment();
submitCommentButton->Enabled = true;
commentBoxAutoHeight();
} }
} }
@ -564,7 +610,7 @@ void PreviewView::NotifyCommentsPageChanged(PreviewModel * sender)
void PreviewView::NotifyCommentsChanged(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++) for (size_t i = 0; i < commentComponents.size(); i++)
{ {
@ -575,8 +621,9 @@ void PreviewView::NotifyCommentsChanged(PreviewModel * sender)
commentTextComponents.clear(); commentTextComponents.clear();
commentsPanel->InnerSize = ui::Point(0, 0); commentsPanel->InnerSize = ui::Point(0, 0);
if (comments) if (commentsPtr)
{ {
auto &comments = *commentsPtr;
for (size_t i = 0; i < commentComponents.size(); i++) for (size_t i = 0; i < commentComponents.size(); i++)
{ {
commentsPanel->RemoveChild(commentComponents[i]); commentsPanel->RemoveChild(commentComponents[i]);
@ -589,11 +636,11 @@ void PreviewView::NotifyCommentsChanged(PreviewModel * sender)
ui::Label * tempUsername; ui::Label * tempUsername;
ui::Label * tempComment; ui::Label * tempComment;
ui::AvatarButton * tempAvatar; ui::AvatarButton * tempAvatar;
for (size_t i = 0; i < comments->size(); i++) for (size_t i = 0; i < comments.size(); i++)
{ {
if (showAvatars) 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] { tempAvatar->SetActionCallback({ [tempAvatar] {
if (tempAvatar->GetUsername().size() > 0) if (tempAvatar->GetUsername().size() > 0)
{ {
@ -604,25 +651,38 @@ void PreviewView::NotifyCommentsChanged(PreviewModel * sender)
commentsPanel->AddChild(tempAvatar); 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) 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 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.HorizontalAlign = ui::Appearance::AlignLeft;
tempUsername->Appearance.VerticalAlign = ui::Appearance::AlignBottom; 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; currentY += 16;
commentComponents.push_back(tempUsername); commentComponents.push_back(tempUsername);
commentsPanel->AddChild(tempUsername); commentsPanel->AddChild(tempUsername);
if (showAvatars) 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 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->SetMultiline(true);
tempComment->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; tempComment->Appearance.HorizontalAlign = ui::Appearance::AlignLeft;
tempComment->Appearance.VerticalAlign = ui::Appearance::AlignTop; tempComment->Appearance.VerticalAlign = ui::Appearance::AlignTop;

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
#include "ServerSaveActivity.h" #include "ServerSaveActivity.h"
#include "graphics/Graphics.h" #include "graphics/Graphics.h"
#include "gui/interface/Label.h" #include "gui/interface/Label.h"
#include "gui/interface/Textbox.h" #include "gui/interface/Textbox.h"
#include "gui/interface/Button.h" #include "gui/interface/Button.h"
@ -10,13 +8,11 @@
#include "gui/dialogues/SaveIDMessage.h" #include "gui/dialogues/SaveIDMessage.h"
#include "gui/dialogues/ConfirmPrompt.h" #include "gui/dialogues/ConfirmPrompt.h"
#include "gui/dialogues/InformationMessage.h" #include "gui/dialogues/InformationMessage.h"
#include "client/Client.h" #include "client/Client.h"
#include "client/ThumbnailRendererTask.h" #include "client/ThumbnailRendererTask.h"
#include "client/GameSave.h" #include "client/GameSave.h"
#include "client/http/UploadSaveRequest.h"
#include "tasks/Task.h" #include "tasks/Task.h"
#include "gui/Style.h" #include "gui/Style.h"
class SaveUploadTask: public Task class SaveUploadTask: public Task
@ -36,7 +32,19 @@ class SaveUploadTask: public Task
bool doWork() override bool doWork() override
{ {
notifyProgress(-1); 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: public:
@ -168,7 +176,7 @@ void ServerSaveActivity::NotifyDone(Task * task)
if(!task->GetSuccess()) if(!task->GetSuccess())
{ {
Exit(); Exit();
new ErrorMessage("Error", Client::Ref().GetLastError()); new ErrorMessage("Error", task->GetError());
} }
else else
{ {
@ -182,26 +190,22 @@ void ServerSaveActivity::NotifyDone(Task * task)
void ServerSaveActivity::Save() 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()) 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] { 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(); saveUpload();
} }); } });
} }
else else
{ {
Exit();
saveUpload(); saveUpload();
} }
} }
else
{
new ErrorMessage("Error", "You must specify a save name.");
}
}
void ServerSaveActivity::AddAuthorInfo() void ServerSaveActivity::AddAuthorInfo()
{ {
@ -223,6 +227,7 @@ void ServerSaveActivity::AddAuthorInfo()
void ServerSaveActivity::saveUpload() void ServerSaveActivity::saveUpload()
{ {
okayButton->Enabled = false;
save->SetName(nameField->GetText()); save->SetName(nameField->GetText());
save->SetDescription(descriptionField->GetText()); save->SetDescription(descriptionField->GetText());
save->SetPublished(publishedCheckbox->GetChecked()); save->SetPublished(publishedCheckbox->GetChecked());
@ -234,16 +239,8 @@ void ServerSaveActivity::saveUpload()
save->SetGameSave(std::move(gameSave)); save->SetGameSave(std::move(gameSave));
} }
AddAuthorInfo(); AddAuthorInfo();
uploadSaveRequest = std::make_unique<http::UploadSaveRequest>(*save);
if(Client::Ref().UploadSave(*save) != RequestOkay) uploadSaveRequest->Start();
{
new ErrorMessage("Error", "Upload failed with error:\n"+Client::Ref().GetLastError());
}
else if (onUploaded)
{
new SaveIDMessage(save->GetID());
onUploaded(std::move(save));
}
} }
void ServerSaveActivity::Exit() 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) if(saveUploadTask)
saveUploadTask->Poll(); saveUploadTask->Poll();
} }

View File

@ -13,6 +13,11 @@
#include "save_online.png.h" #include "save_online.png.h"
namespace http
{
class UploadSaveRequest;
}
namespace ui namespace ui
{ {
class Label; class Label;
@ -25,6 +30,8 @@ class Task;
class VideoBuffer; class VideoBuffer;
class ServerSaveActivity: public WindowActivity, public TaskListener class ServerSaveActivity: public WindowActivity, public TaskListener
{ {
std::unique_ptr<http::UploadSaveRequest> uploadSaveRequest;
using OnUploaded = std::function<void (std::unique_ptr<SaveInfo>)>; using OnUploaded = std::function<void (std::unique_ptr<SaveInfo>)>;
std::unique_ptr<PlaneAdapter<std::vector<pixel_rgba>>> saveToServerImage = format::PixelsFromPNG( std::unique_ptr<PlaneAdapter<std::vector<pixel_rgba>>> saveToServerImage = format::PixelsFromPNG(
std::vector<char>(save_online_png, save_online_png + save_online_png_size) 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/Client.h"
#include "client/SaveInfo.h" #include "client/SaveInfo.h"
#include "client/GameSave.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/platform/Platform.h"
#include "common/tpt-minmax.h" #include "common/tpt-minmax.h"
#include "graphics/Graphics.h" #include "graphics/Graphics.h"
@ -132,13 +138,13 @@ void SearchController::SetPageRelative(int offset)
void SearchController::ChangeSort() void SearchController::ChangeSort()
{ {
if(searchModel->GetSort() == "new") if(searchModel->GetSort() == http::sortByDate)
{ {
searchModel->SetSort("best"); searchModel->SetSort(http::sortByVotes);
} }
else else
{ {
searchModel->SetSort("new"); searchModel->SetSort(http::sortByDate);
} }
searchModel->UpdateSaveList(1, searchModel->GetLastQuery()); searchModel->UpdateSaveList(1, searchModel->GetLastQuery());
} }
@ -183,7 +189,7 @@ void SearchController::SelectAllSaves()
if (!Client::Ref().GetAuthUser().UserID) if (!Client::Ref().GetAuthUser().UserID)
return; return;
if (searchModel->GetShowOwn() || if (searchModel->GetShowOwn() ||
Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod ||
Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin) Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin)
searchModel->SelectAllSaves(); searchModel->SelectAllSaves();
@ -245,9 +251,16 @@ void SearchController::removeSelectedC()
for (size_t i = 0; i < saves.size(); i++) for (size_t i = 0; i < saves.size(); i++)
{ {
notifyStatus(String::Build("Deleting save [", saves[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(); c->Refresh();
return false; return false;
} }
@ -286,37 +299,49 @@ void SearchController::unpublishSelectedC(bool publish)
public: public:
UnpublishSavesTask(std::vector<int> saves_, SearchController *c_, bool publish_) { saves = saves_; c = c_; publish = publish_; } 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, "]")); notifyStatus(String::Build("Publishing save [", saveID, "]"));
if (Client::Ref().PublishSave(saveID) != RequestOkay) auto publishSaveRequest = std::make_unique<http::PublishSaveRequest>(saveID);
return false; publishSaveRequest->Start();
return true; publishSaveRequest->Wait();
publishSaveRequest->Finish();
} }
bool UnpublishSave(int saveID) void UnpublishSave(int saveID)
{ {
notifyStatus(String::Build("Unpublishing save [", saveID, "]")); notifyStatus(String::Build("Unpublishing save [", saveID, "]"));
if (Client::Ref().UnpublishSave(saveID) != RequestOkay) auto unpublishSaveRequest = std::make_unique<http::UnpublishSaveRequest>(saveID);
return false; unpublishSaveRequest->Start();
return true; unpublishSaveRequest->Wait();
unpublishSaveRequest->Finish();
} }
bool doWork() override bool doWork() override
{ {
bool ret;
for (size_t i = 0; i < saves.size(); i++) for (size_t i = 0; i < saves.size(); i++)
{
try
{ {
if (publish) if (publish)
ret = PublishSave(saves[i]); {
PublishSave(saves[i]);
}
else 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 if (publish) // uses html page so error message will be spam
{
notifyError(String::Build("Failed to publish [", saves[i], "], is this save yours?")); notifyError(String::Build("Failed to publish [", saves[i], "], is this save yours?"));
}
else 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(); c->Refresh();
return false; return false;
} }
@ -343,9 +368,16 @@ void SearchController::FavouriteSelected()
for (size_t i = 0; i < saves.size(); i++) for (size_t i = 0; i < saves.size(); i++)
{ {
notifyStatus(String::Build("Favouring save [", saves[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; return false;
} }
notifyProgress((i + 1) * 100 / saves.size()); notifyProgress((i + 1) * 100 / saves.size());
@ -364,9 +396,16 @@ void SearchController::FavouriteSelected()
for (size_t i = 0; i < saves.size(); i++) for (size_t i = 0; i < saves.size(); i++)
{ {
notifyStatus(String::Build("Unfavouring save [", saves[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; return false;
} }
notifyProgress((i + 1) * 100 / saves.size()); notifyProgress((i + 1) * 100 / saves.size());

View File

@ -4,12 +4,14 @@
#include "client/SaveInfo.h" #include "client/SaveInfo.h"
#include "client/GameSave.h" #include "client/GameSave.h"
#include "client/Client.h" #include "client/Client.h"
#include "client/http/SearchSavesRequest.h"
#include "client/http/SearchTagsRequest.h"
#include "common/tpt-minmax.h" #include "common/tpt-minmax.h"
#include <thread> #include <thread>
#include <cmath> #include <cmath>
SearchModel::SearchModel(): SearchModel::SearchModel():
currentSort("best"), currentSort(http::sortByVotes),
currentPage(1), currentPage(1),
resultCount(0), resultCount(0),
showOwn(false), showOwn(false),
@ -28,81 +30,26 @@ bool SearchModel::GetShowTags()
return showTags; 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 = ""; lastError = "";
resultCount = 0; resultCount = 0;
ByteStringBuilder urlStream; searchSaves = std::make_unique<http::SearchSavesRequest>(start, count, query.ToUtf8(), sort, category);
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->Start(); searchSaves->Start();
} }
std::vector<std::unique_ptr<SaveInfo>> SearchModel::EndSearchSaves() std::vector<std::unique_ptr<SaveInfo>> SearchModel::EndSearchSaves()
{ {
std::vector<std::unique_ptr<SaveInfo>> saveArray; 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 try
{ {
std::istringstream dataStream(data); std::tie(resultCount, saveArray) = searchSaves->Finish();
Json::Value objDocument; }
dataStream >> objDocument; catch (const http::RequestError &ex)
resultCount = objDocument["Count"].asInt();
Json::Value savesArray = objDocument["Saves"];
for (Json::UInt j = 0; j < savesArray.size(); j++)
{ {
int tempID = savesArray[j]["ID"].asInt(); lastError = ByteString(ex.what()).FromUtf8();
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();
} }
searchSaves.reset();
return saveArray; return saveArray;
} }
@ -117,40 +64,22 @@ void SearchModel::BeginGetTags(int start, int count, String query)
if(query.length()) if(query.length())
urlStream << format::URLEncode(query.ToUtf8()); 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(); getTags->Start();
} }
std::vector<std::pair<ByteString, int>> SearchModel::EndGetTags() std::vector<std::pair<ByteString, int>> SearchModel::EndGetTags()
{ {
std::vector<std::pair<ByteString, int>> tagArray; std::vector<std::pair<ByteString, int>> tagArray;
auto [ dataStatus, data ] = getTags->Finish();
getTags.reset();
if(dataStatus == 200 && data.size())
{
try try
{ {
std::istringstream dataStream(data); tagArray = getTags->Finish();
Json::Value objDocument; }
dataStream >> objDocument; catch (const http::RequestError &ex)
Json::Value tagsArray = objDocument["Tags"];
for (Json::UInt j = 0; j < tagsArray.size(); j++)
{ {
int tagCount = tagsArray[j]["Count"].asInt(); lastError = ByteString(ex.what()).FromUtf8();
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);
} }
getTags.reset();
return tagArray; return tagArray;
} }
@ -166,7 +95,7 @@ bool SearchModel::UpdateSaveList(int pageNumber, String query)
//resultCount = 0; //resultCount = 0;
currentPage = pageNumber; currentPage = pageNumber;
if(pageNumber == 1 && !showOwn && !showFavourite && currentSort == "best" && query == "") if(pageNumber == 1 && !showOwn && !showFavourite && currentSort == http::sortByVotes && query == "")
SetShowTags(true); SetShowTags(true);
else else
SetShowTags(false); SetShowTags(false);
@ -182,12 +111,16 @@ bool SearchModel::UpdateSaveList(int pageNumber, String query)
BeginGetTags(0, 24, ""); BeginGetTags(0, 24, "");
} }
ByteString category = ""; auto category = http::categoryNone;
if (showFavourite) if (showFavourite)
category = "Favourites"; {
category = http::categoryFavourites;
}
if (showOwn && Client::Ref().GetAuthUser().UserID) 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 true;
} }
return false; return false;
@ -363,7 +296,7 @@ void SearchModel::notifySelectedChanged()
int SearchModel::GetPageCount() 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) return std::max(1, (int)(ceil(resultCount/20.0f))+1); //add one for front page (front page saves are repeated twice)
else else
return std::max(1, (int)(ceil(resultCount/20.0f))); return std::max(1, (int)(ceil(resultCount/20.0f)));

View File

@ -1,26 +1,32 @@
#pragma once #pragma once
#include "common/String.h" #include "common/String.h"
#include "client/http/Request.h" #include "client/Search.h"
#include "Config.h" #include "Config.h"
#include <vector> #include <vector>
#include <atomic> #include <atomic>
#include <memory> #include <memory>
namespace http
{
class SearchSavesRequest;
class SearchTagsRequest;
}
class SaveInfo; class SaveInfo;
class SearchView; class SearchView;
class SearchModel class SearchModel
{ {
private: private:
std::unique_ptr<http::Request> searchSaves; std::unique_ptr<http::SearchSavesRequest> searchSaves;
void BeginSearchSaves(int start, int count, String query, ByteString sort, ByteString category); void BeginSearchSaves(int start, int count, String query, http::Sort sort, http::Category category);
std::vector<std::unique_ptr<SaveInfo>> EndSearchSaves(); std::vector<std::unique_ptr<SaveInfo>> EndSearchSaves();
void BeginGetTags(int start, int count, String query); void BeginGetTags(int start, int count, String query);
std::vector<std::pair<ByteString, int>> EndGetTags(); std::vector<std::pair<ByteString, int>> EndGetTags();
std::unique_ptr<http::Request> getTags; std::unique_ptr<http::SearchTagsRequest> getTags;
std::unique_ptr<SaveInfo> loadedSave; std::unique_ptr<SaveInfo> loadedSave;
ByteString currentSort; http::Sort currentSort;
String lastQuery; String lastQuery;
String lastError; String lastError;
std::vector<int> selected; std::vector<int> selected;
@ -55,8 +61,8 @@ public:
int GetPageCount(); int GetPageCount();
int GetPageNum() { return currentPage; } int GetPageNum() { return currentPage; }
String GetLastQuery() { return lastQuery; } String GetLastQuery() { return lastQuery; }
void SetSort(ByteString sort) { if(!searchSaves) { currentSort = sort; } notifySortChanged(); } void SetSort(http::Sort sort) { if(!searchSaves) { currentSort = sort; } notifySortChanged(); }
ByteString GetSort() { return currentSort; } http::Sort GetSort() { return currentSort; }
void SetShowOwn(bool show) { if(!searchSaves) { if(show!=showOwn) { showOwn = show; } } notifyShowOwnChanged(); } void SetShowOwn(bool show) { if(!searchSaves) { if(show!=showOwn) { showOwn = show; } } notifyShowOwnChanged(); }
bool GetShowOwn() { return showOwn; } bool GetShowOwn() { return showOwn; }
void SetShowFavourite(bool show) { if(show!=showFavourite && !searchSaves) { showFavourite = show; } notifyShowFavouriteChanged(); } 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) void SearchView::NotifySortChanged(SearchModel * sender)
{ {
if(sender->GetSort() == "best") if(sender->GetSort() == http::sortByVotes)
{ {
sortButton->SetToggleState(false); sortButton->SetToggleState(false);
sortButton->SetText("By votes"); sortButton->SetText("By votes");
@ -228,7 +228,7 @@ void SearchView::NotifySortChanged(SearchModel * sender)
void SearchView::NotifyShowOwnChanged(SearchModel * sender) void SearchView::NotifyShowOwnChanged(SearchModel * sender)
{ {
ownButton->SetToggleState(sender->GetShowOwn()); 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; unpublishSelected->Enabled = true;
removeSelected->Enabled = true; removeSelected->Enabled = true;
@ -248,7 +248,7 @@ void SearchView::NotifyShowFavouriteChanged(SearchModel * sender)
unpublishSelected->Enabled = false; unpublishSelected->Enabled = false;
removeSelected->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; unpublishSelected->Enabled = true;
removeSelected->Enabled = true; removeSelected->Enabled = true;
@ -323,7 +323,7 @@ void SearchView::CheckAccess()
favButton->Enabled = true; favButton->Enabled = true;
favouriteSelected->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; unpublishSelected->Enabled = true;
removeSelected->Enabled = true; removeSelected->Enabled = true;
@ -569,7 +569,7 @@ void SearchView::NotifySaveListChanged(SearchModel * sender)
}); });
if(Client::Ref().GetAuthUser().UserID) if(Client::Ref().GetAuthUser().UserID)
saveButton->SetSelectable(true); 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); saveButton->SetShowVotes(true);
saveButtons.push_back(saveButton); saveButtons.push_back(saveButton);
AddComponent(saveButton); AddComponent(saveButton);

View File

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

View File

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

View File

@ -1,13 +1,15 @@
#include "TagsModel.h" #include "TagsModel.h"
#include "TagsView.h" #include "TagsView.h"
#include "TagsModelException.h" #include "TagsModelException.h"
#include "client/Client.h" #include "client/Client.h"
#include "client/SaveInfo.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 */) void TagsModel::SetSave(SaveInfo *newSave /* non-owning */)
{ {
queuedTags.clear();
this->save = newSave; this->save = newSave;
notifyTagsChanged(); notifyTagsChanged();
} }
@ -17,40 +19,73 @@ SaveInfo *TagsModel::GetSave() // non-owning
return save; 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) if (save)
{ {
std::list<ByteString> * tags = Client::Ref().RemoveTag(save->GetID(), tag); save->SetTags(tags);
if(tags) }
{
save->SetTags(std::list<ByteString>(*tags));
notifyTagsChanged(); 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 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) void TagsModel::AddTag(ByteString tag)
{ {
if(save) queuedTags[tag] = true;
{
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());
}
}
} }
void TagsModel::AddObserver(TagsView * observer) void TagsModel::AddObserver(TagsView * observer)

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
#include "UpdateActivity.h" #include "UpdateActivity.h"
#include "client/http/Request.h" #include "client/http/Request.h"
#include "prefs/GlobalPrefs.h" #include "prefs/GlobalPrefs.h"
#include "client/Client.h"
#include "common/platform/Platform.h" #include "common/platform/Platform.h"
#include "tasks/Task.h" #include "tasks/Task.h"
#include "tasks/TaskWindow.h" #include "tasks/TaskWindow.h"
@ -53,7 +52,16 @@ private:
Platform::Millisleep(1); 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) if (status!=200)
{ {
return niceNotifyError("Could not download update: " + String::Build("Server responded with Status ", status)); return niceNotifyError("Could not download update: " + String::Build("Server responded with Status ", status));
@ -107,9 +115,9 @@ private:
} }
}; };
UpdateActivity::UpdateActivity() { UpdateActivity::UpdateActivity(UpdateInfo info)
ByteString file = ByteString::Build(SCHEME, USE_UPDATESERVER ? UPDATESERVER : SERVER, Client::Ref().GetUpdateInfo().File); {
updateDownloadTask = new UpdateDownloadTask(file, this); updateDownloadTask = new UpdateDownloadTask(info.file, this);
updateWindow = new TaskWindow("Downloading update...", updateDownloadTask, true); updateWindow = new TaskWindow("Downloading update...", updateDownloadTask, true);
} }

View File

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

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