diff --git a/src/Misc.h b/src/Misc.h index 2ebc6fe76..5faa307fb 100644 --- a/src/Misc.h +++ b/src/Misc.h @@ -18,6 +18,12 @@ inline std::pair floorDiv(Signed a, Signed b) return { quo, rem }; } +template +inline std::pair ceilDiv(Signed a, Signed b) +{ + return floorDiv(a + b - Signed(1), b); +} + //Linear interpolation template inline T LinearInterpolate(T val1, T val2, T lowerCoord, T upperCoord, T coord) { diff --git a/src/PowderToy.cpp b/src/PowderToy.cpp index e720f0e88..6845670c9 100644 --- a/src/PowderToy.cpp +++ b/src/PowderToy.cpp @@ -7,6 +7,8 @@ #include "client/SaveFile.h" #include "client/SaveInfo.h" #include "client/http/requestmanager/RequestManager.h" +#include "client/http/GetSaveRequest.h" +#include "client/http/GetSaveDataRequest.h" #include "common/platform/Platform.h" #include "graphics/Graphics.h" #include "simulation/SaveRenderer.h" @@ -462,15 +464,31 @@ int main(int argc, char * argv[]) } int saveId = saveIdPart.ToNumber(); - auto newSave = Client::Ref().GetSave(saveId, 0); - if (!newSave) - throw std::runtime_error("Could not load save info"); - auto saveData = Client::Ref().GetSaveData(saveId, 0); - if (!saveData.size()) - throw std::runtime_error(("Could not load save\n" + Client::Ref().GetLastError()).ToUtf8()); - auto newGameSave = std::make_unique(std::move(saveData)); - newSave->SetGameSave(std::move(newGameSave)); - + auto getSave = std::make_unique(saveId, 0); + getSave->Start(); + getSave->Wait(); + std::unique_ptr newSave; + try + { + newSave = getSave->Finish(); + } + catch (const http::RequestError &ex) + { + throw std::runtime_error("Could not load save info\n" + ByteString(ex.what())); + } + auto getSaveData = std::make_unique(saveId, 0); + getSaveData->Start(); + getSaveData->Wait(); + std::unique_ptr saveData; + try + { + saveData = std::make_unique(getSaveData->Finish()); + } + catch (const http::RequestError &ex) + { + throw std::runtime_error("Could not load save\n" + ByteString(ex.what())); + } + newSave->SetGameSave(std::move(saveData)); gameController->LoadSave(std::move(newSave)); } catch (std::exception & e) diff --git a/src/client/Client.cpp b/src/client/Client.cpp index 80c4f0782..29ae55f80 100644 --- a/src/client/Client.cpp +++ b/src/client/Client.cpp @@ -1,6 +1,6 @@ #include "Client.h" #include "prefs/GlobalPrefs.h" -#include "client/http/Request.h" +#include "client/http/StartupRequest.h" #include "ClientListener.h" #include "Format.h" #include "MD5.h" @@ -13,7 +13,6 @@ #include "graphics/Graphics.h" #include "prefs/Prefs.h" #include "lua/CommandInterface.h" -#include "gui/preview/Comment.h" #include "Config.h" #include #include @@ -29,8 +28,6 @@ Client::Client(): messageOfTheDay("Fetching the message of the day..."), - versionCheckRequest(nullptr), - alternateVersionCheckRequest(nullptr), usingAltUpdateServer(false), updateAvailable(false), authUser(0, "") @@ -40,16 +37,7 @@ Client::Client(): authUser.Username = prefs.Get("User.Username", ByteString("")); authUser.SessionID = prefs.Get("User.SessionID", ByteString("")); authUser.SessionKey = prefs.Get("User.SessionKey", ByteString("")); - auto elevation = prefs.Get("User.Elevation", ByteString("")); - authUser.UserElevation = User::ElevationNone; - if (elevation == "Admin") - { - authUser.UserElevation = User::ElevationAdmin; - } - if (elevation == "Mod") - { - authUser.UserElevation = User::ElevationModerator; - } + authUser.UserElevation = prefs.Get("User.Elevation", User::ElevationNone); firstRun = !prefs.BackedByFile(); } @@ -88,24 +76,14 @@ void Client::Initialize() } //Begin version check - versionCheckRequest = std::make_unique(ByteString::Build(SCHEME, SERVER, "/Startup.json")); - - if (authUser.UserID) - { - versionCheckRequest->AuthHeaders(ByteString::Build(authUser.UserID), authUser.SessionID); - } + versionCheckRequest = std::make_unique(false); versionCheckRequest->Start(); - if constexpr (USE_UPDATESERVER) { // use an alternate update server - alternateVersionCheckRequest = std::make_unique(ByteString::Build(SCHEME, UPDATESERVER, "/Startup.json")); - usingAltUpdateServer = true; - if (authUser.UserID) - { - alternateVersionCheckRequest->AuthHeaders(authUser.Username, ""); - } + alternateVersionCheckRequest = std::make_unique(true); alternateVersionCheckRequest->Start(); + usingAltUpdateServer = true; } } @@ -125,203 +103,82 @@ String Client::GetMessageOfTheDay() return messageOfTheDay; } -void Client::AddServerNotification(std::pair notification) +void Client::AddServerNotification(ServerNotification notification) { serverNotifications.push_back(notification); notifyNewNotification(notification); } -std::vector > Client::GetServerNotifications() +std::vector Client::GetServerNotifications() { return serverNotifications; } -RequestStatus Client::ParseServerReturn(ByteString &result, int status, bool json) -{ - lastError = ""; - // no server response, return "Malformed Response" - if (status == 200 && !result.size()) - { - status = 603; - } - if (status == 302) - return RequestOkay; - if (status != 200) - { - lastError = String::Build("HTTP Error ", status, ": ", http::StatusText(status)); - return RequestFailure; - } - - if (json) - { - std::istringstream datastream(result); - Json::Value root; - - try - { - datastream >> root; - // assume everything is fine if an empty [] is returned - if (root.size() == 0) - { - return RequestOkay; - } - int status = root.get("Status", 1).asInt(); - if (status != 1) - { - lastError = ByteString(root.get("Error", "Unspecified Error").asString()).FromUtf8(); - return RequestFailure; - } - } - catch (std::exception &e) - { - // sometimes the server returns a 200 with the text "Error: 401" - if (!strncmp(result.c_str(), "Error: ", 7)) - { - status = ByteString(result.begin() + 7, result.end()).ToNumber(); - lastError = String::Build("HTTP Error ", status, ": ", http::StatusText(status)); - return RequestFailure; - } - lastError = "Could not read response: " + ByteString(e.what()).FromUtf8(); - return RequestFailure; - } - } - else - { - if (strncmp(result.c_str(), "OK", 2)) - { - lastError = result.FromUtf8(); - return RequestFailure; - } - } - return RequestOkay; -} - void Client::Tick() { - CheckUpdate(versionCheckRequest, true); - CheckUpdate(alternateVersionCheckRequest, false); -} - -void Client::CheckUpdate(std::unique_ptr &updateRequest, bool checkSession) -{ - //Check status on version check request - if (updateRequest && updateRequest->CheckDone()) + auto applyUpdateInfo = false; + if (versionCheckRequest && versionCheckRequest->CheckDone()) { - auto [ status, data ] = updateRequest->Finish(); - - if (checkSession && status == 618) + if (versionCheckRequest->StatusCode() == 618) { AddServerNotification({ "Failed to load SSL certificates", ByteString(SCHEME) + "powdertoy.co.uk/FAQ.html" }); } - - if (status != 200) + try { - //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 + auto info = versionCheckRequest->Finish(); + if (!info.sessionGood) { - Json::Value objDocument; - dataStream >> objDocument; - - //Check session - if (checkSession) - { - if (!objDocument["Session"].asBool()) - { - SetAuthUser(User(0, "")); - } - } - - //Notifications from server - 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 item = std::pair(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); - } - } - - if (!updateAvailable) - { - Json::Value betaVersion = versions["Beta"]; - int betaMajor = betaVersion["Major"].asInt(); - int betaMinor = betaVersion["Minor"].asInt(); - int betaBuild = betaVersion["Build"].asInt(); - ByteString betaFile = betaVersion["File"].asString(); - String betaChangelog = ByteString(betaVersion["Changelog"].asString()).FromUtf8(); - if (betaBuild > BUILD_NUM) - { - updateAvailable = true; - updateInfo = UpdateInfo(betaMajor, betaMinor, betaBuild, betaFile, betaChangelog, UpdateInfo::Beta); - } - } - - if constexpr (SNAPSHOT || MOD) - { - Json::Value snapshotVersion = versions["Snapshot"]; - int snapshotSnapshot = snapshotVersion["Snapshot"].asInt(); - ByteString snapshotFile = snapshotVersion["File"].asString(); - String snapshotChangelog = ByteString(snapshotVersion["Changelog"].asString()).FromUtf8(); - if (snapshotSnapshot > SNAPSHOT_ID) - { - updateAvailable = true; - updateInfo = UpdateInfo(snapshotSnapshot, snapshotFile, snapshotChangelog, UpdateInfo::Snapshot); - } - } - - if(updateAvailable) - { - notifyUpdateAvailable(); - } - } - } + SetAuthUser(User(0, "")); } - catch (std::exception & e) + if (!usingAltUpdateServer) { - //Do nothing + updateInfo = info.updateInfo; + applyUpdateInfo = true; + messageOfTheDay = info.messageOfTheDay; + } + for (auto ¬ification : info.notifications) + { + AddServerNotification(notification); } } - updateRequest.reset(); + catch (const http::RequestError &ex) + { + if (!usingAltUpdateServer) + { + messageOfTheDay = ByteString::Build("Error while fetching MotD: ", ex.what()).FromUtf8(); + } + } + versionCheckRequest.reset(); + } + if (alternateVersionCheckRequest && alternateVersionCheckRequest->CheckDone()) + { + try + { + auto info = alternateVersionCheckRequest->Finish(); + updateInfo = info.updateInfo; + applyUpdateInfo = true; + messageOfTheDay = info.messageOfTheDay; + for (auto ¬ification : info.notifications) + { + AddServerNotification(notification); + } + } + catch (const http::RequestError &ex) + { + messageOfTheDay = ByteString::Build("Error while checking for updates: ", ex.what()).FromUtf8(); + } + alternateVersionCheckRequest.reset(); + } + if (applyUpdateInfo && !IGNORE_UPDATES) + { + if (updateInfo) + { + notifyUpdateAvailable(); + } } } -UpdateInfo Client::GetUpdateInfo() +std::optional Client::GetUpdateInfo() { return updateInfo; } @@ -350,7 +207,7 @@ void Client::notifyAuthUserChanged() } } -void Client::notifyNewNotification(std::pair notification) +void Client::notifyNewNotification(ServerNotification notification) { for (std::vector::iterator iterator = listeners.begin(), end = listeners.end(); iterator != end; ++iterator) { @@ -391,16 +248,7 @@ void Client::SetAuthUser(User user) prefs.Set("User.SessionID", authUser.SessionID); prefs.Set("User.SessionKey", authUser.SessionKey); prefs.Set("User.Username", authUser.Username); - ByteString elevation = "None"; - if (authUser.UserElevation == User::ElevationAdmin) - { - elevation = "Admin"; - } - if (authUser.UserElevation == User::ElevationModerator) - { - elevation = "Mod"; - } - prefs.Set("User.Elevation", elevation); + prefs.Set("User.Elevation", authUser.UserElevation); } else { @@ -415,65 +263,6 @@ User Client::GetAuthUser() return authUser; } -RequestStatus Client::UploadSave(SaveInfo & save) -{ - lastError = ""; - int dataStatus; - ByteString data; - ByteString userID = ByteString::Build(authUser.UserID); - if (authUser.UserID) - { - if (!save.GetGameSave()) - { - lastError = "Empty game save"; - return RequestFailure; - } - - save.SetID(0); - - auto [ fromNewerVersion, gameData ] = save.GetGameSave()->Serialise(); - (void)fromNewerVersion; - - if (!gameData.size()) - { - lastError = "Cannot serialize game save"; - return RequestFailure; - } - else if (ALLOW_FAKE_NEWER_VERSION && fromNewerVersion && save.GetPublished()) - { - lastError = "Cannot publish save, incompatible with latest release version."; - return RequestFailure; - } - - std::tie(dataStatus, data) = http::Request::SimpleAuth(ByteString::Build(SCHEME, SERVER, "/Save.api"), userID, authUser.SessionID, { - { "Name", save.GetName().ToUtf8() }, - { "Description", save.GetDescription().ToUtf8() }, - { "Data:save.bin", ByteString(gameData.begin(), gameData.end()) }, - { "Publish", save.GetPublished() ? "Public" : "Private" }, - { "Key", authUser.SessionKey } - }); - } - else - { - lastError = "Not authenticated"; - return RequestFailure; - } - - RequestStatus ret = ParseServerReturn(data, dataStatus, false); - if (ret == RequestOkay) - { - int saveID = ByteString(data.begin() + 3, data.end()).ToNumber(); - if (!saveID) - { - lastError = "Server did not return Save ID"; - ret = RequestFailure; - } - else - save.SetID(saveID); - } - return ret; -} - void Client::MoveStampToFront(ByteString stampID) { auto it = std::find(stampIDs.begin(), stampIDs.end(), stampID); @@ -618,312 +407,6 @@ const std::vector &Client::GetStamps() const return stampIDs; } -RequestStatus Client::ExecVote(int saveID, int direction) -{ - lastError = ""; - int dataStatus; - ByteString data; - - if (authUser.UserID) - { - ByteString saveIDText = ByteString::Build(saveID); - ByteString userIDText = ByteString::Build(authUser.UserID); - std::tie(dataStatus, data) = http::Request::SimpleAuth(ByteString::Build(SCHEME, SERVER, "/Vote.api"), userIDText, authUser.SessionID, { - { "ID", saveIDText }, - { "Action", direction ? (direction == 1 ? "Up" : "Down") : "Reset" }, - { "Key", authUser.SessionKey } - }); - } - else - { - lastError = "Not authenticated"; - return RequestFailure; - } - RequestStatus ret = ParseServerReturn(data, dataStatus, false); - return ret; -} - -std::vector 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(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 item = std::pair(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 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 tempTags; - for (Json::UInt j = 0; j < tagsArray.size(); j++) - tempTags.push_back(tagsArray[j].asString()); - - auto tempSave = std::make_unique(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 Client::LoadSaveFile(ByteString filename) { ByteString err; @@ -964,84 +447,6 @@ std::unique_ptr Client::LoadSaveFile(ByteString filename) return file; } -std::list * Client::RemoveTag(int saveID, ByteString tag) -{ - lastError = ""; - std::list * 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(); - 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 * Client::AddTag(int saveID, ByteString tag) -{ - lastError = ""; - std::list * 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(); - for (Json::UInt j = 0; j < tagsArray.size(); j++) - tags->push_back(tagsArray[j].asString()); - } - catch (std::exception & e) - { - lastError = "Could not read response: " + ByteString(e.what()).FromUtf8(); - } - } - return tags; -} - // stamp-specific wrapper for MergeAuthorInfo // also used for clipboard and lua stamps void Client::MergeStampAuthorInfo(Json::Value stampAuthors) diff --git a/src/client/Client.h b/src/client/Client.h index 1cfa53abe..9bfdb7d25 100644 --- a/src/client/Client.h +++ b/src/client/Client.h @@ -1,6 +1,7 @@ #pragma once #include "common/String.h" #include "common/ExplicitSingleton.h" +#include "StartupInfo.h" #include "User.h" #include #include @@ -10,53 +11,27 @@ class SaveInfo; class SaveFile; -class SaveComment; class GameSave; class VideoBuffer; -enum LoginStatus { - LoginOkay, LoginError -}; - -enum RequestStatus { - RequestOkay, RequestFailure -}; - -class UpdateInfo -{ -public: - enum BuildType { Stable, Beta, Snapshot }; - ByteString File; - String Changelog; - int Major; - int Minor; - int Build; - int Time; - BuildType Type; - UpdateInfo() : File(""), Changelog(""), Major(0), Minor(0), Build(0), Time(0), Type(Stable) {} - UpdateInfo(int major, int minor, int build, ByteString file, String changelog, BuildType type) : File(file), Changelog(changelog), Major(major), Minor(minor), Build(build), Time(0), Type(type) {} - UpdateInfo(int time, ByteString file, String changelog, BuildType type) : File(file), Changelog(changelog), Major(0), Minor(0), Build(0), Time(time), Type(type) {} -}; - class Prefs; class RequestListener; class ClientListener; namespace http { - class Request; + class StartupRequest; } class Client: public ExplicitSingleton { private: String messageOfTheDay; - std::vector > serverNotifications; + std::vector serverNotifications; - std::unique_ptr versionCheckRequest; - std::unique_ptr alternateVersionCheckRequest; + std::unique_ptr versionCheckRequest; + std::unique_ptr alternateVersionCheckRequest; bool usingAltUpdateServer; bool updateAvailable; - UpdateInfo updateInfo; + std::optional updateInfo; - String lastError; bool firstRun; std::vector stampIDs; @@ -69,7 +44,7 @@ private: void notifyUpdateAvailable(); void notifyAuthUserChanged(); void notifyMessageOfTheDay(); - void notifyNewNotification(std::pair notification); + void notifyNewNotification(ServerNotification notification); // Save stealing info Json::Value authors; @@ -91,7 +66,7 @@ public: void ClearAuthorInfo() { authors.clear(); } bool IsAuthorsEmpty() { return authors.size() == 0; } - UpdateInfo GetUpdateInfo(); + std::optional GetUpdateInfo(); Client(); ~Client(); @@ -99,8 +74,8 @@ public: ByteString FileOpenDialogue(); //std::string FileSaveDialogue(); - void AddServerNotification(std::pair notification); - std::vector > GetServerNotifications(); + void AddServerNotification(ServerNotification notification); + std::vector GetServerNotifications(); void SetMessageOfTheDay(String message); String GetMessageOfTheDay(); @@ -111,9 +86,6 @@ public: void AddListener(ClientListener * listener); void RemoveListener(ClientListener * listener); - RequestStatus ExecVote(int saveID, int direction); - RequestStatus UploadSave(SaveInfo & save); - std::unique_ptr GetStamp(ByteString stampID); void DeleteStamp(ByteString stampID); ByteString AddStamp(std::unique_ptr saveData); @@ -121,30 +93,11 @@ public: const std::vector &GetStamps() const; void MoveStampToFront(ByteString stampID); - RequestStatus AddComment(int saveID, String comment); - - std::vector GetSaveData(int saveID, int saveDate); - - LoginStatus Login(ByteString username, ByteString password, User & user); - - std::unique_ptr GetSave(int saveID, int saveDate); std::unique_ptr LoadSaveFile(ByteString filename); - RequestStatus DeleteSave(int saveID); - RequestStatus ReportSave(int saveID, String message); - RequestStatus UnpublishSave(int saveID); - RequestStatus PublishSave(int saveID); - RequestStatus FavouriteSave(int saveID, bool favourite); void SetAuthUser(User user); User GetAuthUser(); - std::list * RemoveTag(int saveID, ByteString tag); //TODO RequestStatus - std::list * AddTag(int saveID, ByteString tag); - String GetLastError() { - return lastError; - } - RequestStatus ParseServerReturn(ByteString &result, int status, bool json); void Tick(); - void CheckUpdate(std::unique_ptr &updateRequest, bool checkSession); String DoMigration(ByteString fromDir, ByteString toDir); }; diff --git a/src/client/ClientListener.h b/src/client/ClientListener.h index 2afe978e1..4a0cb84fa 100644 --- a/src/client/ClientListener.h +++ b/src/client/ClientListener.h @@ -1,5 +1,6 @@ #pragma once #include "common/String.h" +#include "client/ServerNotification.h" class Client; class ClientListener @@ -11,6 +12,6 @@ public: virtual void NotifyUpdateAvailable(Client * sender) {} virtual void NotifyAuthUserChanged(Client * sender) {} virtual void NotifyMessageOfTheDay(Client * sender) {} - virtual void NotifyNewNotification(Client * sender, std::pair notification) {} + virtual void NotifyNewNotification(Client * sender, ServerNotification notification) {} }; diff --git a/src/client/Comment.h b/src/client/Comment.h new file mode 100644 index 000000000..9978536ae --- /dev/null +++ b/src/client/Comment.h @@ -0,0 +1,11 @@ +#pragma once +#include "User.h" + +struct Comment +{ + ByteString authorName; + User::Elevation authorElevation; + bool authorIsSelf; + bool authorIsBanned; + String content; +}; diff --git a/src/client/LoginInfo.h b/src/client/LoginInfo.h new file mode 100644 index 000000000..b28169545 --- /dev/null +++ b/src/client/LoginInfo.h @@ -0,0 +1,10 @@ +#pragma once +#include "User.h" +#include "ServerNotification.h" +#include + +struct LoginInfo +{ + User user; + std::vector notifications; +}; diff --git a/src/client/SaveInfo.cpp b/src/client/SaveInfo.cpp index f9d84dd75..fbac889bd 100644 --- a/src/client/SaveInfo.cpp +++ b/src/client/SaveInfo.cpp @@ -45,7 +45,7 @@ void SaveInfo::SetName(String name) { this->name = name; } -String SaveInfo::GetName() +const String &SaveInfo::GetName() const { return name; } @@ -54,7 +54,7 @@ void SaveInfo::SetDescription(String description) { Description = description; } -String SaveInfo::GetDescription() +const String &SaveInfo::GetDescription() const { return Description; } diff --git a/src/client/SaveInfo.h b/src/client/SaveInfo.h index 2c5167382..6b8111cd9 100644 --- a/src/client/SaveInfo.h +++ b/src/client/SaveInfo.h @@ -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 tags); void SetName(String name); - String GetName(); + const String &GetName() const; void SetDescription(String description); - String GetDescription(); + const String &GetDescription() const; void SetPublished(bool published); bool GetPublished() const; diff --git a/src/client/Search.h b/src/client/Search.h new file mode 100644 index 000000000..4077e5c13 --- /dev/null +++ b/src/client/Search.h @@ -0,0 +1,17 @@ +#pragma once + +namespace http +{ + enum Category + { + categoryNone, + categoryMyOwn, + categoryFavourites, + }; + + enum Sort + { + sortByVotes, + sortByDate, + }; +} diff --git a/src/client/ServerNotification.h b/src/client/ServerNotification.h new file mode 100644 index 000000000..cd3c027be --- /dev/null +++ b/src/client/ServerNotification.h @@ -0,0 +1,8 @@ +#pragma once +#include "common/String.h" + +struct ServerNotification +{ + String text; + ByteString link; +}; diff --git a/src/client/StartupInfo.h b/src/client/StartupInfo.h new file mode 100644 index 000000000..f1a27baa3 --- /dev/null +++ b/src/client/StartupInfo.h @@ -0,0 +1,29 @@ +#pragma once +#include "common/String.h" +#include "ServerNotification.h" +#include +#include + +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 notifications; + std::optional updateInfo; +}; diff --git a/src/client/User.cpp b/src/client/User.cpp new file mode 100644 index 000000000..49b3ac75c --- /dev/null +++ b/src/client/User.cpp @@ -0,0 +1,32 @@ +#include "User.h" + +static const std::vector> 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"; +} diff --git a/src/client/User.h b/src/client/User.h index ea1495d4e..63c121cf3 100644 --- a/src/client/User.h +++ b/src/client/User.h @@ -7,8 +7,14 @@ class User public: enum Elevation { - ElevationAdmin, ElevationModerator, ElevationNone + ElevationNone, + ElevationHalfMod, + ElevationMod, + ElevationAdmin, }; + static Elevation ElevationFromString(ByteString str); + static ByteString ElevationToString(Elevation elevation); + int UserID; ByteString Username; ByteString SessionID; diff --git a/src/client/http/APIRequest.cpp b/src/client/http/APIRequest.cpp index a28c89fcb..f877a50b4 100644 --- a/src/client/http/APIRequest.cpp +++ b/src/client/http/APIRequest.cpp @@ -1,35 +1,36 @@ #include "APIRequest.h" - #include "client/Client.h" namespace http { - APIRequest::APIRequest(ByteString url) : Request(url) + APIRequest::APIRequest(ByteString url, AuthMode authMode, bool newCheckStatus) : Request(url), checkStatus(newCheckStatus) { - User user = Client::Ref().GetAuthUser(); - AuthHeaders(ByteString::Build(user.UserID), user.SessionID); + auto user = Client::Ref().GetAuthUser(); + if (authMode == authRequire && !user.UserID) + { + FailEarly("Not authenticated"); + return; + } + if (authMode != authOmit && user.UserID) + { + AuthHeaders(ByteString::Build(user.UserID), user.SessionID); + } } - APIRequest::Result APIRequest::Finish() + Json::Value APIRequest::Finish() { - Result result; + auto [ status, data ] = Request::Finish(); + ParseResponse(data, status, checkStatus ? responseJson : responseData); + Json::Value document; try { - ByteString data; - std::tie(result.status, data) = Request::Finish(); - Client::Ref().ParseServerReturn(data, result.status, true); - if (result.status == 200 && data.size()) - { - std::istringstream dataStream(data); - Json::Value objDocument; - dataStream >> objDocument; - result.document = std::unique_ptr(new Json::Value(objDocument)); - } + std::istringstream ss(data); + ss >> document; } - catch (std::exception & e) + catch (const std::exception &ex) { + throw RequestError("Could not read response: " + ByteString(ex.what())); } - return result; + return document; } } - diff --git a/src/client/http/APIRequest.h b/src/client/http/APIRequest.h index 9af241ee3..893030805 100644 --- a/src/client/http/APIRequest.h +++ b/src/client/http/APIRequest.h @@ -2,22 +2,22 @@ #include "Request.h" #include "common/String.h" #include -#include -#include namespace http { class APIRequest : public Request { + bool checkStatus; + public: - struct Result + enum AuthMode { - int status; - std::unique_ptr document; + authRequire, + authUse, + authOmit, }; + APIRequest(ByteString url, AuthMode authMode, bool newCheckStatus); - APIRequest(ByteString url); - - Result Finish(); + Json::Value Finish(); }; } diff --git a/src/client/http/AddCommentRequest.cpp b/src/client/http/AddCommentRequest.cpp new file mode 100644 index 000000000..c5291eaf2 --- /dev/null +++ b/src/client/http/AddCommentRequest.cpp @@ -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(); + } +} diff --git a/src/client/http/AddCommentRequest.h b/src/client/http/AddCommentRequest.h new file mode 100644 index 000000000..e776663eb --- /dev/null +++ b/src/client/http/AddCommentRequest.h @@ -0,0 +1,13 @@ +#pragma once +#include "APIRequest.h" + +namespace http +{ + class AddCommentRequest : public APIRequest + { + public: + AddCommentRequest(int saveID, String comment); + + void Finish(); + }; +} diff --git a/src/client/http/AddTagRequest.cpp b/src/client/http/AddTagRequest.cpp new file mode 100644 index 000000000..58cdbe643 --- /dev/null +++ b/src/client/http/AddTagRequest.cpp @@ -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 AddTagRequest::Finish() + { + auto result = APIRequest::Finish(); + std::list 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; + } +} diff --git a/src/client/http/AddTagRequest.h b/src/client/http/AddTagRequest.h new file mode 100644 index 000000000..52f066289 --- /dev/null +++ b/src/client/http/AddTagRequest.h @@ -0,0 +1,14 @@ +#pragma once +#include "APIRequest.h" +#include + +namespace http +{ + class AddTagRequest : public APIRequest + { + public: + AddTagRequest(int saveID, ByteString tag); + + std::list Finish(); + }; +} diff --git a/src/client/http/DeleteSaveRequest.cpp b/src/client/http/DeleteSaveRequest.cpp new file mode 100644 index 000000000..58f7eb2e7 --- /dev/null +++ b/src/client/http/DeleteSaveRequest.cpp @@ -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(); + } +} diff --git a/src/client/http/DeleteSaveRequest.h b/src/client/http/DeleteSaveRequest.h new file mode 100644 index 000000000..def0c6560 --- /dev/null +++ b/src/client/http/DeleteSaveRequest.h @@ -0,0 +1,13 @@ +#pragma once +#include "APIRequest.h" + +namespace http +{ + class DeleteSaveRequest : public APIRequest + { + public: + DeleteSaveRequest(int saveID); + + void Finish(); + }; +} diff --git a/src/client/http/ExecVoteRequest.cpp b/src/client/http/ExecVoteRequest.cpp new file mode 100644 index 000000000..6839ef3d2 --- /dev/null +++ b/src/client/http/ExecVoteRequest.cpp @@ -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); + } +} diff --git a/src/client/http/ExecVoteRequest.h b/src/client/http/ExecVoteRequest.h new file mode 100644 index 000000000..99df9da44 --- /dev/null +++ b/src/client/http/ExecVoteRequest.h @@ -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; + } + }; +} diff --git a/src/client/http/FavouriteSaveRequest.cpp b/src/client/http/FavouriteSaveRequest.cpp new file mode 100644 index 000000000..48ee3b2f0 --- /dev/null +++ b/src/client/http/FavouriteSaveRequest.cpp @@ -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(); + } +} diff --git a/src/client/http/FavouriteSaveRequest.h b/src/client/http/FavouriteSaveRequest.h new file mode 100644 index 000000000..f4d561ff0 --- /dev/null +++ b/src/client/http/FavouriteSaveRequest.h @@ -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; + } + }; +} diff --git a/src/client/http/GetCommentsRequest.cpp b/src/client/http/GetCommentsRequest.cpp new file mode 100644 index 000000000..4207555ef --- /dev/null +++ b/src/client/http/GetCommentsRequest.cpp @@ -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 GetCommentsRequest::Finish() + { + auto result = APIRequest::Finish(); + std::vector 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() == 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; + } +} diff --git a/src/client/http/GetCommentsRequest.h b/src/client/http/GetCommentsRequest.h new file mode 100644 index 000000000..15bd65645 --- /dev/null +++ b/src/client/http/GetCommentsRequest.h @@ -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 Finish(); + }; +} diff --git a/src/client/http/GetSaveDataRequest.cpp b/src/client/http/GetSaveDataRequest.cpp new file mode 100644 index 000000000..e70c8a87a --- /dev/null +++ b/src/client/http/GetSaveDataRequest.cpp @@ -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 GetSaveDataRequest::Finish() + { + auto [ status, data ] = Request::Finish(); + ParseResponse(data, status, responseData); + return std::vector(data.begin(), data.end()); + } +} diff --git a/src/client/http/GetSaveDataRequest.h b/src/client/http/GetSaveDataRequest.h new file mode 100644 index 000000000..50e19fe8d --- /dev/null +++ b/src/client/http/GetSaveDataRequest.h @@ -0,0 +1,13 @@ +#pragma once +#include "Request.h" + +namespace http +{ + class GetSaveDataRequest : public Request + { + public: + GetSaveDataRequest(int saveID, int saveDate); + + std::vector Finish(); + }; +} diff --git a/src/client/http/GetSaveRequest.cpp b/src/client/http/GetSaveRequest.cpp new file mode 100644 index 000000000..79b219688 --- /dev/null +++ b/src/client/http/GetSaveRequest.cpp @@ -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 GetSaveRequest::Finish() + { + auto [ status, data ] = Request::Finish(); + ParseResponse(data, status, responseData); + std::unique_ptr saveInfo; + try + { + Json::Value document; + std::istringstream ss(data); + ss >> document; + std::list tags; + for (auto &tag : document["Tags"]) + { + tags.push_back(tag.asString()); + } + saveInfo = std::make_unique( + 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; + } +} diff --git a/src/client/http/GetSaveRequest.h b/src/client/http/GetSaveRequest.h new file mode 100644 index 000000000..3a2a42264 --- /dev/null +++ b/src/client/http/GetSaveRequest.h @@ -0,0 +1,16 @@ +#pragma once +#include "Request.h" +#include + +class SaveInfo; + +namespace http +{ + class GetSaveRequest : public Request + { + public: + GetSaveRequest(int saveID, int saveDate); + + std::unique_ptr Finish(); + }; +} diff --git a/src/client/http/GetUserInfoRequest.cpp b/src/client/http/GetUserInfoRequest.cpp index 3febb57fa..e8c004a33 100644 --- a/src/client/http/GetUserInfoRequest.cpp +++ b/src/client/http/GetUserInfoRequest.cpp @@ -5,18 +5,18 @@ namespace http { GetUserInfoRequest::GetUserInfoRequest(ByteString username) : - APIRequest(ByteString::Build(SCHEME, SERVER, "/User.json?Name=", username)) + APIRequest(ByteString::Build(SCHEME, SERVER, "/User.json?Name=", username), authOmit, false) { } - std::unique_ptr GetUserInfoRequest::Finish() + UserInfo GetUserInfoRequest::Finish() { - std::unique_ptr user_info; auto result = APIRequest::Finish(); - if (result.document) + UserInfo userInfo; + try { - auto &user = (*result.document)["User"]; - user_info = std::unique_ptr(new UserInfo( + auto &user = result["User"]; + userInfo = UserInfo( user["ID"].asInt(), user["Age"].asInt(), user["Username"].asString(), @@ -29,9 +29,13 @@ namespace http user["Forum"]["Topics"].asInt(), user["Forum"]["Replies"].asInt(), user["Forum"]["Reputation"].asInt() - )); + ); } - return user_info; + catch (const std::exception &ex) + { + throw RequestError("Could not read response: " + ByteString(ex.what())); + } + return userInfo; } } diff --git a/src/client/http/GetUserInfoRequest.h b/src/client/http/GetUserInfoRequest.h index 6e264cebb..ebe0d8c36 100644 --- a/src/client/http/GetUserInfoRequest.h +++ b/src/client/http/GetUserInfoRequest.h @@ -1,7 +1,6 @@ #pragma once #include "APIRequest.h" - -class UserInfo; +#include "client/UserInfo.h" namespace http { @@ -10,6 +9,6 @@ namespace http public: GetUserInfoRequest(ByteString username); - std::unique_ptr Finish(); + UserInfo Finish(); }; } diff --git a/src/client/http/ImageRequest.cpp b/src/client/http/ImageRequest.cpp index 1da912344..d30a80300 100644 --- a/src/client/http/ImageRequest.cpp +++ b/src/client/http/ImageRequest.cpp @@ -1,29 +1,27 @@ #include "ImageRequest.h" #include "graphics/Graphics.h" +#include "client/Client.h" #include namespace http { - ImageRequest::ImageRequest(ByteString url, Vec2 size): - Request(std::move(url)), - size(size) - {} + ImageRequest::ImageRequest(ByteString url, Vec2 newRequestedSize) : Request(url), requestedSize(newRequestedSize) + { + } std::unique_ptr ImageRequest::Finish() { auto [ status, data ] = Request::Finish(); - (void)status; // We don't use this for anything, not ideal >_> - std::unique_ptr vb; - if (data.size()) + ParseResponse(data, status, responseData); + auto vb = VideoBuffer::FromPNG(std::vector(data.begin(), data.end())); + if (vb) { - vb = VideoBuffer::FromPNG(std::vector(data.begin(), data.end())); - if (vb) - vb->Resize(size, true); - else - { - vb = std::make_unique(Vec2(15, 16)); - vb->BlendChar(Vec2(2, 4), 0xE06E, 0xFFFFFF_rgb .WithAlpha(0xFF)); - } + vb->Resize(requestedSize, true); + } + else + { + vb = std::make_unique(Vec2(15, 16)); + vb->BlendChar(Vec2(2, 4), 0xE06E, 0xFFFFFF_rgb .WithAlpha(0xFF)); } return vb; } diff --git a/src/client/http/ImageRequest.h b/src/client/http/ImageRequest.h index fe19e1056..ba5fc6ca3 100644 --- a/src/client/http/ImageRequest.h +++ b/src/client/http/ImageRequest.h @@ -2,7 +2,6 @@ #include "common/String.h" #include "common/Vec2.h" #include "Request.h" - #include class VideoBuffer; @@ -11,10 +10,10 @@ namespace http { class ImageRequest : public Request { - Vec2 size; + Vec2 requestedSize; public: - ImageRequest(ByteString url, Vec2 size); + ImageRequest(ByteString url, Vec2 newRequestedSize); std::unique_ptr Finish(); }; diff --git a/src/client/http/LoginRequest.cpp b/src/client/http/LoginRequest.cpp new file mode 100644 index 000000000..612081ae3 --- /dev/null +++ b/src/client/http/LoginRequest.cpp @@ -0,0 +1,45 @@ +#include "LoginRequest.h" +#include "Config.h" +#include "client/Client.h" +#include + +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; + } +} diff --git a/src/client/http/LoginRequest.h b/src/client/http/LoginRequest.h new file mode 100644 index 000000000..2c430e7b5 --- /dev/null +++ b/src/client/http/LoginRequest.h @@ -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(); + }; +} diff --git a/src/client/http/LogoutRequest.cpp b/src/client/http/LogoutRequest.cpp new file mode 100644 index 000000000..1625fcecb --- /dev/null +++ b/src/client/http/LogoutRequest.cpp @@ -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(); + } +} diff --git a/src/client/http/LogoutRequest.h b/src/client/http/LogoutRequest.h new file mode 100644 index 000000000..46a725296 --- /dev/null +++ b/src/client/http/LogoutRequest.h @@ -0,0 +1,13 @@ +#pragma once +#include "APIRequest.h" + +namespace http +{ + class LogoutRequest : public APIRequest + { + public: + LogoutRequest(); + + void Finish(); + }; +} diff --git a/src/client/http/PublishSaveRequest.cpp b/src/client/http/PublishSaveRequest.cpp new file mode 100644 index 000000000..fe934ede5 --- /dev/null +++ b/src/client/http/PublishSaveRequest.cpp @@ -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(); + } +} diff --git a/src/client/http/PublishSaveRequest.h b/src/client/http/PublishSaveRequest.h new file mode 100644 index 000000000..112857fcd --- /dev/null +++ b/src/client/http/PublishSaveRequest.h @@ -0,0 +1,13 @@ +#pragma once +#include "APIRequest.h" + +namespace http +{ + class PublishSaveRequest : public APIRequest + { + public: + PublishSaveRequest(int saveID); + + void Finish(); + }; +} diff --git a/src/client/http/RemoveTagRequest.cpp b/src/client/http/RemoveTagRequest.cpp new file mode 100644 index 000000000..570941031 --- /dev/null +++ b/src/client/http/RemoveTagRequest.cpp @@ -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 RemoveTagRequest::Finish() + { + auto result = APIRequest::Finish(); + std::list 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; + } +} diff --git a/src/client/http/RemoveTagRequest.h b/src/client/http/RemoveTagRequest.h new file mode 100644 index 000000000..668f2f30d --- /dev/null +++ b/src/client/http/RemoveTagRequest.h @@ -0,0 +1,14 @@ +#pragma once +#include "APIRequest.h" +#include + +namespace http +{ + class RemoveTagRequest : public APIRequest + { + public: + RemoveTagRequest(int saveID, ByteString tag); + + std::list Finish(); + }; +} diff --git a/src/client/http/ReportSaveRequest.cpp b/src/client/http/ReportSaveRequest.cpp new file mode 100644 index 000000000..901f83118 --- /dev/null +++ b/src/client/http/ReportSaveRequest.cpp @@ -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(); + } +} diff --git a/src/client/http/ReportSaveRequest.h b/src/client/http/ReportSaveRequest.h new file mode 100644 index 000000000..36b8e9519 --- /dev/null +++ b/src/client/http/ReportSaveRequest.h @@ -0,0 +1,13 @@ +#pragma once +#include "APIRequest.h" + +namespace http +{ + class ReportSaveRequest : public APIRequest + { + public: + ReportSaveRequest(int saveID, String message); + + void Finish(); + }; +} diff --git a/src/client/http/Request.cpp b/src/client/http/Request.cpp index b6d34c339..2478c6c23 100644 --- a/src/client/http/Request.cpp +++ b/src/client/http/Request.cpp @@ -2,6 +2,8 @@ #include "requestmanager/RequestManager.h" #include #include +#include +#include namespace http { @@ -13,12 +15,28 @@ namespace http Request::~Request() { - if (handle->state != RequestHandle::ready) + bool tryUnregister; { + std::lock_guard lk(handle->stateMx); + tryUnregister = handle->state == RequestHandle::running; + } + if (tryUnregister) + { + // At this point it may have already finished and been unregistered but that's ok, + // attempting to unregister a request multiple times is allowed. We only do the + // state-checking dance so we don't wake up RequestManager if we don't have to. + // In fact, we could just not unregister requests here at all, they'd just run to + // completion and be unregistered later. All this does is cancel them early. RequestManager::Ref().UnregisterRequest(*this); } } + void Request::FailEarly(ByteString error) + { + assert(handle->state == RequestHandle::ready); + handle->failEarly = error; + } + void Request::Verb(ByteString newVerb) { assert(handle->state == RequestHandle::ready); @@ -84,18 +102,36 @@ namespace http return handle->responseHeaders; } - std::pair Request::Finish() + void Request::Wait() { std::unique_lock lk(handle->stateMx); - if (handle->state == RequestHandle::running) + assert(handle->state == RequestHandle::running); + handle->stateCv.wait(lk, [this]() { + return handle->state == RequestHandle::done; + }); + } + + int Request::StatusCode() const + { { - handle->stateCv.wait(lk, [this]() { - return handle->state == RequestHandle::done; - }); + std::unique_lock lk(handle->stateMx); + assert(handle->state == RequestHandle::done); + } + return handle->statusCode; + } + + std::pair Request::Finish() + { + { + std::unique_lock lk(handle->stateMx); + assert(handle->state == RequestHandle::done); } - assert(handle->state == RequestHandle::done); handle->state = RequestHandle::dead; - return { handle->statusCode, std::move(handle->responseData) }; + if (handle->error) + { + throw RequestError(*handle->error); + } + return std::pair{ handle->statusCode, std::move(handle->responseData) }; } void RequestHandle::MarkDone() @@ -106,30 +142,13 @@ namespace http state = RequestHandle::done; } stateCv.notify_one(); - if (error.size()) + if (error) { - std::cerr << error << std::endl; + std::cerr << *error << std::endl; } } - std::pair Request::Simple(ByteString uri, FormData postData) - { - return SimpleAuth(uri, "", "", postData); - } - - std::pair Request::SimpleAuth(ByteString uri, ByteString ID, ByteString session, FormData postData) - { - auto request = std::make_unique(uri); - if (!postData.empty()) - { - request->AddPostData(postData); - } - request->AuthHeaders(ID, session); - request->Start(); - return request->Finish(); - } - - String StatusText(int ret) + const char *StatusText(int ret) { switch (ret) { @@ -211,7 +230,69 @@ namespace http case 619: return "SSL: Failed to Load CRL File"; case 620: return "SSL: Issuer Check Failed"; case 621: return "SSL: Pinned Public Key Mismatch"; - default: return "Unknown Status Code"; + } + return "Unknown Status Code"; + } + + void Request::ParseResponse(const ByteString &result, int status, ResponseType responseType) + { + // no server response, return "Malformed Response" + if (status == 200 && !result.size()) + { + status = 603; + } + if (status == 302) + { + return; + } + if (status != 200) + { + throw RequestError(ByteString::Build("HTTP Error ", status, ": ", http::StatusText(status))); + } + + switch (responseType) + { + case responseOk: + if (strncmp(result.c_str(), "OK", 2)) + { + throw RequestError(result); + } + break; + + case responseJson: + { + std::istringstream ss(result); + Json::Value root; + try + { + ss >> root; + // assume everything is fine if an empty [] is returned + if (root.size() == 0) + { + return; + } + int status = root.get("Status", 1).asInt(); + if (status != 1) + { + throw RequestError(ByteString(root.get("Error", "Unspecified Error").asString())); + } + } + catch (const std::exception &ex) + { + // sometimes the server returns a 200 with the text "Error: 401" + if (!strncmp(result.c_str(), "Error: ", 7)) + { + status = ByteString(result.begin() + 7, result.end()).ToNumber(); + 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; } } } diff --git a/src/client/http/Request.h b/src/client/http/Request.h index 7906c1be1..2c6898499 100644 --- a/src/client/http/Request.h +++ b/src/client/http/Request.h @@ -6,11 +6,18 @@ #include #include #include +#include namespace http { struct RequestHandle; + // Thrown by Finish and ParseResponse + struct RequestError : public std::runtime_error + { + using runtime_error::runtime_error; + }; + class Request { std::shared_ptr handle; @@ -21,6 +28,8 @@ namespace http Request &operator =(const Request &) = delete; ~Request(); + void FailEarly(ByteString error); + void Verb(ByteString newVerb); void AddHeader(ByteString header); @@ -32,13 +41,21 @@ namespace http std::pair CheckProgress() const; // total, done const std::vector &ResponseHeaders() const; + void Wait(); + + int StatusCode() const; // status std::pair Finish(); // status, data - static std::pair Simple(ByteString uri, FormData postData = {}); - static std::pair SimpleAuth(ByteString uri, ByteString ID, ByteString session, FormData postData = {}); + enum ResponseType + { + responseOk, + responseJson, + responseData, + }; + static void ParseResponse(const ByteString &result, int status, ResponseType responseType); friend class RequestManager; }; - String StatusText(int code); + const char *StatusText(int code); } diff --git a/src/client/http/SaveUserInfoRequest.cpp b/src/client/http/SaveUserInfoRequest.cpp index f4d261811..4e0983c4a 100644 --- a/src/client/http/SaveUserInfoRequest.cpp +++ b/src/client/http/SaveUserInfoRequest.cpp @@ -1,29 +1,20 @@ #include "SaveUserInfoRequest.h" -#include "client/UserInfo.h" #include "Config.h" namespace http { - SaveUserInfoRequest::SaveUserInfoRequest(UserInfo &info) : - APIRequest(ByteString::Build(SCHEME, SERVER, "/Profile.json")) + SaveUserInfoRequest::SaveUserInfoRequest(UserInfo info) : + APIRequest(ByteString::Build(SCHEME, SERVER, "/Profile.json"), authRequire, true) { AddPostData(FormData{ { "Location", info.location.ToUtf8() }, - { "Biography", info.biography.ToUtf8() } + { "Biography", info.biography.ToUtf8() }, }); } - bool SaveUserInfoRequest::Finish() + void SaveUserInfoRequest::Finish() { - auto result = APIRequest::Finish(); - if (result.document) - { - return (*result.document)["Status"].asInt() == 1; - } - else - { - return false; - } + APIRequest::Finish(); } } diff --git a/src/client/http/SaveUserInfoRequest.h b/src/client/http/SaveUserInfoRequest.h index a069c585b..006fafa24 100644 --- a/src/client/http/SaveUserInfoRequest.h +++ b/src/client/http/SaveUserInfoRequest.h @@ -1,15 +1,14 @@ #pragma once #include "APIRequest.h" - -class UserInfo; +#include "client/UserInfo.h" namespace http { class SaveUserInfoRequest : public APIRequest { public: - SaveUserInfoRequest(UserInfo &info); + SaveUserInfoRequest(UserInfo info); - bool Finish(); + void Finish(); }; } diff --git a/src/client/http/SearchSavesRequest.cpp b/src/client/http/SearchSavesRequest.cpp new file mode 100644 index 000000000..96103368b --- /dev/null +++ b/src/client/http/SearchSavesRequest.cpp @@ -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>> SearchSavesRequest::Finish() + { + std::vector> saves; + auto result = APIRequest::Finish(); + int count; + try + { + count = result["Count"].asInt(); + for (auto &save : result["Saves"]) + { + auto saveInfo = std::make_unique( + 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) }; + } +} diff --git a/src/client/http/SearchSavesRequest.h b/src/client/http/SearchSavesRequest.h new file mode 100644 index 000000000..5a275bc48 --- /dev/null +++ b/src/client/http/SearchSavesRequest.h @@ -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>> Finish(); + }; +} diff --git a/src/client/http/SearchTagsRequest.cpp b/src/client/http/SearchTagsRequest.cpp new file mode 100644 index 000000000..d4f39a700 --- /dev/null +++ b/src/client/http/SearchTagsRequest.cpp @@ -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> SearchTagsRequest::Finish() + { + std::vector> 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; + } +} diff --git a/src/client/http/SearchTagsRequest.h b/src/client/http/SearchTagsRequest.h new file mode 100644 index 000000000..4bb410bb6 --- /dev/null +++ b/src/client/http/SearchTagsRequest.h @@ -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> Finish(); + }; +} diff --git a/src/client/http/StartupRequest.cpp b/src/client/http/StartupRequest.cpp new file mode 100644 index 000000000..113636c63 --- /dev/null +++ b/src/client/http/StartupRequest.cpp @@ -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 ¬ification : 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 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; + } +} diff --git a/src/client/http/StartupRequest.h b/src/client/http/StartupRequest.h new file mode 100644 index 000000000..2ca616b85 --- /dev/null +++ b/src/client/http/StartupRequest.h @@ -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(); + }; +} diff --git a/src/client/http/UnpublishSaveRequest.cpp b/src/client/http/UnpublishSaveRequest.cpp new file mode 100644 index 000000000..b3e47dae8 --- /dev/null +++ b/src/client/http/UnpublishSaveRequest.cpp @@ -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(); + } +} + diff --git a/src/client/http/UnpublishSaveRequest.h b/src/client/http/UnpublishSaveRequest.h new file mode 100644 index 000000000..afc333a67 --- /dev/null +++ b/src/client/http/UnpublishSaveRequest.h @@ -0,0 +1,13 @@ +#pragma once +#include "APIRequest.h" + +namespace http +{ + class UnpublishSaveRequest : public APIRequest + { + public: + UnpublishSaveRequest(int saveID); + + void Finish(); + }; +} diff --git a/src/client/http/UpdateRequest.cpp b/src/client/http/UpdateRequest.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/src/client/http/UpdateRequest.h b/src/client/http/UpdateRequest.h new file mode 100644 index 000000000..e69de29bb diff --git a/src/client/http/UploadSaveRequest.cpp b/src/client/http/UploadSaveRequest.cpp new file mode 100644 index 000000000..ccb961066 --- /dev/null +++ b/src/client/http/UploadSaveRequest.cpp @@ -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(); + if (!saveID) + { + throw RequestError("Server did not return Save ID"); + } + return saveID; + } +} diff --git a/src/client/http/UploadSaveRequest.h b/src/client/http/UploadSaveRequest.h new file mode 100644 index 000000000..29d07ed18 --- /dev/null +++ b/src/client/http/UploadSaveRequest.h @@ -0,0 +1,15 @@ +#pragma once +#include "Request.h" + +class SaveInfo; + +namespace http +{ + class UploadSaveRequest : public Request + { + public: + UploadSaveRequest(const SaveInfo &saveInfo); + + int Finish(); + }; +} diff --git a/src/client/http/meson.build b/src/client/http/meson.build index 6a6bcb688..1048712b3 100644 --- a/src/client/http/meson.build +++ b/src/client/http/meson.build @@ -5,6 +5,25 @@ client_files += files( 'Request.cpp', 'SaveUserInfoRequest.cpp', 'ThumbnailRequest.cpp', + 'AddTagRequest.cpp', + 'RemoveTagRequest.cpp', + 'GetSaveRequest.cpp', + 'PublishSaveRequest.cpp', + 'UnpublishSaveRequest.cpp', + 'ReportSaveRequest.cpp', + 'FavouriteSaveRequest.cpp', + 'AddCommentRequest.cpp', + 'DeleteSaveRequest.cpp', + 'LoginRequest.cpp', + 'GetSaveDataRequest.cpp', + 'ExecVoteRequest.cpp', + 'UploadSaveRequest.cpp', + 'StartupRequest.cpp', + 'UpdateRequest.cpp', + 'SearchSavesRequest.cpp', + 'SearchTagsRequest.cpp', + 'GetCommentsRequest.cpp', + 'LogoutRequest.cpp', ) subdir('requestmanager') diff --git a/src/client/http/requestmanager/Common.cpp b/src/client/http/requestmanager/Common.cpp index 77dc48ccb..a8d18c5c9 100644 --- a/src/client/http/requestmanager/Common.cpp +++ b/src/client/http/requestmanager/Common.cpp @@ -22,6 +22,13 @@ namespace http void RequestManager::RegisterRequest(Request &request) { + if (request.handle->failEarly) + { + request.handle->error = request.handle->failEarly.value(); + request.handle->statusCode = 600; + request.handle->MarkDone(); + return; + } if (disableNetwork) { request.handle->statusCode = 604; diff --git a/src/client/http/requestmanager/Libcurl.cpp b/src/client/http/requestmanager/Libcurl.cpp index eda1bc3f7..0451b9a7d 100644 --- a/src/client/http/requestmanager/Libcurl.cpp +++ b/src/client/http/requestmanager/Libcurl.cpp @@ -285,16 +285,20 @@ namespace http void RequestManager::RegisterRequestImpl(Request &request) { auto manager = static_cast(this); - std::lock_guard lk(manager->sharedStateMx); - manager->requestHandlesToRegister.push_back(request.handle); + { + std::lock_guard lk(manager->sharedStateMx); + manager->requestHandlesToRegister.push_back(request.handle); + } curl_multi_wakeup(manager->curlMulti); } void RequestManager::UnregisterRequestImpl(Request &request) { auto manager = static_cast(this); - std::lock_guard lk(manager->sharedStateMx); - manager->requestHandlesToUnregister.push_back(request.handle); + { + std::lock_guard lk(manager->sharedStateMx); + manager->requestHandlesToUnregister.push_back(request.handle); + } curl_multi_wakeup(manager->curlMulti); } diff --git a/src/client/http/requestmanager/RequestManager.h b/src/client/http/requestmanager/RequestManager.h index 883e4c610..a5e82ae56 100644 --- a/src/client/http/requestmanager/RequestManager.h +++ b/src/client/http/requestmanager/RequestManager.h @@ -8,6 +8,7 @@ #include #include #include +#include namespace http { @@ -42,7 +43,8 @@ namespace http int statusCode = 0; ByteString responseData; std::vector responseHeaders; - ByteString error; + std::optional error; + std::optional failEarly; RequestHandle(CtorTag) { diff --git a/src/client/meson.build b/src/client/meson.build index e6711da8e..55503feee 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -5,6 +5,7 @@ client_files = files( 'ThumbnailRendererTask.cpp', 'Client.cpp', 'GameSave.cpp', + 'User.cpp', ) subdir('http') diff --git a/src/gui/game/GameController.cpp b/src/gui/game/GameController.cpp index 351ffb9de..7c70016d3 100644 --- a/src/gui/game/GameController.cpp +++ b/src/gui/game/GameController.cpp @@ -63,6 +63,7 @@ #include "Config.h" #include +#include #ifdef GetUserName # undef GetUserName // dammit windows @@ -699,6 +700,7 @@ bool GameController::KeyRelease(int key, int scan, bool repeat, bool shift, bool void GameController::Tick() { + gameModel->Tick(); if(firstTick) { commandInterface->Init(); @@ -1440,16 +1442,9 @@ void GameController::FrameStep() 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); - } - catch(GameModelException & ex) - { - new ErrorMessage("Error while voting", ByteString(ex.what()).FromUtf8()); - } + gameModel->SetVote(direction); } } @@ -1536,7 +1531,7 @@ void GameController::NotifyAuthUserChanged(Client * sender) gameModel->SetUser(newUser); } -void GameController::NotifyNewNotification(Client * sender, std::pair notification) +void GameController::NotifyNewNotification(Client * sender, ServerNotification notification) { class LinkNotification : public Notification { @@ -1550,7 +1545,7 @@ void GameController::NotifyNewNotification(Client * sender, std::pairAddNotification(new LinkNotification(notification.second, notification.first)); + gameModel->AddNotification(new LinkNotification(notification.link, notification.text)); } void GameController::NotifyUpdateAvailable(Client * sender) @@ -1564,7 +1559,13 @@ void GameController::NotifyUpdateAvailable(Client * sender) void Action() override { - UpdateInfo info = Client::Ref().GetUpdateInfo(); + auto optinfo = Client::Ref().GetUpdateInfo(); + if (!optinfo.has_value()) + { + std::cerr << "odd, the update has disappeared" << std::endl; + return; + } + UpdateInfo info = optinfo.value(); StringBuilder updateMessage; if (Platform::CanUpdate()) { @@ -1593,36 +1594,41 @@ void GameController::NotifyUpdateAvailable(Client * sender) } updateMessage << "\nNew version:\n "; - if (info.Type == UpdateInfo::Beta) + if (info.channel == UpdateInfo::channelBeta) { - updateMessage << info.Major << "." << info.Minor << " Beta, Build " << info.Build; + updateMessage << info.major << "." << info.minor << " Beta, Build " << info.build; } - else if (info.Type == UpdateInfo::Snapshot) + else if (info.channel == UpdateInfo::channelSnapshot) { if constexpr (MOD) { - updateMessage << "Mod version " << info.Time; + updateMessage << "Mod version " << info.build; } else { - updateMessage << "Snapshot " << info.Time; + updateMessage << "Snapshot " << info.build; } } - else if(info.Type == UpdateInfo::Stable) + else if(info.channel == UpdateInfo::channelStable) { - updateMessage << info.Major << "." << info.Minor << " Stable, Build " << info.Build; + updateMessage << info.major << "." << info.minor << " Stable, Build " << info.build; } - if (info.Changelog.length()) - updateMessage << "\n\nChangelog:\n" << info.Changelog; + if (info.changeLog.length()) + updateMessage << "\n\nChangelog:\n" << info.changeLog; - new ConfirmPrompt("Run Updater", updateMessage.Build(), { [this] { c->RunUpdater(); } }); + new ConfirmPrompt("Run Updater", updateMessage.Build(), { [this, info] { c->RunUpdater(info); } }); } }; - switch(sender->GetUpdateInfo().Type) + auto optinfo = sender->GetUpdateInfo(); + if (!optinfo.has_value()) { - case UpdateInfo::Snapshot: + return; + } + switch(optinfo.value().channel) + { + case UpdateInfo::channelSnapshot: if constexpr (MOD) { gameModel->AddNotification(new UpdateNotification(this, "A new mod update is available - click here to update")); @@ -1632,10 +1638,10 @@ void GameController::NotifyUpdateAvailable(Client * sender) gameModel->AddNotification(new UpdateNotification(this, "A new snapshot is available - click here to update")); } break; - case UpdateInfo::Stable: + case UpdateInfo::channelStable: gameModel->AddNotification(new UpdateNotification(this, "A new version is available - click here to update")); break; - case UpdateInfo::Beta: + case UpdateInfo::channelBeta: gameModel->AddNotification(new UpdateNotification(this, "A new beta is available - click here to update")); break; } @@ -1646,25 +1652,16 @@ void GameController::RemoveNotification(Notification * notification) gameModel->RemoveNotification(notification); } -void GameController::RunUpdater() +void GameController::RunUpdater(UpdateInfo info) { if (Platform::CanUpdate()) { Exit(); - new UpdateActivity(); + new UpdateActivity(info); } else { - ByteString file; - if constexpr (USE_UPDATESERVER) - { - file = ByteString::Build(SCHEME, UPDATESERVER, Client::Ref().GetUpdateInfo().File); - } - else - { - file = ByteString::Build(SCHEME, SERVER, Client::Ref().GetUpdateInfo().File); - } - Platform::OpenURI(file); + Platform::OpenURI(info.file); } } diff --git a/src/gui/game/GameController.h b/src/gui/game/GameController.h index 47f07dd2b..d09b8e3f1 100644 --- a/src/gui/game/GameController.h +++ b/src/gui/game/GameController.h @@ -1,5 +1,6 @@ #pragma once #include "client/ClientListener.h" +#include "client/StartupInfo.h" #include "gui/interface/Point.h" #include "gui/interface/Colour.h" #include "simulation/Sign.h" @@ -181,8 +182,8 @@ public: void NotifyUpdateAvailable(Client * sender) override; void NotifyAuthUserChanged(Client * sender) override; - void NotifyNewNotification(Client * sender, std::pair notification) override; - void RunUpdater(); + void NotifyNewNotification(Client * sender, ServerNotification notification) override; + void RunUpdater(UpdateInfo info); bool GetMouseClickRequired(); void RemoveCustomGOLType(const ByteString &identifier); diff --git a/src/gui/game/GameModel.cpp b/src/gui/game/GameModel.cpp index ced5a8a3d..5a064762d 100644 --- a/src/gui/game/GameModel.cpp +++ b/src/gui/game/GameModel.cpp @@ -17,6 +17,7 @@ #include "client/GameSave.h" #include "client/SaveFile.h" #include "client/SaveInfo.h" +#include "client/http/ExecVoteRequest.h" #include "common/platform/Platform.h" #include "graphics/Renderer.h" #include "simulation/Air.h" @@ -30,6 +31,7 @@ #include "simulation/ToolClasses.h" #include "gui/game/DecorationTool.h" #include "gui/interface/Engine.h" +#include "gui/dialogues/ErrorMessage.h" #include #include #include @@ -780,18 +782,33 @@ void GameModel::SetUndoHistoryLimit(unsigned int undoHistoryLimit_) void GameModel::SetVote(int direction) { - if(currentSave) + queuedVote = direction; +} + +void GameModel::Tick() +{ + if (execVoteRequest && execVoteRequest->CheckDone()) { - RequestStatus status = Client::Ref().ExecVote(currentSave->GetID(), direction); - if(status == RequestOkay) + try { - currentSave->vote = direction; + execVoteRequest->Finish(); + currentSave->vote = execVoteRequest->Direction(); notifySaveChanged(); } - else + catch (const http::RequestError &ex) { - throw GameModelException("Could not vote: "+Client::Ref().GetLastError()); + new ErrorMessage("Error while voting", ByteString(ex.what()).FromUtf8()); } + execVoteRequest.reset(); + } + if (!execVoteRequest && queuedVote) + { + if (currentSave) + { + execVoteRequest = std::make_unique(currentSave->GetID(), *queuedVote); + execVoteRequest->Start(); + } + queuedVote.reset(); } } diff --git a/src/gui/game/GameModel.h b/src/gui/game/GameModel.h index 0a02df0ad..2fc8a187f 100644 --- a/src/gui/game/GameModel.h +++ b/src/gui/game/GameModel.h @@ -5,6 +5,7 @@ #include #include #include +#include class Menu; class Tool; @@ -21,6 +22,11 @@ class Snapshot; struct SnapshotDelta; class GameSave; +namespace http +{ + class ExecVoteRequest; +}; + class ToolSelection { public: @@ -40,6 +46,8 @@ struct HistoryEntry class GameModel { + std::unique_ptr execVoteRequest; + private: std::vector notifications; //int clipboardSize; @@ -119,10 +127,14 @@ private: void SaveToSimParameters(const GameSave &saveData); + std::optional queuedVote; + public: GameModel(); ~GameModel(); + void Tick(); + void SetEdgeMode(int edgeMode); int GetEdgeMode(); void SetTemperatureScale(int temperatureScale); diff --git a/src/gui/game/GameView.cpp b/src/gui/game/GameView.cpp index 7b28ec375..7e27acd36 100644 --- a/src/gui/game/GameView.cpp +++ b/src/gui/game/GameView.cpp @@ -1426,8 +1426,7 @@ void GameView::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, c->ReloadSim(); break; case SDL_SCANCODE_A: - if ((Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator - || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin) && ctrl) + if (Client::Ref().GetAuthUser().UserElevation != User::ElevationNone && ctrl) { ByteString authorString = Client::Ref().GetAuthorInfo().toStyledString(); new InformationMessage("Save authorship info", authorString.FromUtf8(), true); diff --git a/src/gui/login/LoginController.cpp b/src/gui/login/LoginController.cpp index 4dc72ed73..2d53c7cd5 100644 --- a/src/gui/login/LoginController.cpp +++ b/src/gui/login/LoginController.cpp @@ -1,7 +1,7 @@ #include "LoginController.h" - #include "client/Client.h" - +#include "client/http/LoginRequest.h" +#include "client/http/LogoutRequest.h" #include "LoginView.h" #include "LoginModel.h" #include "Controller.h" @@ -23,15 +23,19 @@ void LoginController::Login(ByteString username, ByteString password) loginModel->Login(username, password); } -User LoginController::GetUser() +void LoginController::Logout() { - return loginModel->GetUser(); + loginModel->Logout(); +} + +void LoginController::Tick() +{ + loginModel->Tick(); } void LoginController::Exit() { loginView->CloseActiveWindow(); - Client::Ref().SetAuthUser(loginModel->GetUser()); if (onDone) onDone(); HasExited = true; diff --git a/src/gui/login/LoginController.h b/src/gui/login/LoginController.h index 6a0fc5c5e..a81ede74f 100644 --- a/src/gui/login/LoginController.h +++ b/src/gui/login/LoginController.h @@ -14,8 +14,9 @@ public: bool HasExited; LoginController(std::function onDone = nullptr); void Login(ByteString username, ByteString password); + void Logout(); + void Tick(); void Exit(); LoginView * GetView() { return loginView; } - User GetUser(); - virtual ~LoginController(); + ~LoginController(); }; diff --git a/src/gui/login/LoginModel.cpp b/src/gui/login/LoginModel.cpp index 970d95862..b5d591fb2 100644 --- a/src/gui/login/LoginModel.cpp +++ b/src/gui/login/LoginModel.cpp @@ -1,39 +1,32 @@ #include "LoginModel.h" - #include "LoginView.h" - #include "client/Client.h" - -LoginModel::LoginModel(): - currentUser(0, "") -{ - -} +#include "client/http/LoginRequest.h" +#include "client/http/LogoutRequest.h" void LoginModel::Login(ByteString username, ByteString password) { if (username.Contains("@")) { statusText = "Use your Powder Toy account to log in, not your email. If you don't have a Powder Toy account, you can create one at https://powdertoy.co.uk/Register.html"; - loginStatus = false; + loginStatus = loginIdle; notifyStatusChanged(); return; } statusText = "Logging in..."; - loginStatus = false; + loginStatus = loginWorking; notifyStatusChanged(); - LoginStatus status = Client::Ref().Login(username, password, currentUser); - switch(status) - { - case LoginOkay: - statusText = "Logged in"; - loginStatus = true; - break; - case LoginError: - statusText = Client::Ref().GetLastError(); - break; - } + loginRequest = std::make_unique(username, password); + loginRequest->Start(); +} + +void LoginModel::Logout() +{ + statusText = "Logging out..."; + loginStatus = loginWorking; notifyStatusChanged(); + logoutRequest = std::make_unique(); + logoutRequest->Start(); } void LoginModel::AddObserver(LoginView * observer) @@ -46,14 +39,47 @@ String LoginModel::GetStatusText() return statusText; } -User LoginModel::GetUser() +void LoginModel::Tick() { - return currentUser; -} - -bool LoginModel::GetStatus() -{ - return loginStatus; + 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"; + loginStatus = loginSucceeded; + } + catch (const http::RequestError &ex) + { + statusText = ByteString(ex.what()).FromUtf8(); + loginStatus = loginIdle; + } + notifyStatusChanged(); + loginRequest.reset(); + } + if (logoutRequest && logoutRequest->CheckDone()) + { + try + { + logoutRequest->Finish(); + auto &client = Client::Ref(); + client.SetAuthUser(User(0, "")); + statusText = "Logged out"; + } + catch (const http::RequestError &ex) + { + statusText = ByteString(ex.what()).FromUtf8(); + } + loginStatus = loginIdle; + notifyStatusChanged(); + logoutRequest.reset(); + } } void LoginModel::notifyStatusChanged() @@ -64,6 +90,7 @@ void LoginModel::notifyStatusChanged() } } -LoginModel::~LoginModel() { +LoginModel::~LoginModel() +{ + // Satisfy std::unique_ptr } - diff --git a/src/gui/login/LoginModel.h b/src/gui/login/LoginModel.h index 6e10911e5..3a8f229dc 100644 --- a/src/gui/login/LoginModel.h +++ b/src/gui/login/LoginModel.h @@ -2,21 +2,41 @@ #include "common/String.h" #include "client/User.h" #include +#include + +namespace http +{ + class LoginRequest; + class LogoutRequest; +} + +enum LoginStatus +{ + loginIdle, + loginWorking, + loginSucceeded, +}; class LoginView; class LoginModel { + std::unique_ptr loginRequest; + std::unique_ptr logoutRequest; std::vector observers; String statusText; - bool loginStatus; + LoginStatus loginStatus = loginIdle; void notifyStatusChanged(); - User currentUser; + public: - LoginModel(); void Login(ByteString username, ByteString password); + void Logout(); void AddObserver(LoginView * observer); String GetStatusText(); - bool GetStatus(); + LoginStatus GetStatus() const + { + return loginStatus; + } + void Tick(); User GetUser(); - virtual ~LoginModel(); + ~LoginModel(); }; diff --git a/src/gui/login/LoginView.cpp b/src/gui/login/LoginView.cpp index 9f8452344..1ec8e5077 100644 --- a/src/gui/login/LoginView.cpp +++ b/src/gui/login/LoginView.cpp @@ -1,18 +1,13 @@ #include "LoginView.h" - #include "LoginModel.h" #include "LoginController.h" - #include "graphics/Graphics.h" #include "gui/interface/Button.h" #include "gui/interface/Label.h" #include "gui/interface/Textbox.h" #include "gui/Style.h" - #include "client/Client.h" - #include "Misc.h" - #include LoginView::LoginView(): @@ -39,11 +34,15 @@ LoginView::LoginView(): loginButton->Appearance.HorizontalAlign = ui::Appearance::AlignRight; loginButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; loginButton->Appearance.TextInactive = style::Colour::ConfirmButton; - loginButton->SetActionCallback({ [this] { c->Login(usernameField->GetText().ToUtf8(), passwordField->GetText().ToUtf8()); } }); + loginButton->SetActionCallback({ [this] { + c->Login(usernameField->GetText().ToUtf8(), passwordField->GetText().ToUtf8()); + } }); AddComponent(cancelButton); cancelButton->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; cancelButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; - cancelButton->SetActionCallback({ [this] { c->Exit(); } }); + cancelButton->SetActionCallback({ [this] { + c->Logout(); + } }); AddComponent(titleLabel); titleLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; titleLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; @@ -85,12 +84,17 @@ void LoginView::NotifyStatusChanged(LoginModel * sender) targetSize.Y = 87; infoLabel->SetText(sender->GetStatusText()); infoLabel->AutoHeight(); + auto notWorking = sender->GetStatus() != loginWorking; + loginButton->Enabled = notWorking; + cancelButton->Enabled = notWorking && Client::Ref().GetAuthUser().UserID; + usernameField->Enabled = notWorking; + passwordField->Enabled = notWorking; if (sender->GetStatusText().length()) { targetSize.Y += infoLabel->Size.Y+2; infoLabel->Visible = true; } - if(sender->GetStatus()) + if (sender->GetStatus() == loginSucceeded) { c->Exit(); } @@ -98,6 +102,7 @@ void LoginView::NotifyStatusChanged(LoginModel * sender) void LoginView::OnTick(float dt) { + c->Tick(); //if(targetSize != Size) { ui::Point difference = targetSize-Size; diff --git a/src/gui/preview/Comment.h b/src/gui/preview/Comment.h deleted file mode 100644 index e9ea7da50..000000000 --- a/src/gui/preview/Comment.h +++ /dev/null @@ -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) - { - } -}; - diff --git a/src/gui/preview/PreviewController.cpp b/src/gui/preview/PreviewController.cpp index bd5116638..205c3f8b4 100644 --- a/src/gui/preview/PreviewController.cpp +++ b/src/gui/preview/PreviewController.cpp @@ -6,6 +6,10 @@ #include "client/Client.h" #include "client/SaveInfo.h" #include "client/GameSave.h" +#include "client/http/GetSaveRequest.h" +#include "client/http/GetSaveDataRequest.h" +#include "client/http/GetCommentsRequest.h" +#include "client/http/FavouriteSaveRequest.h" #include "common/platform/Platform.h" #include "graphics/Graphics.h" #include "gui/dialogues/ErrorMessage.h" @@ -51,30 +55,6 @@ void PreviewController::Update() } } -bool PreviewController::SubmitComment(String comment) -{ - if(comment.length() < 4) - { - new ErrorMessage("Error", "Comment is too short"); - return false; - } - else - { - RequestStatus status = Client::Ref().AddComment(saveId, comment); - if(status != RequestOkay) - { - new ErrorMessage("Error submitting comment", Client::Ref().GetLastError()); - return false; - } - else - { - previewModel->CommentAdded(); - previewModel->UpdateComments(1); - } - } - return true; -} - void PreviewController::ShowLogin() { loginWindow = new LoginController(); @@ -106,32 +86,11 @@ void PreviewController::DoOpen() previewModel->SetDoOpen(true); } -void PreviewController::Report(String message) -{ - if(Client::Ref().ReportSave(saveId, message) == RequestOkay) - { - Exit(); - new InformationMessage("Information", "Report submitted", false); - } - else - new ErrorMessage("Error", "Unable to file report: " + Client::Ref().GetLastError()); -} - void PreviewController::FavouriteSave() { - if(previewModel->GetSaveInfo() && Client::Ref().GetAuthUser().UserID) + if (previewModel->GetSaveInfo() && Client::Ref().GetAuthUser().UserID) { - try - { - if(previewModel->GetSaveInfo()->Favourite) - previewModel->SetFavourite(false); - else - previewModel->SetFavourite(true); - } - catch (PreviewModelException & e) - { - new ErrorMessage("Error", ByteString(e.what()).FromUtf8()); - } + previewModel->SetFavourite(!previewModel->GetSaveInfo()->Favourite); } } @@ -161,6 +120,12 @@ bool PreviewController::PrevCommentPage() return false; } +void PreviewController::CommentAdded() +{ + previewModel->CommentAdded(); + previewModel->UpdateComments(1); +} + void PreviewController::Exit() { previewView->CloseActiveWindow(); diff --git a/src/gui/preview/PreviewController.h b/src/gui/preview/PreviewController.h index 321a6beaa..834512807 100644 --- a/src/gui/preview/PreviewController.h +++ b/src/gui/preview/PreviewController.h @@ -23,7 +23,6 @@ public: void Exit(); void DoOpen(); void OpenInBrowser(); - void Report(String message); void ShowLogin(); bool GetDoOpen(); const SaveInfo *GetSaveInfo() const; @@ -31,10 +30,10 @@ public: PreviewView * GetView() { return previewView; } void Update(); void FavouriteSave(); - bool SubmitComment(String comment); bool NextCommentPage(); bool PrevCommentPage(); + void CommentAdded(); virtual ~PreviewController(); }; diff --git a/src/gui/preview/PreviewModel.cpp b/src/gui/preview/PreviewModel.cpp index b2cc81ca4..2dd1d29ec 100644 --- a/src/gui/preview/PreviewModel.cpp +++ b/src/gui/preview/PreviewModel.cpp @@ -1,46 +1,27 @@ #include "PreviewModel.h" -#include "client/http/Request.h" +#include "client/http/GetSaveDataRequest.h" +#include "client/http/GetSaveRequest.h" +#include "client/http/GetCommentsRequest.h" +#include "client/http/FavouriteSaveRequest.h" #include "Format.h" +#include "Misc.h" #include "client/Client.h" #include "client/GameSave.h" #include "client/SaveInfo.h" #include "gui/dialogues/ErrorMessage.h" -#include "gui/preview/Comment.h" #include "PreviewModelException.h" #include "PreviewView.h" #include "Config.h" #include #include -PreviewModel::PreviewModel(): - doOpen(false), - canOpen(true), - saveData(NULL), - saveComments(NULL), - commentBoxEnabled(false), - commentsLoaded(false), - commentsTotal(0), - commentsPageNumber(1) -{ -} - -PreviewModel::~PreviewModel() -{ - delete saveData; - ClearComments(); -} +constexpr auto commentsPerPage = 20; void PreviewModel::SetFavourite(bool favourite) { if (saveInfo) { - if (Client::Ref().FavouriteSave(saveInfo->id, favourite) == RequestOkay) - saveInfo->Favourite = favourite; - else if (favourite) - throw PreviewModelException("Error, could not fav. the save: " + Client::Ref().GetLastError()); - else - throw PreviewModelException("Error, could not unfav. the save: " + Client::Ref().GetLastError()); - notifySaveChanged(); + queuedFavourite = favourite; } } @@ -64,37 +45,22 @@ void PreviewModel::UpdateSave(int saveID, int saveDate) this->saveDate = saveDate; saveInfo.reset(); - if (saveData) - { - delete saveData; - saveData = NULL; - } - ClearComments(); + saveData.reset(); + saveComments.reset(); notifySaveChanged(); notifySaveCommentsChanged(); - ByteString url; - if (saveDate) - url = ByteString::Build(STATICSCHEME, STATICSERVER, "/", saveID, "_", saveDate, ".cps"); - else - url = ByteString::Build(STATICSCHEME, STATICSERVER, "/", saveID, ".cps"); - saveDataDownload = std::make_unique(url); + saveDataDownload = std::make_unique(saveID, saveDate); saveDataDownload->Start(); - url = ByteString::Build(SCHEME, SERVER , "/Browse/View.json?ID=", saveID); - if (saveDate) - url += ByteString::Build("&Date=", saveDate); - saveInfoDownload = std::make_unique(url); - saveInfoDownload->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID); + saveInfoDownload = std::make_unique(saveID, saveDate); saveInfoDownload->Start(); if (!GetDoOpen()) { commentsLoaded = false; - url = ByteString::Build(SCHEME, SERVER, "/Browse/Comments.json?ID=", saveID, "&Start=", (commentsPageNumber-1)*20, "&Count=20"); - commentsDownload = std::make_unique(url); - commentsDownload->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID); + commentsDownload = std::make_unique(saveID, (commentsPageNumber - 1) * commentsPerPage, commentsPerPage); commentsDownload->Start(); } } @@ -131,7 +97,7 @@ int PreviewModel::GetCommentsPageNum() int PreviewModel::GetCommentsPageCount() { - return std::max(1, (int)(ceil(commentsTotal/20.0f))); + return std::max(1, ceilDiv(commentsTotal, commentsPerPage).first); } bool PreviewModel::GetCommentsLoaded() @@ -144,14 +110,12 @@ void PreviewModel::UpdateComments(int pageNumber) if (commentsLoaded) { commentsLoaded = false; - ClearComments(); + saveComments.reset(); commentsPageNumber = pageNumber; if (!GetDoOpen()) { - ByteString url = ByteString::Build(SCHEME, SERVER, "/Browse/Comments.json?ID=", saveID, "&Start=", (commentsPageNumber-1)*20, "&Count=20"); - commentsDownload = std::make_unique(url); - commentsDownload->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID); + commentsDownload = std::make_unique(saveID, (commentsPageNumber - 1) * commentsPerPage, commentsPerPage); commentsDownload->Start(); } @@ -189,175 +153,105 @@ void PreviewModel::OnSaveReady() notifySaveCommentsChanged(); } -void PreviewModel::ClearComments() -{ - if (saveComments) - { - for (size_t i = 0; i < saveComments->size(); i++) - delete saveComments->at(i); - saveComments->clear(); - delete saveComments; - saveComments = NULL; - } -} - -bool PreviewModel::ParseSaveInfo(ByteString &saveInfoResponse) -{ - saveInfo.reset(); - - try // how does this differ from Client::GetSave? - { - std::istringstream dataStream(saveInfoResponse); - Json::Value objDocument; - dataStream >> objDocument; - - int tempID = objDocument["ID"].asInt(); - int tempScoreUp = objDocument["ScoreUp"].asInt(); - int tempScoreDown = objDocument["ScoreDown"].asInt(); - int tempMyScore = objDocument["ScoreMine"].asInt(); - ByteString tempUsername = objDocument["Username"].asString(); - String tempName = ByteString(objDocument["Name"].asString()).FromUtf8(); - String tempDescription = ByteString(objDocument["Description"].asString()).FromUtf8(); - int tempCreatedDate = objDocument["DateCreated"].asInt(); - int tempUpdatedDate = objDocument["Date"].asInt(); - bool tempPublished = objDocument["Published"].asBool(); - bool tempFavourite = objDocument["Favourite"].asBool(); - int tempComments = objDocument["Comments"].asInt(); - int tempViews = objDocument["Views"].asInt(); - int tempVersion = objDocument["Version"].asInt(); - - Json::Value tagsArray = objDocument["Tags"]; - std::list tempTags; - for (Json::UInt j = 0; j < tagsArray.size(); j++) - tempTags.push_back(tagsArray[j].asString()); - - auto newSaveInfo = std::make_unique(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(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(); - 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(); - ByteString username = commentsArray[j]["Username"].asString(); - ByteString formattedUsername = commentsArray[j]["FormattedUsername"].asString(); - if (formattedUsername == "jacobot") - formattedUsername = "\bt" + formattedUsername; - String comment = ByteString(commentsArray[j]["Text"].asString()).FromUtf8(); - saveComments->push_back(new SaveComment(userID, username, formattedUsername, comment)); - } - return true; - } - catch (std::exception &e) - { - return false; - } -} - void PreviewModel::Update() { + auto triggerOnSaveReady = false; if (saveDataDownload && saveDataDownload->CheckDone()) { - auto [ status, ret ] = saveDataDownload->Finish(); - - ByteString nothing; - Client::Ref().ParseServerReturn(nothing, status, true); - if (status == 200 && ret.size()) + try { - delete saveData; - saveData = new std::vector(ret.begin(), ret.end()); - if (saveInfo && saveData) - OnSaveReady(); + saveData = saveDataDownload->Finish(); + triggerOnSaveReady = true; } - else + catch (const http::RequestError &ex) { + auto why = ByteString(ex.what()).FromUtf8(); for (size_t i = 0; i < observers.size(); i++) { - observers[i]->SaveLoadingError(Client::Ref().GetLastError()); + observers[i]->SaveLoadingError(why); } } saveDataDownload.reset(); } - if (saveInfoDownload && saveInfoDownload->CheckDone()) { - auto [ status, ret ] = saveInfoDownload->Finish(); - - ByteString nothing; - Client::Ref().ParseServerReturn(nothing, status, true); - if (status == 200 && ret.size()) + try { - if (ParseSaveInfo(ret)) + saveInfo = saveInfoDownload->Finish(); + triggerOnSaveReady = true; + // This is a workaround for a bug on the TPT server where the wrong 404 save is returned + // Redownload the .cps file for a fixed version of the 404 save + if (saveInfo->GetID() == 404 && saveID != 404) { - if (saveInfo && saveData) - OnSaveReady(); - } - else - { - for (size_t i = 0; i < observers.size(); i++) - observers[i]->SaveLoadingError("Could not parse save info"); + saveData.reset(); + saveDataDownload = std::make_unique(2157797, 0); + saveDataDownload->Start(); } } - else + catch (const http::RequestError &ex) { + auto why = ByteString(ex.what()).FromUtf8(); for (size_t i = 0; i < observers.size(); i++) - observers[i]->SaveLoadingError(Client::Ref().GetLastError()); + { + observers[i]->SaveLoadingError(why); + } } saveInfoDownload.reset(); } + if (triggerOnSaveReady && saveInfo && saveData) + { + OnSaveReady(); + } if (commentsDownload && commentsDownload->CheckDone()) { - auto [ status, ret ] = commentsDownload->Finish(); - ClearComments(); - - ByteString nothing; - Client::Ref().ParseServerReturn(nothing, status, true); - if (status == 200 && ret.size()) - ParseComments(ret); - + try + { + saveComments = commentsDownload->Finish(); + } + catch (const http::RequestError &ex) + { + // TODO: handle + } commentsLoaded = true; notifySaveCommentsChanged(); notifyCommentsPageChanged(); - commentsDownload.reset(); } -} -std::vector * PreviewModel::GetComments() -{ - return saveComments; + if (favouriteSaveRequest && favouriteSaveRequest->CheckDone()) + { + 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(saveInfo->id, *queuedFavourite); + favouriteSaveRequest->Start(); + } + queuedFavourite.reset(); + } } void PreviewModel::notifySaveChanged() diff --git a/src/gui/preview/PreviewModel.h b/src/gui/preview/PreviewModel.h index 709afa366..bbba16a90 100644 --- a/src/gui/preview/PreviewModel.h +++ b/src/gui/preview/PreviewModel.h @@ -1,47 +1,54 @@ #pragma once #include "common/String.h" +#include "client/Comment.h" #include #include +#include namespace http { - class Request; + class GetSaveDataRequest; + class GetSaveRequest; + class GetCommentsRequest; + class FavouriteSaveRequest; } class PreviewView; class SaveInfo; -class SaveComment; class PreviewModel { - bool doOpen; - bool canOpen; + bool doOpen = false; + bool canOpen = true; std::vector observers; std::unique_ptr saveInfo; - std::vector * saveData; - std::vector * saveComments; + std::optional> saveData; + std::optional> saveComments; void notifySaveChanged(); void notifySaveCommentsChanged(); void notifyCommentsPageChanged(); void notifyCommentBoxEnabledChanged(); - std::unique_ptr saveDataDownload; - std::unique_ptr saveInfoDownload; - std::unique_ptr commentsDownload; + std::unique_ptr saveDataDownload; + std::unique_ptr saveInfoDownload; + std::unique_ptr commentsDownload; + std::unique_ptr favouriteSaveRequest; int saveID; int saveDate; - bool commentBoxEnabled; - bool commentsLoaded; - int commentsTotal; - int commentsPageNumber; + bool commentBoxEnabled = false; + bool commentsLoaded = false; + int commentsTotal = 0; + int commentsPageNumber = 1; + + std::optional queuedFavourite; public: - PreviewModel(); - ~PreviewModel(); - const SaveInfo *GetSaveInfo() const; std::unique_ptr TakeSaveInfo(); - std::vector * GetComments(); + const std::vector *GetComments() const + { + return saveComments ? &*saveComments : nullptr; + } bool GetCommentBoxEnabled(); void SetCommentBoxEnabled(bool enabledState); @@ -59,7 +66,6 @@ public: bool GetCanOpen(); void SetDoOpen(bool doOpen); void Update(); - void ClearComments(); void OnSaveReady(); bool ParseSaveInfo(ByteString &saveInfoResponse); bool ParseComments(ByteString &commentsResponse); diff --git a/src/gui/preview/PreviewView.cpp b/src/gui/preview/PreviewView.cpp index de419ad17..e4b490451 100644 --- a/src/gui/preview/PreviewView.cpp +++ b/src/gui/preview/PreviewView.cpp @@ -4,6 +4,8 @@ #include "client/Client.h" #include "client/SaveInfo.h" +#include "client/http/AddCommentRequest.h" +#include "client/http/ReportSaveRequest.h" #include "gui/dialogues/TextPrompt.h" #include "gui/profile/ProfileActivity.h" @@ -17,12 +19,12 @@ #include "gui/interface/Textbox.h" #include "gui/interface/Engine.h" #include "gui/dialogues/ErrorMessage.h" +#include "gui/dialogues/InformationMessage.h" #include "gui/interface/Point.h" #include "gui/interface/Window.h" #include "gui/Style.h" #include "common/tpt-rand.h" -#include "Comment.h" #include "Format.h" #include "Misc.h" @@ -57,6 +59,7 @@ PreviewView::PreviewView(std::unique_ptr newSavePreview): favButton = new ui::Button(ui::Point(50, Size.Y-19), ui::Point(51, 19), "Fav"); favButton->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; favButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; + favButton->SetTogglable(true); favButton->SetIcon(IconFavourite); favButton->SetActionCallback({ [this] { c->FavouriteSave(); } }); favButton->Enabled = Client::Ref().GetAuthUser().UserID?true:false; @@ -68,7 +71,12 @@ PreviewView::PreviewView(std::unique_ptr newSavePreview): reportButton->SetIcon(IconReport); reportButton->SetActionCallback({ [this] { new TextPrompt("Report Save", "Things to consider when reporting:\n\bw1)\bg When reporting stolen saves, please include the ID of the original save.\n\bw2)\bg Do not ask for saves to be removed from front page unless they break the rules.\n\bw3)\bg You may report saves for comments or tags too (including your own saves)", "", "[reason]", true, { [this](String const &resultText) { - c->Report(resultText); + if (reportSaveRequest) + { + return; + } + reportSaveRequest = std::make_unique(c->SaveID(), resultText); + reportSaveRequest->Start(); } }); } }); reportButton->Enabled = Client::Ref().GetAuthUser().UserID?true:false; @@ -222,7 +230,12 @@ void PreviewView::CheckComment() if (!commentWarningLabel) return; String text = addCommentBox->GetText().ToLower(); - if (!userIsAuthor && (text.Contains("stolen") || text.Contains("copied"))) + if (addCommentRequest) + { + commentWarningLabel->SetText("Submitting comment..."); + commentHelpText = true; + } + else if (!userIsAuthor && (text.Contains("stolen") || text.Contains("copied"))) { if (!commentHelpText) { @@ -375,6 +388,37 @@ void PreviewView::OnTick(float dt) ErrorMessage::Blocking("Error loading save", doErrorMessage); c->Exit(); } + + if (reportSaveRequest && reportSaveRequest->CheckDone()) + { + try + { + reportSaveRequest->Finish(); + c->Exit(); + new InformationMessage("Information", "Report submitted", false); + } + catch (const http::RequestError &ex) + { + new ErrorMessage("Error", "Unable to file report: " + ByteString(ex.what()).FromUtf8()); + } + reportSaveRequest.reset(); + } + if (addCommentRequest && addCommentRequest->CheckDone()) + { + try + { + addCommentBox->SetText(""); + c->CommentAdded(); + } + catch (const http::RequestError &ex) + { + new ErrorMessage("Error submitting comment", ByteString(ex.what()).FromUtf8()); + } + submitCommentButton->Enabled = true; + commentBoxAutoHeight(); + addCommentRequest.reset(); + CheckComment(); + } } void PreviewView::OnTryExit(ExitMethod method) @@ -448,16 +492,16 @@ void PreviewView::NotifySaveChanged(PreviewModel * sender) if(save->Favourite) { favButton->Enabled = true; - favButton->SetText("Unfav"); + favButton->SetToggleState(true); } else if(Client::Ref().GetAuthUser().UserID) { favButton->Enabled = true; - favButton->SetText("Fav"); + favButton->SetToggleState(false); } else { - favButton->SetText("Fav"); + favButton->SetToggleState(false); favButton->Enabled = false; } @@ -477,6 +521,7 @@ void PreviewView::NotifySaveChanged(PreviewModel * sender) saveNameLabel->SetText(""); authorDateLabel->SetText(""); saveDescriptionLabel->SetText(""); + favButton->SetToggleState(false); favButton->Enabled = false; if (!sender->GetCanOpen()) openButton->Enabled = false; @@ -485,21 +530,22 @@ void PreviewView::NotifySaveChanged(PreviewModel * sender) void PreviewView::submitComment() { - if(addCommentBox) + if (addCommentBox) { String comment = addCommentBox->GetText(); + if (comment.length() < 4) + { + new ErrorMessage("Error", "Comment is too short"); + return; + } + submitCommentButton->Enabled = false; - addCommentBox->SetText(""); - addCommentBox->SetPlaceholder("Submitting comment"); //This doesn't appear to ever show since no separate thread is created FocusComponent(NULL); - if (!c->SubmitComment(comment)) - addCommentBox->SetText(comment); + addCommentRequest = std::make_unique(c->SaveID(), comment); + addCommentRequest->Start(); - addCommentBox->SetPlaceholder("Add comment"); - submitCommentButton->Enabled = true; - - commentBoxAutoHeight(); + CheckComment(); } } @@ -564,7 +610,7 @@ void PreviewView::NotifyCommentsPageChanged(PreviewModel * sender) void PreviewView::NotifyCommentsChanged(PreviewModel * sender) { - std::vector * comments = sender->GetComments(); + auto commentsPtr = sender->GetComments(); for (size_t i = 0; i < commentComponents.size(); i++) { @@ -575,8 +621,9 @@ void PreviewView::NotifyCommentsChanged(PreviewModel * sender) commentTextComponents.clear(); commentsPanel->InnerSize = ui::Point(0, 0); - if (comments) + if (commentsPtr) { + auto &comments = *commentsPtr; for (size_t i = 0; i < commentComponents.size(); i++) { commentsPanel->RemoveChild(commentComponents[i]); @@ -589,11 +636,11 @@ void PreviewView::NotifyCommentsChanged(PreviewModel * sender) ui::Label * tempUsername; ui::Label * tempComment; ui::AvatarButton * tempAvatar; - for (size_t i = 0; i < comments->size(); i++) + for (size_t i = 0; i < comments.size(); i++) { if (showAvatars) { - tempAvatar = new ui::AvatarButton(ui::Point(2, currentY+7), ui::Point(26, 26), comments->at(i)->authorName); + tempAvatar = new ui::AvatarButton(ui::Point(2, currentY+7), ui::Point(26, 26), comments[i].authorName); tempAvatar->SetActionCallback({ [tempAvatar] { if (tempAvatar->GetUsername().size() > 0) { @@ -604,25 +651,38 @@ void PreviewView::NotifyCommentsChanged(PreviewModel * sender) commentsPanel->AddChild(tempAvatar); } + auto authorNameFormatted = comments[i].authorName.FromUtf8(); + if (comments[i].authorElevation != User::ElevationNone || comments[i].authorName == "jacobot") + { + authorNameFormatted = "\bt" + authorNameFormatted; + } + else if (comments[i].authorIsBanned) + { + authorNameFormatted = "\bg" + authorNameFormatted; + } + else if (Client::Ref().GetAuthUser().UserID && Client::Ref().GetAuthUser().Username == comments[i].authorName) + { + authorNameFormatted = "\bo" + authorNameFormatted; + } + else if (sender->GetSaveInfo() && sender->GetSaveInfo()->GetUserName() == comments[i].authorName) + { + authorNameFormatted = "\bl" + authorNameFormatted; + } if (showAvatars) - tempUsername = new ui::Label(ui::Point(31, currentY+3), ui::Point(Size.X-((XRES/2) + 13 + 26), 16), comments->at(i)->authorNameFormatted.FromUtf8()); + tempUsername = new ui::Label(ui::Point(31, currentY+3), ui::Point(Size.X-((XRES/2) + 13 + 26), 16), authorNameFormatted); else - tempUsername = new ui::Label(ui::Point(5, currentY+3), ui::Point(Size.X-((XRES/2) + 13), 16), comments->at(i)->authorNameFormatted.FromUtf8()); + tempUsername = new ui::Label(ui::Point(5, currentY+3), ui::Point(Size.X-((XRES/2) + 13), 16), authorNameFormatted); tempUsername->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; tempUsername->Appearance.VerticalAlign = ui::Appearance::AlignBottom; - if (Client::Ref().GetAuthUser().UserID && Client::Ref().GetAuthUser().Username == comments->at(i)->authorName) - tempUsername->SetTextColour(ui::Colour(255, 255, 100)); - else if (sender->GetSaveInfo() && sender->GetSaveInfo()->GetUserName() == comments->at(i)->authorName) - tempUsername->SetTextColour(ui::Colour(255, 100, 100)); currentY += 16; commentComponents.push_back(tempUsername); commentsPanel->AddChild(tempUsername); if (showAvatars) - tempComment = new ui::Label(ui::Point(31, currentY+5), ui::Point(Size.X-((XRES/2) + 13 + 26), -1), comments->at(i)->comment); + tempComment = new ui::Label(ui::Point(31, currentY+5), ui::Point(Size.X-((XRES/2) + 13 + 26), -1), comments[i].content); else - tempComment = new ui::Label(ui::Point(5, currentY+5), ui::Point(Size.X-((XRES/2) + 13), -1), comments->at(i)->comment); + tempComment = new ui::Label(ui::Point(5, currentY+5), ui::Point(Size.X-((XRES/2) + 13), -1), comments[i].content); tempComment->SetMultiline(true); tempComment->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; tempComment->Appearance.VerticalAlign = ui::Appearance::AlignTop; diff --git a/src/gui/preview/PreviewView.h b/src/gui/preview/PreviewView.h index a08835670..02deafd4f 100644 --- a/src/gui/preview/PreviewView.h +++ b/src/gui/preview/PreviewView.h @@ -5,6 +5,12 @@ #include "common/String.h" #include "gui/interface/Window.h" +namespace http +{ + class AddCommentRequest; + class ReportSaveRequest; +} + namespace ui { class Button; @@ -64,6 +70,10 @@ class PreviewView: public ui::Window void submitComment(); bool CheckSwearing(String text); void CheckComment(); + + std::unique_ptr addCommentRequest; + std::unique_ptr reportSaveRequest; + public: void AttachController(PreviewController * controller); PreviewView(std::unique_ptr newSavePreviev); diff --git a/src/gui/profile/ProfileActivity.cpp b/src/gui/profile/ProfileActivity.cpp index 417c05e7d..fc472ae7f 100644 --- a/src/gui/profile/ProfileActivity.cpp +++ b/src/gui/profile/ProfileActivity.cpp @@ -1,5 +1,7 @@ #include "ProfileActivity.h" #include "client/Client.h" +#include "client/http/SaveUserInfoRequest.h" +#include "client/http/GetUserInfoRequest.h" #include "common/platform/Platform.h" #include "gui/Style.h" #include "gui/interface/AvatarButton.h" @@ -200,30 +202,30 @@ void ProfileActivity::OnTick(float dt) if (saveUserInfoRequest && saveUserInfoRequest->CheckDone()) { - auto SaveUserInfoStatus = saveUserInfoRequest->Finish(); - if (SaveUserInfoStatus) + try { + saveUserInfoRequest->Finish(); Exit(); } - else + catch (const http::RequestError &ex) { doError = true; - doErrorMessage = "Could not save user info: " + Client::Ref().GetLastError(); + doErrorMessage = "Could not save user info: " + ByteString(ex.what()).FromUtf8(); } saveUserInfoRequest.reset(); } if (getUserInfoRequest && getUserInfoRequest->CheckDone()) { - auto getUserInfoResult = getUserInfoRequest->Finish(); - if (getUserInfoResult) + try { + auto userInfo = getUserInfoRequest->Finish(); loading = false; - setUserInfo(*getUserInfoResult); + setUserInfo(userInfo); } - else + catch (const http::RequestError &ex) { doError = true; - doErrorMessage = "Could not load user info: " + Client::Ref().GetLastError(); + doErrorMessage = "Could not load user info: " + ByteString(ex.what()).FromUtf8(); } getUserInfoRequest.reset(); } diff --git a/src/gui/profile/ProfileActivity.h b/src/gui/profile/ProfileActivity.h index 7f53fea93..87db26142 100644 --- a/src/gui/profile/ProfileActivity.h +++ b/src/gui/profile/ProfileActivity.h @@ -2,10 +2,14 @@ #include "common/String.h" #include "Activity.h" #include "client/UserInfo.h" -#include "client/http/SaveUserInfoRequest.h" -#include "client/http/GetUserInfoRequest.h" #include +namespace http +{ + class SaveUserInfoRequest; + class GetUserInfoRequest; +} + namespace ui { class Label; diff --git a/src/gui/save/ServerSaveActivity.cpp b/src/gui/save/ServerSaveActivity.cpp index c3dab8d99..a955c14f9 100644 --- a/src/gui/save/ServerSaveActivity.cpp +++ b/src/gui/save/ServerSaveActivity.cpp @@ -1,7 +1,5 @@ #include "ServerSaveActivity.h" - #include "graphics/Graphics.h" - #include "gui/interface/Label.h" #include "gui/interface/Textbox.h" #include "gui/interface/Button.h" @@ -10,13 +8,11 @@ #include "gui/dialogues/SaveIDMessage.h" #include "gui/dialogues/ConfirmPrompt.h" #include "gui/dialogues/InformationMessage.h" - #include "client/Client.h" #include "client/ThumbnailRendererTask.h" #include "client/GameSave.h" - +#include "client/http/UploadSaveRequest.h" #include "tasks/Task.h" - #include "gui/Style.h" class SaveUploadTask: public Task @@ -36,7 +32,19 @@ class SaveUploadTask: public Task bool doWork() override { notifyProgress(-1); - return Client::Ref().UploadSave(save) == RequestOkay; + auto uploadSaveRequest = std::make_unique(save); + uploadSaveRequest->Start(); + uploadSaveRequest->Wait(); + try + { + save.SetID(uploadSaveRequest->Finish()); + } + catch (const http::RequestError &ex) + { + notifyError(ByteString(ex.what()).FromUtf8()); + return false; + } + return true; } public: @@ -168,7 +176,7 @@ void ServerSaveActivity::NotifyDone(Task * task) if(!task->GetSuccess()) { Exit(); - new ErrorMessage("Error", Client::Ref().GetLastError()); + new ErrorMessage("Error", task->GetError()); } else { @@ -182,24 +190,20 @@ void ServerSaveActivity::NotifyDone(Task * task) void ServerSaveActivity::Save() { - if(nameField->GetText().length()) + if (!nameField->GetText().length()) { - if(Client::Ref().GetAuthUser().Username != save->GetUserName() && publishedCheckbox->GetChecked()) - { - new ConfirmPrompt("Publish", "This save was created by " + save->GetUserName().FromUtf8() + ", you're about to publish this under your own name; If you haven't been given permission by the author to do so, please uncheck the publish box, otherwise continue", { [this] { - Exit(); - saveUpload(); - } }); - } - else - { - Exit(); + new ErrorMessage("Error", "You must specify a save name."); + return; + } + if(Client::Ref().GetAuthUser().Username != save->GetUserName() && publishedCheckbox->GetChecked()) + { + new ConfirmPrompt("Publish", "This save was created by " + save->GetUserName().FromUtf8() + ", you're about to publish this under your own name; If you haven't been given permission by the author to do so, please uncheck the publish box, otherwise continue", { [this] { saveUpload(); - } + } }); } else { - new ErrorMessage("Error", "You must specify a save name."); + saveUpload(); } } @@ -223,6 +227,7 @@ void ServerSaveActivity::AddAuthorInfo() void ServerSaveActivity::saveUpload() { + okayButton->Enabled = false; save->SetName(nameField->GetText()); save->SetDescription(descriptionField->GetText()); save->SetPublished(publishedCheckbox->GetChecked()); @@ -234,16 +239,8 @@ void ServerSaveActivity::saveUpload() save->SetGameSave(std::move(gameSave)); } AddAuthorInfo(); - - if(Client::Ref().UploadSave(*save) != RequestOkay) - { - new ErrorMessage("Error", "Upload failed with error:\n"+Client::Ref().GetLastError()); - } - else if (onUploaded) - { - new SaveIDMessage(save->GetID()); - onUploaded(std::move(save)); - } + uploadSaveRequest = std::make_unique(*save); + uploadSaveRequest->Start(); } void ServerSaveActivity::Exit() @@ -364,6 +361,26 @@ void ServerSaveActivity::OnTick(float dt) } } + if (uploadSaveRequest && uploadSaveRequest->CheckDone()) + { + okayButton->Enabled = true; + try + { + save->SetID(uploadSaveRequest->Finish()); + Exit(); + new SaveIDMessage(save->GetID()); + if (onUploaded) + { + onUploaded(std::move(save)); + } + } + catch (const http::RequestError &ex) + { + new ErrorMessage("Error", "Upload failed with error:\n" + ByteString(ex.what()).FromUtf8()); + } + uploadSaveRequest.reset(); + } + if(saveUploadTask) saveUploadTask->Poll(); } diff --git a/src/gui/save/ServerSaveActivity.h b/src/gui/save/ServerSaveActivity.h index 833cf3122..8b64faaf2 100644 --- a/src/gui/save/ServerSaveActivity.h +++ b/src/gui/save/ServerSaveActivity.h @@ -13,6 +13,11 @@ #include "save_online.png.h" +namespace http +{ + class UploadSaveRequest; +} + namespace ui { class Label; @@ -25,6 +30,8 @@ class Task; class VideoBuffer; class ServerSaveActivity: public WindowActivity, public TaskListener { + std::unique_ptr uploadSaveRequest; + using OnUploaded = std::function)>; std::unique_ptr>> saveToServerImage = format::PixelsFromPNG( std::vector(save_online_png, save_online_png + save_online_png_size) diff --git a/src/gui/search/SearchController.cpp b/src/gui/search/SearchController.cpp index 2d94d5f7c..7a1509edb 100644 --- a/src/gui/search/SearchController.cpp +++ b/src/gui/search/SearchController.cpp @@ -7,6 +7,12 @@ #include "client/Client.h" #include "client/SaveInfo.h" #include "client/GameSave.h" +#include "client/http/DeleteSaveRequest.h" +#include "client/http/PublishSaveRequest.h" +#include "client/http/UnpublishSaveRequest.h" +#include "client/http/FavouriteSaveRequest.h" +#include "client/http/SearchSavesRequest.h" +#include "client/http/SearchTagsRequest.h" #include "common/platform/Platform.h" #include "common/tpt-minmax.h" #include "graphics/Graphics.h" @@ -132,13 +138,13 @@ void SearchController::SetPageRelative(int offset) void SearchController::ChangeSort() { - if(searchModel->GetSort() == "new") + if(searchModel->GetSort() == http::sortByDate) { - searchModel->SetSort("best"); + searchModel->SetSort(http::sortByVotes); } else { - searchModel->SetSort("new"); + searchModel->SetSort(http::sortByDate); } searchModel->UpdateSaveList(1, searchModel->GetLastQuery()); } @@ -183,7 +189,7 @@ void SearchController::SelectAllSaves() if (!Client::Ref().GetAuthUser().UserID) return; if (searchModel->GetShowOwn() || - Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator || + Client::Ref().GetAuthUser().UserElevation == User::ElevationMod || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin) searchModel->SelectAllSaves(); @@ -245,9 +251,16 @@ void SearchController::removeSelectedC() for (size_t i = 0; i < saves.size(); i++) { notifyStatus(String::Build("Deleting save [", saves[i], "] ...")); - if (Client::Ref().DeleteSave(saves[i])!=RequestOkay) + auto deleteSaveRequest = std::make_unique(saves[i]); + deleteSaveRequest->Start(); + deleteSaveRequest->Wait(); + try { - notifyError(String::Build("Failed to delete [", saves[i], "]: ", Client::Ref().GetLastError())); + deleteSaveRequest->Finish(); + } + catch (const http::RequestError &ex) + { + notifyError(String::Build("Failed to delete [", saves[i], "]: ", ByteString(ex.what()).FromAscii())); c->Refresh(); return false; } @@ -286,37 +299,49 @@ void SearchController::unpublishSelectedC(bool publish) public: UnpublishSavesTask(std::vector saves_, SearchController *c_, bool publish_) { saves = saves_; c = c_; publish = publish_; } - bool PublishSave(int saveID) + void PublishSave(int saveID) { notifyStatus(String::Build("Publishing save [", saveID, "]")); - if (Client::Ref().PublishSave(saveID) != RequestOkay) - return false; - return true; + auto publishSaveRequest = std::make_unique(saveID); + publishSaveRequest->Start(); + publishSaveRequest->Wait(); + publishSaveRequest->Finish(); } - bool UnpublishSave(int saveID) + void UnpublishSave(int saveID) { notifyStatus(String::Build("Unpublishing save [", saveID, "]")); - if (Client::Ref().UnpublishSave(saveID) != RequestOkay) - return false; - return true; + auto unpublishSaveRequest = std::make_unique(saveID); + unpublishSaveRequest->Start(); + unpublishSaveRequest->Wait(); + unpublishSaveRequest->Finish(); } bool doWork() override { - bool ret; for (size_t i = 0; i < saves.size(); i++) { - if (publish) - ret = PublishSave(saves[i]); - else - ret = UnpublishSave(saves[i]); - if (!ret) + try + { + if (publish) + { + PublishSave(saves[i]); + } + else + { + UnpublishSave(saves[i]); + } + } + catch (const http::RequestError &ex) { if (publish) // uses html page so error message will be spam + { notifyError(String::Build("Failed to publish [", saves[i], "], is this save yours?")); + } else - notifyError(String::Build("Failed to unpublish [", saves[i], "]: " + Client::Ref().GetLastError())); + { + notifyError(String::Build("Failed to unpublish [", saves[i], "]: ", ByteString(ex.what()).FromAscii())); + } c->Refresh(); return false; } @@ -343,9 +368,16 @@ void SearchController::FavouriteSelected() for (size_t i = 0; i < saves.size(); i++) { notifyStatus(String::Build("Favouring save [", saves[i], "]")); - if (Client::Ref().FavouriteSave(saves[i], true)!=RequestOkay) + auto favouriteSaveRequest = std::make_unique(saves[i], true); + favouriteSaveRequest->Start(); + favouriteSaveRequest->Wait(); + try { - notifyError(String::Build("Failed to favourite [", saves[i], "]: " + Client::Ref().GetLastError())); + favouriteSaveRequest->Finish(); + } + catch (const http::RequestError &ex) + { + notifyError(String::Build("Failed to favourite [", saves[i], "]: ", ByteString(ex.what()).FromAscii())); return false; } notifyProgress((i + 1) * 100 / saves.size()); @@ -364,9 +396,16 @@ void SearchController::FavouriteSelected() for (size_t i = 0; i < saves.size(); i++) { notifyStatus(String::Build("Unfavouring save [", saves[i], "]")); - if (Client::Ref().FavouriteSave(saves[i], false)!=RequestOkay) + auto unfavouriteSaveRequest = std::make_unique(saves[i], false); + unfavouriteSaveRequest->Start(); + unfavouriteSaveRequest->Wait(); + try { - notifyError(String::Build("Failed to unfavourite [", saves[i], "]: " + Client::Ref().GetLastError())); + unfavouriteSaveRequest->Finish(); + } + catch (const http::RequestError &ex) + { + notifyError(String::Build("Failed to unfavourite [", saves[i], "]: ", ByteString(ex.what()).FromAscii())); return false; } notifyProgress((i + 1) * 100 / saves.size()); diff --git a/src/gui/search/SearchModel.cpp b/src/gui/search/SearchModel.cpp index 9f7f2a8d8..1b03ea377 100644 --- a/src/gui/search/SearchModel.cpp +++ b/src/gui/search/SearchModel.cpp @@ -4,12 +4,14 @@ #include "client/SaveInfo.h" #include "client/GameSave.h" #include "client/Client.h" +#include "client/http/SearchSavesRequest.h" +#include "client/http/SearchTagsRequest.h" #include "common/tpt-minmax.h" #include #include SearchModel::SearchModel(): - currentSort("best"), + currentSort(http::sortByVotes), currentPage(1), resultCount(0), showOwn(false), @@ -28,81 +30,26 @@ bool SearchModel::GetShowTags() return showTags; } -void SearchModel::BeginSearchSaves(int start, int count, String query, ByteString sort, ByteString category) +void SearchModel::BeginSearchSaves(int start, int count, String query, http::Sort sort, http::Category category) { lastError = ""; resultCount = 0; - ByteStringBuilder urlStream; - ByteString data; - urlStream << SCHEME << SERVER << "/Browse.json?Start=" << start << "&Count=" << count; - if(query.length() || sort.length()) - { - urlStream << "&Search_Query="; - if(query.length()) - urlStream << format::URLEncode(query.ToUtf8()); - if(sort == "date") - { - if(query.length()) - urlStream << format::URLEncode(" "); - urlStream << format::URLEncode("sort:") << format::URLEncode(sort); - } - } - if(category.length()) - { - urlStream << "&Category=" << format::URLEncode(category); - } - searchSaves = std::make_unique(urlStream.Build()); - auto authUser = Client::Ref().GetAuthUser(); - if (authUser.UserID) - { - searchSaves->AuthHeaders(ByteString::Build(Client::Ref().GetAuthUser().UserID), Client::Ref().GetAuthUser().SessionID); - } + searchSaves = std::make_unique(start, count, query.ToUtf8(), sort, category); searchSaves->Start(); } std::vector> SearchModel::EndSearchSaves() { std::vector> saveArray; - auto [ dataStatus, data ] = searchSaves->Finish(); + try + { + std::tie(resultCount, saveArray) = searchSaves->Finish(); + } + catch (const http::RequestError &ex) + { + lastError = ByteString(ex.what()).FromUtf8(); + } searchSaves.reset(); - auto &client = Client::Ref(); - client.ParseServerReturn(data, dataStatus, true); - if (dataStatus == 200 && data.size()) - { - try - { - std::istringstream dataStream(data); - Json::Value objDocument; - dataStream >> objDocument; - - resultCount = objDocument["Count"].asInt(); - Json::Value savesArray = objDocument["Saves"]; - for (Json::UInt j = 0; j < savesArray.size(); j++) - { - int tempID = savesArray[j]["ID"].asInt(); - int tempCreatedDate = savesArray[j]["Created"].asInt(); - int tempUpdatedDate = savesArray[j]["Updated"].asInt(); - int tempScoreUp = savesArray[j]["ScoreUp"].asInt(); - int tempScoreDown = savesArray[j]["ScoreDown"].asInt(); - ByteString tempUsername = savesArray[j]["Username"].asString(); - String tempName = ByteString(savesArray[j]["Name"].asString()).FromUtf8(); - int tempVersion = savesArray[j]["Version"].asInt(); - bool tempPublished = savesArray[j]["Published"].asBool(); - auto tempSaveInfo = std::make_unique(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(); - } return saveArray; } @@ -117,40 +64,22 @@ void SearchModel::BeginGetTags(int start, int count, String query) if(query.length()) urlStream << format::URLEncode(query.ToUtf8()); } - getTags = std::make_unique(urlStream.Build()); + getTags = std::make_unique(start, count, query.ToUtf8()); getTags->Start(); } std::vector> SearchModel::EndGetTags() { std::vector> tagArray; - auto [ dataStatus, data ] = getTags->Finish(); + try + { + tagArray = getTags->Finish(); + } + catch (const http::RequestError &ex) + { + lastError = ByteString(ex.what()).FromUtf8(); + } getTags.reset(); - if(dataStatus == 200 && data.size()) - { - try - { - std::istringstream dataStream(data); - Json::Value objDocument; - dataStream >> objDocument; - - Json::Value tagsArray = objDocument["Tags"]; - for (Json::UInt j = 0; j < tagsArray.size(); j++) - { - int tagCount = tagsArray[j]["Count"].asInt(); - ByteString tag = tagsArray[j]["Tag"].asString(); - tagArray.push_back(std::pair(tag, tagCount)); - } - } - catch (std::exception & e) - { - lastError = "Could not read response: " + ByteString(e.what()).FromUtf8(); - } - } - else - { - lastError = http::StatusText(dataStatus); - } return tagArray; } @@ -166,7 +95,7 @@ bool SearchModel::UpdateSaveList(int pageNumber, String query) //resultCount = 0; currentPage = pageNumber; - if(pageNumber == 1 && !showOwn && !showFavourite && currentSort == "best" && query == "") + if(pageNumber == 1 && !showOwn && !showFavourite && currentSort == http::sortByVotes && query == "") SetShowTags(true); else SetShowTags(false); @@ -182,12 +111,16 @@ bool SearchModel::UpdateSaveList(int pageNumber, String query) BeginGetTags(0, 24, ""); } - ByteString category = ""; - if(showFavourite) - category = "Favourites"; - if(showOwn && Client::Ref().GetAuthUser().UserID) - category = "by:"+Client::Ref().GetAuthUser().Username; - BeginSearchSaves((currentPage-1)*20, 20, lastQuery, currentSort=="new"?"date":"votes", category); + auto category = http::categoryNone; + if (showFavourite) + { + category = http::categoryFavourites; + } + if (showOwn && Client::Ref().GetAuthUser().UserID) + { + category = http::categoryMyOwn; + } + BeginSearchSaves((currentPage-1)*20, 20, lastQuery, currentSort, category); return true; } return false; @@ -363,7 +296,7 @@ void SearchModel::notifySelectedChanged() int SearchModel::GetPageCount() { - if (!showOwn && !showFavourite && currentSort == "best" && lastQuery == "") + if (!showOwn && !showFavourite && currentSort == http::sortByVotes && lastQuery == "") return std::max(1, (int)(ceil(resultCount/20.0f))+1); //add one for front page (front page saves are repeated twice) else return std::max(1, (int)(ceil(resultCount/20.0f))); diff --git a/src/gui/search/SearchModel.h b/src/gui/search/SearchModel.h index ef440b46b..51a7fa177 100644 --- a/src/gui/search/SearchModel.h +++ b/src/gui/search/SearchModel.h @@ -1,26 +1,32 @@ #pragma once #include "common/String.h" -#include "client/http/Request.h" +#include "client/Search.h" #include "Config.h" #include #include #include +namespace http +{ + class SearchSavesRequest; + class SearchTagsRequest; +} + class SaveInfo; class SearchView; class SearchModel { private: - std::unique_ptr searchSaves; - void BeginSearchSaves(int start, int count, String query, ByteString sort, ByteString category); + std::unique_ptr searchSaves; + void BeginSearchSaves(int start, int count, String query, http::Sort sort, http::Category category); std::vector> EndSearchSaves(); void BeginGetTags(int start, int count, String query); std::vector> EndGetTags(); - std::unique_ptr getTags; + std::unique_ptr getTags; std::unique_ptr loadedSave; - ByteString currentSort; + http::Sort currentSort; String lastQuery; String lastError; std::vector selected; @@ -55,8 +61,8 @@ public: int GetPageCount(); int GetPageNum() { return currentPage; } String GetLastQuery() { return lastQuery; } - void SetSort(ByteString sort) { if(!searchSaves) { currentSort = sort; } notifySortChanged(); } - ByteString GetSort() { return currentSort; } + void SetSort(http::Sort sort) { if(!searchSaves) { currentSort = sort; } notifySortChanged(); } + http::Sort GetSort() { return currentSort; } void SetShowOwn(bool show) { if(!searchSaves) { if(show!=showOwn) { showOwn = show; } } notifyShowOwnChanged(); } bool GetShowOwn() { return showOwn; } void SetShowFavourite(bool show) { if(show!=showFavourite && !searchSaves) { showFavourite = show; } notifyShowFavouriteChanged(); } diff --git a/src/gui/search/SearchView.cpp b/src/gui/search/SearchView.cpp index 8803ba13b..6f1ede05a 100644 --- a/src/gui/search/SearchView.cpp +++ b/src/gui/search/SearchView.cpp @@ -211,7 +211,7 @@ void SearchView::Search(String query) void SearchView::NotifySortChanged(SearchModel * sender) { - if(sender->GetSort() == "best") + if(sender->GetSort() == http::sortByVotes) { sortButton->SetToggleState(false); sortButton->SetText("By votes"); @@ -228,7 +228,7 @@ void SearchView::NotifySortChanged(SearchModel * sender) void SearchView::NotifyShowOwnChanged(SearchModel * sender) { ownButton->SetToggleState(sender->GetShowOwn()); - if(sender->GetShowOwn() || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator) + if(sender->GetShowOwn() || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod) { unpublishSelected->Enabled = true; removeSelected->Enabled = true; @@ -248,7 +248,7 @@ void SearchView::NotifyShowFavouriteChanged(SearchModel * sender) unpublishSelected->Enabled = false; removeSelected->Enabled = false; } - else if(sender->GetShowOwn() || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator) + else if(sender->GetShowOwn() || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod) { unpublishSelected->Enabled = true; removeSelected->Enabled = true; @@ -323,7 +323,7 @@ void SearchView::CheckAccess() favButton->Enabled = true; favouriteSelected->Enabled = true; - if (Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator) + if (Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod) { unpublishSelected->Enabled = true; removeSelected->Enabled = true; @@ -569,7 +569,7 @@ void SearchView::NotifySaveListChanged(SearchModel * sender) }); if(Client::Ref().GetAuthUser().UserID) saveButton->SetSelectable(true); - if (saves[i]->GetUserName() == Client::Ref().GetAuthUser().Username || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator) + if (saves[i]->GetUserName() == Client::Ref().GetAuthUser().Username || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod) saveButton->SetShowVotes(true); saveButtons.push_back(saveButton); AddComponent(saveButton); diff --git a/src/gui/tags/TagsController.cpp b/src/gui/tags/TagsController.cpp index 428954625..df9ec12b3 100644 --- a/src/gui/tags/TagsController.cpp +++ b/src/gui/tags/TagsController.cpp @@ -1,8 +1,8 @@ #include "TagsController.h" - #include "TagsModel.h" #include "TagsView.h" - +#include "client/http/AddTagRequest.h" +#include "client/http/RemoveTagRequest.h" #include "gui/interface/Engine.h" #include "client/SaveInfo.h" #include "Controller.h" @@ -36,6 +36,11 @@ void TagsController::AddTag(ByteString tag) tagsModel->AddTag(tag); } +void TagsController::Tick() +{ + tagsModel->Tick(); +} + void TagsController::Exit() { tagsView->CloseActiveWindow(); diff --git a/src/gui/tags/TagsController.h b/src/gui/tags/TagsController.h index 70838482b..d0c69f625 100644 --- a/src/gui/tags/TagsController.h +++ b/src/gui/tags/TagsController.h @@ -18,5 +18,6 @@ public: void RemoveTag(ByteString tag); void AddTag(ByteString tag); void Exit(); + void Tick(); virtual ~TagsController(); }; diff --git a/src/gui/tags/TagsModel.cpp b/src/gui/tags/TagsModel.cpp index d320ea460..4ed33a5ab 100644 --- a/src/gui/tags/TagsModel.cpp +++ b/src/gui/tags/TagsModel.cpp @@ -1,13 +1,15 @@ #include "TagsModel.h" - #include "TagsView.h" #include "TagsModelException.h" - #include "client/Client.h" #include "client/SaveInfo.h" +#include "client/http/AddTagRequest.h" +#include "client/http/RemoveTagRequest.h" +#include "gui/dialogues/ErrorMessage.h" void TagsModel::SetSave(SaveInfo *newSave /* non-owning */) { + queuedTags.clear(); this->save = newSave; notifyTagsChanged(); } @@ -17,40 +19,73 @@ SaveInfo *TagsModel::GetSave() // non-owning return save; } -void TagsModel::RemoveTag(ByteString tag) +void TagsModel::Tick() { - if(save) + auto triggerTags = false; + std::list tags; + if (addTagRequest && addTagRequest->CheckDone()) { - std::list * tags = Client::Ref().RemoveTag(save->GetID(), tag); - if(tags) + try { - save->SetTags(std::list(*tags)); - notifyTagsChanged(); - delete tags; + tags = addTagRequest->Finish(); + triggerTags = true; } - else + catch (const http::RequestError &ex) { - throw TagsModelException(Client::Ref().GetLastError()); + 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) + { + save->SetTags(tags); + } + notifyTagsChanged(); + } + if (!addTagRequest && !removeTagRequest && !queuedTags.empty()) + { + auto it = queuedTags.begin(); + auto [ tag, add ] = *it; + queuedTags.erase(it); + if (save) + { + if (add) + { + addTagRequest = std::make_unique(save->GetID(), tag); + addTagRequest->Start(); + } + else + { + removeTagRequest = std::make_unique(save->GetID(), tag); + removeTagRequest->Start(); + } } } } +void TagsModel::RemoveTag(ByteString tag) +{ + queuedTags[tag] = false; +} + void TagsModel::AddTag(ByteString tag) { - if(save) - { - std::list * tags = Client::Ref().AddTag(save->GetID(), tag); - if(tags) - { - save->SetTags(std::list(*tags)); - notifyTagsChanged(); - delete tags; - } - else - { - throw TagsModelException(Client::Ref().GetLastError()); - } - } + queuedTags[tag] = true; } void TagsModel::AddObserver(TagsView * observer) diff --git a/src/gui/tags/TagsModel.h b/src/gui/tags/TagsModel.h index a69d31ec1..b1a9d1bce 100644 --- a/src/gui/tags/TagsModel.h +++ b/src/gui/tags/TagsModel.h @@ -1,11 +1,22 @@ #pragma once #include "common/String.h" #include +#include +#include + +namespace http +{ + class AddTagRequest; + class RemoveTagRequest; +} class SaveInfo; class TagsView; class TagsModel { + std::unique_ptr addTagRequest; + std::unique_ptr removeTagRequest; + std::map queuedTags; SaveInfo *save = nullptr; // non-owning std::vector observers; void notifyTagsChanged(); @@ -15,4 +26,5 @@ public: void RemoveTag(ByteString tag); void AddTag(ByteString tag); SaveInfo *GetSave(); // non-owning + void Tick(); }; diff --git a/src/gui/tags/TagsView.cpp b/src/gui/tags/TagsView.cpp index a5cae8730..b366cc8c7 100644 --- a/src/gui/tags/TagsView.cpp +++ b/src/gui/tags/TagsView.cpp @@ -49,6 +49,11 @@ TagsView::TagsView(): AddComponent(title); } +void TagsView::OnTick(float dt) +{ + c->Tick(); +} + void TagsView::OnDraw() { Graphics * g = GetGraphics(); @@ -76,7 +81,7 @@ void TagsView::NotifyTagsChanged(TagsModel * sender) tags.push_back(tempLabel); AddComponent(tempLabel); - if(sender->GetSave()->GetUserName() == Client::Ref().GetAuthUser().Username || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator) + if(sender->GetSave()->GetUserName() == Client::Ref().GetAuthUser().Username || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin || Client::Ref().GetAuthUser().UserElevation == User::ElevationMod) { ui::Button * tempButton = new ui::Button(ui::Point(15, 37+(16*i)), ui::Point(11, 12)); tempButton->Appearance.icon = IconDelete; @@ -85,14 +90,7 @@ void TagsView::NotifyTagsChanged(TagsModel * sender) tempButton->Appearance.HorizontalAlign = ui::Appearance::AlignCentre; tempButton->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; tempButton->SetActionCallback({ [this, tag] { - try - { - c->RemoveTag(tag); - } - catch(TagsModelException & ex) - { - new ErrorMessage("Could not remove tag", ByteString(ex.what()).FromUtf8()); - } + c->RemoveTag(tag); } }); tags.push_back(tempButton); AddComponent(tempButton); @@ -125,13 +123,6 @@ void TagsView::addTag() new ErrorMessage("Tag not long enough", "Must be at least 4 letters"); return; } - try - { - c->AddTag(tagInput->GetText().ToUtf8()); - } - catch(TagsModelException & ex) - { - new ErrorMessage("Could not add tag", ByteString(ex.what()).FromUtf8()); - } + c->AddTag(tagInput->GetText().ToUtf8()); tagInput->SetText(""); } diff --git a/src/gui/tags/TagsView.h b/src/gui/tags/TagsView.h index 11f3cc6bf..6e3a92295 100644 --- a/src/gui/tags/TagsView.h +++ b/src/gui/tags/TagsView.h @@ -22,6 +22,7 @@ class TagsView: public ui::Window { public: TagsView(); void OnDraw() override; + void OnTick(float dt) override; void AttachController(TagsController * c_) { c = c_; } void OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) override; void NotifyTagsChanged(TagsModel * sender); diff --git a/src/gui/update/UpdateActivity.cpp b/src/gui/update/UpdateActivity.cpp index e44beb58a..bad425682 100644 --- a/src/gui/update/UpdateActivity.cpp +++ b/src/gui/update/UpdateActivity.cpp @@ -1,7 +1,6 @@ #include "UpdateActivity.h" #include "client/http/Request.h" #include "prefs/GlobalPrefs.h" -#include "client/Client.h" #include "common/platform/Platform.h" #include "tasks/Task.h" #include "tasks/TaskWindow.h" @@ -53,7 +52,16 @@ private: Platform::Millisleep(1); } - auto [ status, data ] = request->Finish(); + int status; + ByteString data; + try + { + std::tie(status, data) = request->Finish(); + } + catch (const http::RequestError &ex) + { + return niceNotifyError("Could not download update: " + String::Build("Server responded with Status ", ByteString(ex.what()).FromAscii())); + } if (status!=200) { return niceNotifyError("Could not download update: " + String::Build("Server responded with Status ", status)); @@ -107,9 +115,9 @@ private: } }; -UpdateActivity::UpdateActivity() { - ByteString file = ByteString::Build(SCHEME, USE_UPDATESERVER ? UPDATESERVER : SERVER, Client::Ref().GetUpdateInfo().File); - updateDownloadTask = new UpdateDownloadTask(file, this); +UpdateActivity::UpdateActivity(UpdateInfo info) +{ + updateDownloadTask = new UpdateDownloadTask(info.file, this); updateWindow = new TaskWindow("Downloading update...", updateDownloadTask, true); } diff --git a/src/gui/update/UpdateActivity.h b/src/gui/update/UpdateActivity.h index f6adfbbc1..159a270f2 100644 --- a/src/gui/update/UpdateActivity.h +++ b/src/gui/update/UpdateActivity.h @@ -1,4 +1,5 @@ #pragma once +#include "client/StartupInfo.h" class Task; class TaskWindow; @@ -7,7 +8,7 @@ class UpdateActivity Task * updateDownloadTask; TaskWindow * updateWindow; public: - UpdateActivity(); + UpdateActivity(UpdateInfo info); virtual ~UpdateActivity(); void Exit(); virtual void NotifyDone(Task * sender); diff --git a/src/lua/LegacyLuaAPI.cpp b/src/lua/LegacyLuaAPI.cpp index 753adb634..be23d81cb 100644 --- a/src/lua/LegacyLuaAPI.cpp +++ b/src/lua/LegacyLuaAPI.cpp @@ -18,7 +18,6 @@ #include "gui/game/GameController.h" #include "gui/game/GameModel.h" #include "gui/interface/Engine.h" -#include "client/http/Request.h" #include #include #include @@ -1292,48 +1291,6 @@ int luatpt_setdrawcap(lua_State* l) return 0; } -int luatpt_getscript(lua_State* l) -{ - int scriptID = luaL_checkinteger(l, 1); - auto filename = tpt_lua_checkByteString(l, 2); - int runScript = luaL_optint(l, 3, 0); - int confirmPrompt = luaL_optint(l, 4, 1); - - ByteString url = ByteString::Build(SCHEME, "starcatcher.us/scripts/main.lua?get=", scriptID); - if (confirmPrompt && !ConfirmPrompt::Blocking("Do you want to install script?", url.FromUtf8(), "Install")) - return 0; - - auto [ ret, scriptData ] = http::Request::Simple(url); - if (!scriptData.size()) - { - return luaL_error(l, "Server did not return data"); - } - if (ret != 200) - { - return luaL_error(l, http::StatusText(ret).ToUtf8().c_str()); - } - - if (scriptData.Contains("Invalid script ID")) - { - return luaL_error(l, "Invalid Script ID"); - } - - if (Platform::FileExists(filename) && confirmPrompt && !ConfirmPrompt::Blocking("File already exists, overwrite?", filename.FromUtf8(), "Overwrite")) - { - return 0; - } - if (!Platform::WriteFile(std::vector(scriptData.begin(), scriptData.end()), filename)) - { - return luaL_error(l, "Unable to write to file"); - } - if (runScript) - { - tpt_lua_dostring(l, ByteString::Build("dofile('", filename, "')")); - } - - return 0; -} - int luatpt_setwindowsize(lua_State* l) { int scale = luaL_optint(l,1,1); diff --git a/src/lua/LuaHttp.cpp b/src/lua/LuaHttp.cpp index fb966c9bd..4f619d847 100644 --- a/src/lua/LuaHttp.cpp +++ b/src/lua/LuaHttp.cpp @@ -132,7 +132,17 @@ public: { headers = request->ResponseHeaders(); } - std::tie(status, data) = request->Finish(); + // Get this separately so it's always present. + status = request->StatusCode(); + try + { + data = request->Finish().second; + } + catch (const http::RequestError &ex) + { + // Nothing, the only way to fail here is to fail in RequestManager, and + // that means the problem has already been printed to std::cerr. + } request.reset(); if (type == getAuthToken) { diff --git a/src/lua/LuaScriptInterface.cpp b/src/lua/LuaScriptInterface.cpp index abc918e93..c197d5f2f 100644 --- a/src/lua/LuaScriptInterface.cpp +++ b/src/lua/LuaScriptInterface.cpp @@ -24,6 +24,7 @@ #include "client/GameSave.h" #include "client/SaveFile.h" #include "client/SaveInfo.h" +#include "client/http/Request.h" #include "common/platform/Platform.h" #include "graphics/Graphics.h" #include "graphics/Renderer.h" @@ -44,6 +45,9 @@ #include "gui/game/GameModel.h" #include "gui/game/Tool.h" #include "gui/game/Brush.h" +#include "gui/dialogues/ConfirmPrompt.h" +#include "gui/dialogues/ErrorMessage.h" +#include "gui/dialogues/InformationMessage.h" #include "eventcompat.lua.h" @@ -4398,6 +4402,52 @@ bool LuaScriptInterface::HandleEvent(const GameControllerEvent &event) void LuaScriptInterface::OnTick() { + if (scriptDownload && scriptDownload->CheckDone()) + { + + auto ret = scriptDownload->StatusCode(); + ByteString scriptData; + auto handleResponse = [this, &scriptData, &ret]() { + if (!scriptData.size()) + { + new ErrorMessage("Script download", "Server did not return data"); + return; + } + if (ret != 200) + { + new ErrorMessage("Script download", ByteString(http::StatusText(ret)).FromUtf8()); + return; + } + if (Platform::FileExists(scriptDownloadFilename) && scriptDownloadConfirmPrompt && !ConfirmPrompt::Blocking("File already exists, overwrite?", scriptDownloadFilename.FromUtf8(), "Overwrite")) + { + return; + } + if (!Platform::WriteFile(std::vector(scriptData.begin(), scriptData.end()), scriptDownloadFilename)) + { + new ErrorMessage("Script download", "Unable to write to file"); + return; + } + if (scriptDownloadRunScript) + { + if (tpt_lua_dostring(l, ByteString::Build("dofile('", scriptDownloadFilename, "')"))) + { + new ErrorMessage("Script download", luacon_geterror()); + return; + } + } + new InformationMessage("Script download", "Script successfully downloaded", false); + }; + try + { + scriptData = scriptDownload->Finish().second; + handleResponse(); + } + catch (const http::RequestError &ex) + { + new ErrorMessage("Script download", ByteString(ex.what()).FromUtf8()); + } + scriptDownload.reset(); + } lua_getglobal(l, "simulation"); if (lua_istable(l, -1)) { @@ -4815,3 +4865,33 @@ CommandInterface *CommandInterface::Create(GameController * c, GameModel * m) return new LuaScriptInterface(c, m); } +int LuaScriptInterface::luatpt_getscript(lua_State* l) +{ + auto *luacon_ci = static_cast(commandInterface); + + int scriptID = luaL_checkinteger(l, 1); + auto filename = tpt_lua_checkByteString(l, 2); + bool runScript = luaL_optint(l, 3, 0); + int confirmPrompt = luaL_optint(l, 4, 1); + + if (luacon_ci->scriptDownload) + { + new ErrorMessage("Script download", "A script download is already pending"); + return 0; + } + + ByteString url = ByteString::Build(SCHEME, "starcatcher.us/scripts/main.lua?get=", scriptID); + if (confirmPrompt && !ConfirmPrompt::Blocking("Do you want to install script?", url.FromUtf8(), "Install")) + { + return 0; + } + + luacon_ci->scriptDownload = std::make_unique(url); + luacon_ci->scriptDownload->Start(); + luacon_ci->scriptDownloadFilename = filename; + luacon_ci->scriptDownloadRunScript = runScript; + luacon_ci->scriptDownloadConfirmPrompt = confirmPrompt; + + luacon_controller->HideConsole(); + return 0; +} diff --git a/src/lua/LuaScriptInterface.h b/src/lua/LuaScriptInterface.h index 6cc72911e..dcc1fe5e6 100644 --- a/src/lua/LuaScriptInterface.h +++ b/src/lua/LuaScriptInterface.h @@ -7,6 +7,12 @@ #include "simulation/StructProperty.h" #include "simulation/ElementDefs.h" #include +#include + +namespace http +{ + class Request; +} namespace ui { @@ -22,6 +28,11 @@ class LuaComponent; class LuaScriptInterface: public TPTScriptInterface { + std::unique_ptr scriptDownload; + ByteString scriptDownloadFilename; + bool scriptDownloadRunScript; + bool scriptDownloadConfirmPrompt; + int luacon_mousex, luacon_mousey, luacon_mousebutton; ByteString luacon_selectedl, luacon_selectedr, luacon_selectedalt, luacon_selectedreplace; bool luacon_mousedown; @@ -181,6 +192,8 @@ class LuaScriptInterface: public TPTScriptInterface static int event_unregister(lua_State * l); static int event_getmodifiers(lua_State * l); + static int luatpt_getscript(lua_State * l); + void initHttpAPI(); void initSocketAPI(); diff --git a/src/prefs/Prefs.cpp b/src/prefs/Prefs.cpp index 23bdb4ab0..3561011da 100644 --- a/src/prefs/Prefs.cpp +++ b/src/prefs/Prefs.cpp @@ -1,6 +1,7 @@ #include "Prefs.h" #include "common/platform/Platform.h" #include "common/tpt-rand.h" +#include "client/User.h" #include #include @@ -115,6 +116,9 @@ template<> ByteString Prefs::Bipacker::Unpack(const Json::Value &va template<> Json::Value Prefs::Bipacker::Pack (const String &value) { return Json::Value(value.ToUtf8()); } template<> String Prefs::Bipacker::Unpack(const Json::Value &value) { return ByteString(value.asString()).FromUtf8(); } +template<> Json::Value Prefs::Bipacker::Pack (const User::Elevation &value) { return Json::Value(User::ElevationToString(value)); } +template<> User::Elevation Prefs::Bipacker::Unpack(const Json::Value &value) { return User::ElevationFromString(value.asString()); } + template struct Prefs::Bipacker> {