From ca457045e89f9bfea5314ffa0d13eb3b29227f8f Mon Sep 17 00:00:00 2001 From: Rafael Date: Thu, 28 Nov 2024 23:25:33 -0600 Subject: [PATCH] Working user profile download, avatar download --- source/main/Application.h | 6 +- source/main/gui/panels/GUI_GameMainMenu.cpp | 21 ++- source/main/gui/panels/GUI_LoginBox.cpp | 190 +++++++++++++++++--- source/main/gui/panels/GUI_LoginBox.h | 25 ++- source/main/main.cpp | 41 ++++- 5 files changed, 234 insertions(+), 49 deletions(-) diff --git a/source/main/Application.h b/source/main/Application.h index fed0a7bb45..7d536820a3 100644 --- a/source/main/Application.h +++ b/source/main/Application.h @@ -106,13 +106,17 @@ enum MsgType MSG_NET_RECV_ERROR, MSG_NET_USERAUTH_SUCCESS, //!< Payload = GUI::UserAuthToken* (owner) MSG_NET_USERAUTH_FAILURE, + MSG_NET_USERAUTH_LOGOUT_REQUESTED, MSG_NET_USERAUTH_RV_REQUESTED, MSG_NET_USERAUTH_RV_FAILURE, MSG_NET_USERAUTH_RV_SUCCESS, - MSG_NET_USERAUTH_PROFILE_REQUESTED, MSG_NET_USERAUTH_TFA_REQUESTED, MSG_NET_USERAUTH_TFA_FAILURE, MSG_NET_USERAUTH_TFA_TRIGGERED, + MSG_NET_USERPROFILE_REQUESTED, + MSG_NET_USERPROFILE_FINISHED, + MSG_NET_USERPROFILE_AVATAR_REQUESTED, + MSG_NET_USERPROFILE_AVATAR_FINISHED, MSG_NET_REFRESH_SERVERLIST_SUCCESS, //!< Payload = GUI::MpServerInfoVec* (owner) MSG_NET_REFRESH_SERVERLIST_FAILURE, //!< Payload = RoR::CurlFailInfo* (owner) MSG_NET_REFRESH_REPOLIST_SUCCESS, //!< Payload = GUI::ResourcesCollection* (owner) diff --git a/source/main/gui/panels/GUI_GameMainMenu.cpp b/source/main/gui/panels/GUI_GameMainMenu.cpp index 9d03536ce7..c5bd9db764 100644 --- a/source/main/gui/panels/GUI_GameMainMenu.cpp +++ b/source/main/gui/panels/GUI_GameMainMenu.cpp @@ -241,15 +241,28 @@ void GameMainMenu::DrawProfileBox() ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar; if (ImGui::Begin(_LC("MainMenu", "Profile box"), nullptr, flags)) { - ImGui::Image( - reinterpret_cast(FetchIcon("blank.png")->getHandle()), - image_size); if (App::remote_user_auth_state->getEnum() == UserAuthState::AUTHENTICATED) { + const auto& user = App::GetGuiManager()->LoginBox.GetUserProfile(); + + if (!user.avatar) + { + ImGui::Image( + reinterpret_cast(FetchIcon("blank.png")->getHandle()), + image_size); + } + else if (user.avatar) + { + ImGui::Image( + reinterpret_cast(user.avatar->getHandle()), + image_size); + } + ImGui::SameLine(); + ImGui::Text("Hello, %s", user.username.c_str()); if (ImGui::Button("Log out", button_size)) { - // TODO open as a link + App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_LOGOUT_REQUESTED)); } } else diff --git a/source/main/gui/panels/GUI_LoginBox.cpp b/source/main/gui/panels/GUI_LoginBox.cpp index f73388764a..adb0169fa4 100644 --- a/source/main/gui/panels/GUI_LoginBox.cpp +++ b/source/main/gui/panels/GUI_LoginBox.cpp @@ -29,6 +29,7 @@ #include "GUIManager.h" #include "GUIUtils.h" #include "AppContext.h" +#include "PlatformUtils.h" #include "Language.h" #include "RoRVersion.h" @@ -61,6 +62,102 @@ static size_t CurlWriteFunc(void* ptr, size_t size, size_t nmemb, std::string* d return size * nmemb; } +static size_t CurlOgreDataStreamWriteFunc(char* data_ptr, size_t _unused, size_t data_length, void* userdata) +{ + Ogre::DataStream* ogre_datastream = static_cast(userdata); + if (data_length > 0 && ogre_datastream->isWriteable()) + { + return ogre_datastream->write((const void*)data_ptr, data_length); + } + else + { + return 0; + } +} + +void GetUserProfileAvatarTask(int user_id, std::string avatar_url) +{ + // The avatar URL may not be of *.rigsofrods.org, as it may also be a gravatar. + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string filename = std::to_string(user_id) + ".png"; + std::string file = PathCombine(App::sys_avatar_dir->getStr(), filename); + long response_code = 0; + + CURL* curl = curl_easy_init(); + Ogre::DataStreamPtr datastream = Ogre::ResourceGroupManager::getSingleton().createResource(file, RGN_AVATAR); + + curl_easy_setopt(curl, CURLOPT_URL, avatar_url.c_str()); + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlOgreDataStreamWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, datastream.get()); + + CURLcode curl_result = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + if (curl_result != CURLE_OK || response_code != 200) + { + Ogre::LogManager::getSingleton().stream() + << "[RoR|UserAuthManager] Failed to download user avatar, player will have a default avatar" + << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code; + } + + curl_easy_cleanup(curl); + curl = nullptr; + // sweep sweep + // send back the file saved to the request queue + // so wecan update the user icon on the fly + App::GetGameContext()->PushMessage(Message(MSG_NET_USERPROFILE_AVATAR_FINISHED, file)); +} + +void UserAuthInvalidateTokenTask() +{ + std::string auth_header = std::string("Authorization: Bearer ") + App::remote_login_token->getStr(); + std::string user_agent = fmt::format("{}/{}", "Rigs of Rods Client", ROR_VERSION_STRING); + std::string url = App::remote_query_url->getStr() + "/auth/logout"; + std::string response_payload; + std::string response_header; + long response_code = 0; + + struct curl_slist* slist; + slist = NULL; + slist = curl_slist_append(slist, "Accept: application/json"); + slist = curl_slist_append(slist, "Content-Type: application/json"); + slist = curl_slist_append(slist, auth_header.c_str()); + + CURL* curl = curl_easy_init(); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // todo api url + endpoint +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); + + CURLcode curl_result = curl_easy_perform(curl); + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + + curl_easy_cleanup(curl); + curl = nullptr; + slist = NULL; + + rapidjson::Document j_data_doc; + j_data_doc.Parse(response_payload.c_str()); + + if (curl_result != CURLE_OK || response_code != 200) + { + Ogre::LogManager::getSingleton().stream() + << "[RoR|UserAuthManager] Failed invalidate user tokens, the player's user profile will still be deleted;" + << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code; + return; + } + + // job done +} + void GetUserProfileTask() { std::string auth_header = std::string("Authorization: Bearer ") + App::remote_login_token->getStr(); @@ -78,7 +175,11 @@ void GetUserProfileTask() CURL* curl = curl_easy_init(); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); // todo api url + endpoint +#ifdef _WIN32 + curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA); +#endif // _WIN32 curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload); curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header); @@ -89,8 +190,8 @@ void GetUserProfileTask() curl = nullptr; slist = NULL; - rapidjson::Document j_response_body; - j_response_body.Parse(response_payload.c_str()); + rapidjson::Document j_data_doc; + j_data_doc.Parse(response_payload.c_str()); if (curl_result != CURLE_OK || response_code != 200) { @@ -101,9 +202,16 @@ void GetUserProfileTask() } GUI::UserProfile* user_profile_ptr = new GUI::UserProfile(); - - // this could be a success, of sorts - return; + rapidjson::Value& j_response_body = j_data_doc["me"]; + user_profile_ptr->username = j_response_body["username"].GetString(); + user_profile_ptr->avatar_url = j_response_body["avatar_urls"]["o"].GetString(); // Consider all sizes, later + user_profile_ptr->email = j_response_body["email"].GetString(); + user_profile_ptr->user_id = j_response_body["user_id"].GetInt(); + user_profile_ptr->avatar = Ogre::TexturePtr(); + + App::GetGameContext()->PushMessage( + Message(MSG_NET_USERPROFILE_FINISHED, + static_cast(user_profile_ptr))); } void ValidateOrRefreshTokenTask(std::string login_token, std::string refresh_token) @@ -220,13 +328,13 @@ void UserAuthWithTfaTask(std::string login, std::string passwd, std::string prov rapidjson::Document j_response_body; j_response_body.Parse(response_payload.c_str()); - if (j_response_body.HasParseError() || !j_response_body.IsObject()) - { - App::GetGameContext()->PushMessage( - Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) - ); - return; - } + //if (j_response_body.HasParseError() || !j_response_body.IsObject()) + //{ + // App::GetGameContext()->PushMessage( + // Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) + // ); + // return; + //} if (response_code == 400) // a failure, bad tfa code { @@ -307,7 +415,7 @@ void PostAuthTriggerTfa(std::string login, std::string passwd, std::string provi App::GetGameContext()->PushMessage(Message(MSG_NET_USERAUTH_TFA_TRIGGERED)); } -void PostAuth(std::string login, std::string passwd) +void UserAuthTask(std::string login, std::string passwd) { rapidjson::Document j_request_body; j_request_body.SetObject(); @@ -352,13 +460,13 @@ void PostAuth(std::string login, std::string passwd) rapidjson::Document j_response_body; j_response_body.Parse(response_payload.c_str()); - if (j_response_body.HasParseError() || !j_response_body.IsArray()) - { - App::GetGameContext()->PushMessage( - Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) - ); - return; - } + //if (j_response_body.HasParseError() || !j_response_body.IsArray()) + //{ + // App::GetGameContext()->PushMessage( + // Message(MSG_NET_USERAUTH_FAILURE, _LC("Login", "There was an unexpected server error. Please retry.")) + // ); + // return; + //} if (response_code == 400) { @@ -411,12 +519,7 @@ void PostAuth(std::string login, std::string passwd) LoginBox::LoginBox() : m_base_url(App::remote_query_url->getStr() + "/auth") -{ - Ogre::WorkQueue* wq = Ogre::Root::getSingleton().getWorkQueue(); - m_ogre_workqueue_channel = wq->getChannel("RoR/UserAvatars"); - wq->addRequestHandler(m_ogre_workqueue_channel, this); - wq->addResponseHandler(m_ogre_workqueue_channel, this); -} +{ } LoginBox::~LoginBox() {} @@ -522,11 +625,20 @@ void LoginBox::Login() std::string login(m_login); std::string passwd(m_passwd); - std::packaged_task task(PostAuth); + std::packaged_task task(UserAuthTask); std::thread(std::move(task), login, passwd).detach(); #endif } +void LoginBox::Logout() +{ +#if defined(USE_CURL) + std::thread([] { + UserAuthInvalidateTokenTask(); + }).detach(); +#endif +} + void LoginBox::UpdateUserAuth(UserAuthToken* data) { if (data) { @@ -537,19 +649,39 @@ void LoginBox::UpdateUserAuth(UserAuthToken* data) } } -void LoginBox::UpdateUserProfile() +void LoginBox::FetchUserProfile() { #if defined(USE_CURL) std::thread(GetUserProfileTask).detach(); #endif // defined(USE_CURL) } +void LoginBox::FetchUserProfileAvatar() +{ +#if defined(USE_CURL) + std::packaged_task task(GetUserProfileAvatarTask); + std::thread(std::move(task), m_user_profile.user_id, m_user_profile.avatar_url).detach(); +#endif // defined(USE_CURL) +} + +void LoginBox::UpdateUserProfile(UserProfile* data) +{ + m_user_profile = *data; +} + void LoginBox::ShowError(std::string const& msg) { m_loading = false; m_errors = msg; } +void LoginBox::UpdateUserProfileAvatar(std::string file) +{ + // runs on main thread? yes. thread safe? no + m_user_profile.avatar = FetchIcon(file.c_str()); + m_user_profile.avatar->load(); +} + void LoginBox::ConfirmTfa() { #if defined(USE_CURL) @@ -602,7 +734,7 @@ void LoginBox::TfaTriggered() void LoginBox::ValidateOrRefreshToken() { #if defined(USE_CURL) - m_loading = true; + //m_loading = true; std::packaged_task task(ValidateOrRefreshTokenTask); std::thread( diff --git a/source/main/gui/panels/GUI_LoginBox.h b/source/main/gui/panels/GUI_LoginBox.h index 544fcea9b1..4a825ae028 100644 --- a/source/main/gui/panels/GUI_LoginBox.h +++ b/source/main/gui/panels/GUI_LoginBox.h @@ -37,8 +37,10 @@ namespace GUI { struct UserProfile { + int user_id; std::string username; std::string email; + std::string avatar_url; Ogre::TexturePtr avatar; }; @@ -48,17 +50,9 @@ struct UserAuthToken std::string refresh_token; }; -const char* const ROUTE_LOGIN = "/login"; -const char* const ROUTE_LOGOUT = "/logout"; -const char* const ROUTE_REFRESH = "/refresh"; - -class LoginBox: - public Ogre::WorkQueue::RequestHandler, - public Ogre::WorkQueue::ResponseHandler +class LoginBox { public: - const Ogre::uint16 WORKQUEUE_ROR_USERPROFILE_AVATAR = 1; - LoginBox(); ~LoginBox(); @@ -66,20 +60,24 @@ class LoginBox: bool IsVisible() const { return m_is_visible; } void ShowError(std::string const& msg); - /// - /// - /// void ConfirmTfa(); void TriggerTfa(); void NeedsTfa(std::vector tfa_providers); void TfaTriggered(); void Login(); + void Logout(); void Draw(); - void UpdateUserProfile(); + void FetchUserProfile(); + void FetchUserProfileAvatar(); + void UpdateUserProfile(UserProfile* data); void UpdateUserAuth(UserAuthToken* data); + void UpdateUserProfileAvatar(std::string file); void ValidateOrRefreshToken(); + UserProfile GetUserProfile() { return m_user_profile; } + int GetUserAuthStatus() const { return m_logged_in; } + private: bool m_is_visible = false; Str<1000> m_login; @@ -96,7 +94,6 @@ class LoginBox: bool m_logged_in = false; //< Local copy UserAuthToken m_auth_tokens; //< Local copy UserProfile m_user_profile; //< Local copy - Ogre::uint16 m_ogre_workqueue_channel = 0; }; } diff --git a/source/main/main.cpp b/source/main/main.cpp index 1964ce2355..1866a9f43e 100644 --- a/source/main/main.cpp +++ b/source/main/main.cpp @@ -703,10 +703,11 @@ int main(int argc, char *argv[]) { GUI::UserAuthToken* data = static_cast(m.payload); App::remote_user_auth_state->setVal((int)RoR::UserAuthState::AUTHENTICATED); - // set the struct... App::GetGuiManager()->LoginBox.UpdateUserAuth(data); App::GetGuiManager()->LoginBox.SetVisible(false); + App::GetGameContext()->PushMessage(Message(MSG_NET_USERPROFILE_REQUESTED)); App::GetGameContext()->PushMessage(Message(MSG_GUI_OPEN_MENU_REQUESTED)); + delete data; break; } @@ -717,6 +718,14 @@ int main(int argc, char *argv[]) break; } + case MSG_NET_USERAUTH_LOGOUT_REQUESTED: + { + App::remote_login_token->setStr(""); + App::remote_refresh_token->setStr(""); + App::remote_user_auth_state->setVal((int)RoR::UserAuthState::UNAUTHENTICATED); + break; + } + case MSG_NET_USERAUTH_RV_REQUESTED: { App::remote_user_auth_state->setVal((int)RoR::UserAuthState::EXPIRED); @@ -729,6 +738,8 @@ int main(int argc, char *argv[]) GUI::UserAuthToken* data = static_cast(m.payload); App::remote_user_auth_state->setVal((int)RoR::UserAuthState::AUTHENTICATED); App::GetGuiManager()->LoginBox.UpdateUserAuth(data); + App::GetGameContext()->PushMessage(Message(MSG_NET_USERPROFILE_REQUESTED)); + delete data; break; } @@ -736,6 +747,7 @@ int main(int argc, char *argv[]) { std::vector* tfa_providers_ptr = reinterpret_cast*>(m.payload); App::GetGuiManager()->LoginBox.NeedsTfa(*tfa_providers_ptr); + delete tfa_providers_ptr; break; } @@ -751,6 +763,33 @@ int main(int argc, char *argv[]) break; } + case MSG_NET_USERPROFILE_REQUESTED: + { + App::GetGuiManager()->LoginBox.FetchUserProfile(); + break; + } + + case MSG_NET_USERPROFILE_FINISHED: + { + GUI::UserProfile* data = static_cast(m.payload); + App::GetGuiManager()->LoginBox.UpdateUserProfile(data); + App::GetGameContext()->PushMessage(Message(MSG_NET_USERPROFILE_AVATAR_REQUESTED)); + delete data; + break; + } + + case MSG_NET_USERPROFILE_AVATAR_REQUESTED: + { + App::GetGuiManager()->LoginBox.FetchUserProfileAvatar(); + break; + } + + case MSG_NET_USERPROFILE_AVATAR_FINISHED: + { + App::GetGuiManager()->LoginBox.UpdateUserProfileAvatar(m.description); + break; + } + case MSG_NET_REFRESH_SERVERLIST_SUCCESS: { GUI::MpServerInfoVec* data = static_cast(m.payload);