From 830c97c5219207db0254076190f84f493641a7cf Mon Sep 17 00:00:00 2001 From: Ralph Sennhauser Date: Sun, 4 Jan 2026 12:36:28 +0100 Subject: [PATCH] Use XMPP Roster for BuddyList This allows the same user to keep the buddy list across clients and config resets. At the same time this allows for per lobby buddy lists which is needed as the same nickname may refer to different people. Use lower case comparison for buddy names. This is in line with the idea of local part of jid should be seen as normalized nick and is used server-side to compare user names. The migration is kept simple. I.e. only attempted if the roster is empty and the config indicates no migration was done before. Fixes: #6824 Signed-off-by: Ralph Sennhauser --- binaries/data/config/default.cfg | 2 +- .../mods/public/gui/common/gamedescription.js | 14 +- .../PlayerAssignmentsController.js | 3 +- .../lobby/LobbyPage/Buttons/BuddyButton.js | 42 ++-- .../mods/public/gui/lobby/LobbyPage/Game.js | 2 +- .../public/gui/lobby/LobbyPage/PlayerList.js | 2 +- .../mods/public/gui/lobby/XmppMessages.js | 3 + .../prelobby/common/terms/Privacy_Policy.txt | 6 +- source/lib/external_libraries/gloox.h | 4 +- source/lobby/XmppClient.cpp | 191 +++++++++++++++++- source/lobby/XmppClient.h | 36 ++++ source/lobby/scripting/JSInterface_Lobby.cpp | 20 ++ source/network/NetServer.cpp | 22 +- source/network/NetServer.h | 14 +- .../network/scripting/JSInterface_Network.cpp | 15 +- source/ps/ConfigDB.cpp | 2 +- 16 files changed, 313 insertions(+), 65 deletions(-) diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg index 79ec2db76e..3879754d35 100644 --- a/binaries/data/config/default.cfg +++ b/binaries/data/config/default.cfg @@ -566,7 +566,7 @@ terms_of_service = "0" ; Version (hash) of the Terms of Service tha privacy_policy = "0" ; Version (hash) of the Privacy Policy that the user has accepted xpartamupp = "wfgbot29" ; Name of the server-side XMPP-account that manage games echelon = "echelon29" ; Name of the server-side XMPP-account that manages ratings -buddies = "," ; Comma separated list of playernames that the current user has marked as buddies +buddies.migrated = false ; Whether a former local buddy list was already migrated to the server roster rememberpassword = true ; Whether to store the encrypted password in the user config [lobby.columns] diff --git a/binaries/data/mods/public/gui/common/gamedescription.js b/binaries/data/mods/public/gui/common/gamedescription.js index fbc891d714..306e64f6f0 100644 --- a/binaries/data/mods/public/gui/common/gamedescription.js +++ b/binaries/data/mods/public/gui/common/gamedescription.js @@ -8,13 +8,6 @@ var g_DescriptionHighlight = "orange"; */ var g_DefaultLobbyRating = 1200; -/** - * XEP-0172 doesn't restrict nicknames, but our lobby policy does. - * So use this human readable delimiter to separate buddy names in the config file. - */ -var g_BuddyListDelimiter = ","; - - /** * Returns the nickname without the lobby rating. */ @@ -24,11 +17,6 @@ function splitRatingFromNick(playerName) return { "nick": result ? result[1] : playerName, "rating": result ? +result[2] : "" }; } -/** - * Array of playernames that the current user has marked as buddies. - */ -var g_Buddies = Engine.ConfigDB_GetValue("user", "lobby.buddies").split(g_BuddyListDelimiter); - /** * Denotes which players are a lobby buddy of the current user. */ @@ -119,7 +107,7 @@ function formatPlayerInfo(playerDataArray, playerStates) playerDescriptions[teamIdx].push(sprintf(playerDescription, { "playerName": coloredText( - (g_Buddies.indexOf(playerNick) != -1 ? g_BuddySymbol + " " : "") + + (Engine.IsLobbyBuddy(playerNick) ? g_BuddySymbol + " " : "") + escapeText(playerData.Name), (typeof getPlayerColor === 'function' ? (isAI ? "white" : getPlayerColor(playerNick)) : diff --git a/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js b/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js index e0116fc803..b7b345900b 100644 --- a/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js +++ b/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js @@ -103,7 +103,8 @@ class PlayerAssignmentsController { const assignOption = Engine.ConfigDB_GetValue("user", this.ConfigAssignPlayers); if (assignOption == "disabled" || - assignOption == "buddies" && g_Buddies.indexOf(splitRatingFromNick(newAssignments[newGUID].name).nick) == -1) + assignOption == "buddies" && + !Engine.IsLobbyBuddy(splitRatingFromNick(newAssignments[newGUID].name).nick)) return; } diff --git a/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/BuddyButton.js b/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/BuddyButton.js index ae6707a9af..cfbb2ed385 100644 --- a/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/BuddyButton.js +++ b/binaries/data/mods/public/gui/lobby/LobbyPage/Buttons/BuddyButton.js @@ -11,9 +11,10 @@ class BuddyButton this.toggleBuddyButton = Engine.GetGUIObjectByName("toggleBuddyButton"); this.toggleBuddyButton.onPress = this.onPress.bind(this); - const rebuild = this.rebuild.bind(this); - xmppMessages.registerXmppMessageHandler("system", "connected", rebuild); - xmppMessages.registerXmppMessageHandler("system", "disconnected", rebuild); + const rebuildAndNotify = this.rebuildAndNotify.bind(this); + xmppMessages.registerXmppMessageHandler("system", "connected", rebuildAndNotify); + xmppMessages.registerXmppMessageHandler("system", "disconnected", rebuildAndNotify); + xmppMessages.registerXmppMessageHandler("roster", "change", rebuildAndNotify); this.rebuild(); } @@ -31,38 +32,33 @@ class BuddyButton rebuild() { - this.toggleBuddyButton.caption = - g_Buddies.indexOf(this.playerName) != -1 ? - this.UnmarkString : - this.MarkString; + const isBuddy = !!this.playerName && Engine.IsLobbyBuddy(this.playerName); + + this.toggleBuddyButton.caption = isBuddy ? this.UnmarkString : this.MarkString; this.toggleBuddyButton.enabled = Engine.IsXmppClientConnected() && !!this.playerName && this.playerName != g_Nickname; } + rebuildAndNotify() + { + this.rebuild(); + + for (const handler of this.buddyChangedHandlers) + handler(); + } + /** * Toggle the buddy state of the selected player. */ onPress() { - if (!this.playerName || this.playerName == g_Nickname || this.playerName.indexOf(g_BuddyListDelimiter) != -1) + if (!this.playerName || this.playerName == g_Nickname) return; - const index = g_Buddies.indexOf(this.playerName); - if (index != -1) - g_Buddies.splice(index, 1); + if (Engine.IsLobbyBuddy(this.playerName)) + Engine.LobbyRemoveBuddy(this.playerName); else - g_Buddies.push(this.playerName); - - Engine.ConfigDB_CreateAndSaveValue( - "user", - "lobby.buddies", - g_Buddies.filter(nick => nick).join(g_BuddyListDelimiter) || g_BuddyListDelimiter - ); - - this.rebuild(); - - for (const handler of this.buddyChangedHandlers) - handler(); + Engine.LobbyAddBuddy(this.playerName); } } diff --git a/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js b/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js index 8105bbbfaf..6f3d15a436 100644 --- a/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js +++ b/binaries/data/mods/public/gui/lobby/LobbyPage/Game.js @@ -187,7 +187,7 @@ class Game playerRatingTotal += playerNickRating.rating || g_DefaultLobbyRating; // Sort games with playing buddies above games with spectating buddies - if (hasBuddies < 2 && g_Buddies.indexOf(playerNickRating.nick) != -1) + if (hasBuddies < 2 && Engine.IsLobbyBuddy(playerNickRating.nick)) hasBuddies = player.Team == "observer" ? 1 : 2; } diff --git a/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js b/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js index 755a0a4a1c..bfce1527ca 100644 --- a/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js +++ b/binaries/data/mods/public/gui/lobby/LobbyPage/PlayerList.js @@ -96,7 +96,7 @@ class PlayerList parsePlayer(sortKey, player) { - player.isBuddy = g_Buddies.indexOf(player.name) != -1; + player.isBuddy = Engine.IsLobbyBuddy(player.name); switch (sortKey) { diff --git a/binaries/data/mods/public/gui/lobby/XmppMessages.js b/binaries/data/mods/public/gui/lobby/XmppMessages.js index 55f0db383f..dd896b217f 100644 --- a/binaries/data/mods/public/gui/lobby/XmppMessages.js +++ b/binaries/data/mods/public/gui/lobby/XmppMessages.js @@ -112,5 +112,8 @@ XmppMessages.prototype.MessageTypes = { "profile", "leaderboard", "ratinglist" + ], + "roster": [ + "change" ] }; diff --git a/binaries/data/mods/public/gui/prelobby/common/terms/Privacy_Policy.txt b/binaries/data/mods/public/gui/prelobby/common/terms/Privacy_Policy.txt index 3b572a87fb..29a037f58d 100644 --- a/binaries/data/mods/public/gui/prelobby/common/terms/Privacy_Policy.txt +++ b/binaries/data/mods/public/gui/prelobby/common/terms/Privacy_Policy.txt @@ -1,6 +1,6 @@ [font="sans-bold-18"]0 A.D. Empires Ascendant Multiplayer Lobby Privacy Policy[/font] -[font="sans-bold-14"]Document Date:[/font] 2025-04-10 +[font="sans-bold-14"]Document Date:[/font] 2026-10-07 [font="sans-bold-16"]Personal data processed by Wildfire Games and purposes of processing:[/font] [font="sans-bold-14"]1. Player name[/font] @@ -38,6 +38,10 @@ Wildfire Games stores match statistics to: 1. Verify the accuracy of the rating score and identify and enforce Terms of Service violations relating to that. 2. Compute and publish community statistics to improve the game and the service, for example to improve the balancing of the game or to make certain aspects of the game more appealing. +[font="sans-bold-14"]7. Buddies[/font] +Players can mark other players as buddies to distinguish them from all other players. +Wildfire Games stores each player's buddy list to ensure that it is available across different computers a player might be using. + [font="sans-bold-14"]Security of processing:[/font] The transmission of personal data is secured using TLS encryption (GDPR 32). Personal data is protected against unintentional loss in encrypted backups for additional time (GDPR 30.1.g, GDPR 32). diff --git a/source/lib/external_libraries/gloox.h b/source/lib/external_libraries/gloox.h index f72003d055..f1379f9574 100644 --- a/source/lib/external_libraries/gloox.h +++ b/source/lib/external_libraries/gloox.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2025 Wildfire Games. +/* Copyright (C) 2026 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -62,6 +62,8 @@ #include // IWYU pragma: export #include // IWYU pragma: export #include // IWYU pragma: export +#include // IWYU pragma: export +#include // IWYU pragma: export #include // IWYU pragma: export #include // IWYU pragma: export #include // IWYU pragma: export diff --git a/source/lobby/XmppClient.cpp b/source/lobby/XmppClient.cpp index e05b5ab8eb..a26d107351 100644 --- a/source/lobby/XmppClient.cpp +++ b/source/lobby/XmppClient.cpp @@ -39,6 +39,7 @@ #include "scriptinterface/Request.h" #include "scriptinterface/StructuredClone.h" +#include #include #include #include @@ -51,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +62,8 @@ namespace { +constexpr const char* ROSTER_GROUP_NAME_BUDDIES{"buddies"}; + //debug #if 1 #define DbgXMPP(x) @@ -129,7 +133,7 @@ std::optional CreateMucRoom(gloox::MUCRoomHandler* handler, cons class XmppClient::Impl : public gloox::ConnectionListener, public gloox::MUCRoomHandler, public gloox::IqHandler, public gloox::RegistrationHandler, public gloox::MessageHandler, - public gloox::Jingle::SessionHandler, public gloox::LogHandler + public gloox::Jingle::SessionHandler, public gloox::LogHandler, public gloox::RosterListener { public: // Basic @@ -161,6 +165,7 @@ public: gloox::CertStatus m_certStatus; bool m_initialLoadComplete; bool m_regOpt; + std::set m_Buddies; private: /* Xmpp handlers */ @@ -216,6 +221,37 @@ private: void handleSessionInitiation(gloox::Jingle::Session* session, const gloox::Jingle::Session::Jingle* jingle); + /** + * Rebuild m_Buddies from the roster. + */ + void UpdateBuddies(); + + /** + * Move a pre-roster buddy list from the config into the roster, then drop + * it from the config. Does nothing once that migration has happened. + * + * Must only be called once the roster has been received, since an empty + * roster cannot be distinguished from one without buddies. + */ + void MigrateBuddyList(); + + /* Roster */ + void handleItemAdded(const gloox::JID& jid) override; + void handleItemSubscribed(const gloox::JID& /*jid*/) override {} + void handleItemRemoved(const gloox::JID& jid) override; + void handleItemUpdated(const gloox::JID& jid) override; + void handleItemUnsubscribed(const gloox::JID& /*jid*/) override {} + void handleRoster(const gloox::Roster& /*roster*/) override; + void handleRosterPresence(const gloox::RosterItem& /*item*/, const std::string& /*resource*/, gloox::Presence::PresenceType /*presence*/, const std::string& /*msg*/) override {} + void handleSelfPresence(const gloox::RosterItem& /*item*/, const std::string& /*resource*/, gloox::Presence::PresenceType /*presence*/, const std::string& /*msg*/) override {} + bool handleSubscriptionRequest(const gloox::JID& /*jid*/, const std::string& /*msg*/) override { return false; } + bool handleUnsubscriptionRequest(const gloox::JID& /*jid*/, const std::string& /*msg*/) override { return false; } + void handleNonrosterPresence(const gloox::Presence& /*presence*/) override {} + void handleRosterError(const gloox::IQ& iq) override; +#if GLOOXVERSION >= 0x010100 + void handleRosterItemExchange(const gloox::JID& /*from*/, const gloox::RosterX* /*items*/) override {} +#endif + template void CreateGUIMessage( const std::string& type, @@ -223,14 +259,16 @@ private: const std::time_t time, Args const&... args); +public: struct SPlayer { SPlayer(const gloox::Presence::PresenceType presence, const gloox::MUCRoomRole role, - const std::string& rating) - : m_Presence(presence), m_Role(role), m_Rating(rating) + const std::string& bareJid, const std::string& rating) + : m_Presence(presence), m_Role(role), m_BareJid(bareJid), m_Rating(rating) { } gloox::Presence::PresenceType m_Presence; gloox::MUCRoomRole m_Role; + std::string m_BareJid; std::string m_Rating; }; using PlayerMap = std::map; @@ -335,6 +373,8 @@ XmppClient::Impl::Impl(const Script::Interface* scriptInterface, const std::stri m_client.registerMessageHandler(this); + m_client.rosterManager()->registerRosterListener(this, false); + m_registration.registerRegistrationHandler(this); // Uncomment to see the raw stanzas @@ -428,6 +468,7 @@ void XmppClient::Impl::onDisconnect(gloox::ConnectionError error) m_PlayerMap.clear(); m_PlayerMapUpdate = true; m_HistoricGuiMessages.clear(); + m_Buddies.clear(); m_initialLoadComplete = false; CreateGUIMessage( @@ -1190,7 +1231,7 @@ void XmppClient::Impl::handleMUCParticipantPresence(gloox::MUCRoom*, const gloox m_PlayerMap.emplace( std::piecewise_construct, std::forward_as_tuple(participant.newNick), - std::forward_as_tuple(presence.presence(), participant.role, std::move(m_PlayerMap.at(nick).m_Rating))); + std::forward_as_tuple(presence.presence(), participant.role, participant.jid ? participant.jid->bare() : "", std::move(m_PlayerMap.at(nick).m_Rating))); else LOGERROR("Nickname changed to an existing nick!"); @@ -1282,12 +1323,16 @@ void XmppClient::Impl::handleMUCParticipantPresence(gloox::MUCRoom*, const gloox m_PlayerMap.emplace( std::piecewise_construct, std::forward_as_tuple(nick), - std::forward_as_tuple(presence.presence(), participant.role, std::string())); + std::forward_as_tuple(presence.presence(), participant.role, + participant.jid ? participant.jid->bare() : "", + std::string())); } else { it->second.m_Presence = presence.presence(); it->second.m_Role = participant.role; + if (participant.jid) + it->second.m_BareJid = participant.jid->bare(); } } @@ -1511,3 +1556,139 @@ void XmppClient::Impl::handleSessionInitiation(gloox::Jingle::Session*, g_NetServer->SendHolePunchingMessage(candidate.ip, candidate.port); } + +// ---------------------------------------------------------------------------- +// +// Roster +// +// ---------------------------------------------------------------------------- + +namespace { +// TODO: Helper only needed till split view is supported by all compilers. +std::vector splitBuddyList(std::string buddyList) +{ + std::vector buddies{}; + size_t posStart{0}; + size_t posEnd{0}; + while ((posEnd = buddyList.find(",", posStart)) != std::string::npos) + { + buddies.push_back(buddyList.substr(posStart, posEnd - posStart)); + posStart = posEnd + 1; + } + buddies.push_back(buddyList.substr(posStart)); + return buddies; +} + +/** + * Whether the given roster item is one of the buddies tracked in the buddy list. + */ +bool IsBuddyRosterItem(const gloox::RosterItem& item) +{ + const gloox::StringList groups = item.groups(); + return !item.name().empty() && + std::find(groups.begin(), groups.end(), ROSTER_GROUP_NAME_BUDDIES) != groups.end(); +} +} // namespace + +void XmppClient::AddBuddy(const std::string& nick) +{ + DbgXMPP("Add buddy: " + nick); + const auto it = m_Impl->m_PlayerMap.find(nick); + + if (it == m_Impl->m_PlayerMap.end()) + LOGWARNING("XmppClient: Can't add unknown player to roster: {}", nick); + else if (it->second.m_BareJid.empty()) + LOGWARNING("XmppClient: Anonymous player can't be added to roster: {}", nick); + else + m_Impl->m_client.rosterManager()->add(it->second.m_BareJid, nick, gloox::StringList{ROSTER_GROUP_NAME_BUDDIES}); +} + +void XmppClient::RemoveBuddy(const std::string& nick) +{ + DbgXMPP("Remove buddy: " + nick); + const auto it = m_Impl->m_PlayerMap.find(nick); + + if (it == m_Impl->m_PlayerMap.end()) + LOGWARNING("XmppClient: Can't remove unknown player from roster: {}", nick); + else if (it->second.m_BareJid.empty()) + LOGWARNING("XmppClient: Anonymous player can't be removed from roster: {}", nick); + else + m_Impl->m_client.rosterManager()->remove(it->second.m_BareJid); +} + +bool XmppClient::IsBuddy(const std::string& nick) const +{ + return m_Impl->m_Buddies.find(CStr(nick).LowerCase()) != m_Impl->m_Buddies.end(); +} + +std::set XmppClient::GetBuddies() const +{ + return m_Impl->m_Buddies; +} + +void XmppClient::Impl::UpdateBuddies() +{ + m_Buddies.clear(); + for (const auto& [jid, item] : *m_client.rosterManager()->roster()) + { + if (IsBuddyRosterItem(*item)) + m_Buddies.insert(CStr(item->name()).LowerCase()); + } +} + +void XmppClient::Impl::MigrateBuddyList() +{ + if (g_ConfigDB.Get("lobby.buddies.migrated", false)) + return; + + if (m_client.rosterManager()->roster()->empty()) + { + const std::string buddyList{g_ConfigDB.Get("lobby.buddies", std::string{})}; + for (const std::string& nick : splitBuddyList(buddyList)) + { + if (nick.empty()) + continue; + + m_client.rosterManager()->add(nick + "@" + m_server, nick, gloox::StringList{ROSTER_GROUP_NAME_BUDDIES}); + } + } + + g_ConfigDB.RemoveValue(CFG_USER, "lobby.buddies"); + g_ConfigDB.SetValueString(CFG_USER, "lobby.buddies.migrated", "true"); + g_ConfigDB.WriteFile(CFG_USER); +} + +void XmppClient::Impl::handleRoster(const gloox::Roster& /*roster*/) +{ + MigrateBuddyList(); + UpdateBuddies(); + CreateGUIMessage("roster", "change", std::time(nullptr)); +} + +void XmppClient::Impl::handleItemAdded(const gloox::JID& /*jid*/) +{ + UpdateBuddies(); + CreateGUIMessage("roster", "change", std::time(nullptr)); +} + +void XmppClient::Impl::handleItemRemoved(const gloox::JID& jid) +{ + // gloox (1.0.28) calles this before before updating the roster which means + // calling UpdateBuddies here would still leave the removed buddy in + // m_Buddies; drop just that entry instead. + if (const gloox::RosterItem* item = m_client.rosterManager()->getRosterItem(jid); item && IsBuddyRosterItem(*item)) + m_Buddies.erase(CStr(item->name()).LowerCase()); + + CreateGUIMessage("roster", "change", std::time(nullptr)); +} + +void XmppClient::Impl::handleItemUpdated(const gloox::JID& /*jid*/) +{ + UpdateBuddies(); + CreateGUIMessage("roster", "change", std::time(nullptr)); +} + +void XmppClient::Impl::handleRosterError(const gloox::IQ& iq) +{ + LOGERROR("{}", iq.tag()->xml()); +} diff --git a/source/lobby/XmppClient.h b/source/lobby/XmppClient.h index 42c1bc29be..2a84412820 100644 --- a/source/lobby/XmppClient.h +++ b/source/lobby/XmppClient.h @@ -20,6 +20,8 @@ #include #include +#include +#include namespace Script { class Interface; } namespace Script { class Request; } @@ -69,6 +71,40 @@ public: void SendMUCMessage(const std::string& message); void SendStunEndpointToHost(const std::string& ip, std::uint16_t port, const std::string& hostJID); + /* Roster */ + + /** + * Add the given nickname to the buddies of the current user. + * + * @param nick The nickname of a player in the lobby. + */ + void AddBuddy(const std::string& nick); + + /** + * Remove the given nickname from the buddies of the current user. + * + * @param nick The nickname of a player in the lobby. + */ + void RemoveBuddy(const std::string& nick); + + /** + * Whether the given nickname is a buddy of the current user. + * + * The buddies are stored in the XMPP roster of the connected lobby. Since + * a nickname only identifies a user within one lobby, this is meaningless + * without a connection and always returns false in that case. + * + * @param nick The nickname to look up, compared case-insensitively + */ + bool IsBuddy(const std::string& nick) const; + + /** + * Get the nicknames of all buddies of the current user. + * + * @returns The (lowercase) nicknames of the buddies in the lobby's roster + */ + std::set GetBuddies() const; + private: class Impl; const std::unique_ptr m_Impl; diff --git a/source/lobby/scripting/JSInterface_Lobby.cpp b/source/lobby/scripting/JSInterface_Lobby.cpp index 714613ee9e..ffb104c9fb 100644 --- a/source/lobby/scripting/JSInterface_Lobby.cpp +++ b/source/lobby/scripting/JSInterface_Lobby.cpp @@ -51,6 +51,21 @@ bool HasXmppClient() return g_XmppClient; } +/** + * Whether the given nickname is a buddy of the current user. + * + * @param nick The nickname to look up, compared case-insensitively + */ +static bool IsLobbyBuddy([[maybe_unused]] const std::string& nick) +{ +#if CONFIG2_LOBBY + return g_XmppClient && g_XmppClient->IsBuddy(nick); +#else + // Without the lobby there is no roster to look the nickname up in. + return false; +#endif +} + void SetRankedGame(bool isRanked) { g_rankedGame = isRanked; @@ -190,6 +205,7 @@ void RegisterScriptFunctions(const Script::Request& rq) { // Lobby functions Script::Function::Register<&HasXmppClient>(rq, "HasXmppClient"); + Script::Function::Register<&IsLobbyBuddy>(rq, "IsLobbyBuddy"); Script::Function::Register<&SetRankedGame>(rq, "SetRankedGame"); #if CONFIG2_LOBBY // Allow the lobby to be disabled Script::Function::Register<&StartXmppClient>(rq, "StartXmppClient"); @@ -229,6 +245,10 @@ void RegisterScriptFunctions(const Script::Request& rq) REGISTER_XMPP(GetRole, "LobbyGetPlayerRole"); REGISTER_XMPP(GetRating, "LobbyGetPlayerRating"); REGISTER_XMPP(GetSubject, "LobbyGetRoomSubject"); + + // Roster + REGISTER_XMPP(AddBuddy, "LobbyAddBuddy"); + REGISTER_XMPP(RemoveBuddy, "LobbyRemoveBuddy"); #undef REGISTER_XMPP Script::Function::Register<&EncryptPassword>(rq, "EncryptPassword"); diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index a4033ede4f..22c6e761d3 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -118,9 +118,10 @@ static CStr DebugName(CNetServerSession* session) CNetServerWorker::CNetServerWorker(const bool continueSavedGame, std::uint16_t port, const bool useLobbyAuth, std::string password, std::string controllerSecret, - std::string initAttributes) : + std::string initAttributes, std::set buddies) : m_ContinuesSavedGame{continueSavedGame}, m_LobbyAuth{useLobbyAuth}, + m_Buddies{std::move(buddies)}, m_ControllerSecret{std::move(controllerSecret)}, m_Password{std::move(password)} { @@ -1078,17 +1079,9 @@ bool CNetServerWorker::OnAuthenticate(CNetServerSession* session, CFsmEvent 0) + isRejoining = true; } } @@ -1695,9 +1688,10 @@ void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, std::uint16_t CNetServer::CNetServer(const bool continueSavedGame, std::uint16_t port, const bool useLobbyAuth, - std::string password, std::string controllerSecret, std::string initAttributes) : + std::string password, std::string controllerSecret, std::string initAttributes, + std::set buddies) : m_Worker{continueSavedGame, port, useLobbyAuth, password, std::move(controllerSecret), - std::move(initAttributes)}, + std::move(initAttributes), std::move(buddies)}, m_LobbyAuth{useLobbyAuth}, m_Password{std::move(password)} { diff --git a/source/network/NetServer.h b/source/network/NetServer.h index 776d071916..90ab25c5b8 100644 --- a/source/network/NetServer.h +++ b/source/network/NetServer.h @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -135,7 +136,8 @@ private: friend class CNetServer; CNetServerWorker(const bool continuesSavedGame, std::uint16_t port, const bool useLobbyAuth, - std::string password, std::string controllerSecret, std::string initAttributes); + std::string password, std::string controllerSecret, std::string initAttributes, + std::set buddies); ~CNetServerWorker(); bool CheckPassword(const std::string& password, const std::string& salt) const; @@ -262,6 +264,13 @@ private: */ const bool m_LobbyAuth; + /** + * The nicknames (lowercase) of the host's buddies at the time the game was + * hosted, used to decide who may join after the game has started + * (lateobserver authentication). + */ + const std::set m_Buddies; + std::unique_ptr m_Host; std::vector> m_Sessions; @@ -356,7 +365,8 @@ class CNetServer NONCOPYABLE(CNetServer); public: CNetServer(const bool isSavedGame, std::uint16_t port, const bool useLobbyAuth = false, - std::string password = {}, std::string controllerSecret = {}, std::string initAttributes = {}); + std::string password = {}, std::string controllerSecret = {}, std::string initAttributes = {}, + std::set buddies = {}); /** * Call from the GUI to asynchronously notify all clients that they should start loading the game. diff --git a/source/network/scripting/JSInterface_Network.cpp b/source/network/scripting/JSInterface_Network.cpp index 4ff5f0d3d4..d6e88595aa 100644 --- a/source/network/scripting/JSInterface_Network.cpp +++ b/source/network/scripting/JSInterface_Network.cpp @@ -20,6 +20,7 @@ #include "JSInterface_Network.h" #include "lib/code_generation.h" +#include "lib/config2.h" #include "lib/debug.h" #include "lib/utf8.h" #include "lobby/XmppClient.h" @@ -45,6 +46,7 @@ #include #include #include +#include #include #include #include @@ -109,7 +111,18 @@ void StartNetworkHost(const CStrW& playerName, const std::uint16_t serverPort, c HashCryptographically(password, hostJID + password + PS_SERIALIZATION_VERSION) : ""; const std::string secret = ps_generate_guid(); - g_NetServer = new CNetServer(continueSavedGame, serverPort, hasLobby, hashedPassword, secret); + + // Snapshot the host's buddies from the roster of the lobby they are + // connected to. Hosting without a lobby means there is no roster, so + // nobody is a buddy. + std::set buddies; +#if CONFIG2_LOBBY + if (hasLobby) + buddies = g_XmppClient->GetBuddies(); +#endif + + g_NetServer = new CNetServer(continueSavedGame, serverPort, hasLobby, hashedPassword, secret, + {}, buddies); // Generate a secret to identify the host client. diff --git a/source/ps/ConfigDB.cpp b/source/ps/ConfigDB.cpp index 777361cb4e..62878420ce 100644 --- a/source/ps/ConfigDB.cpp +++ b/source/ps/ConfigDB.cpp @@ -55,7 +55,7 @@ void TriggerAllHooks(const std::multimap>& hooks, co // These entries will not be printed to logfiles, so that logfiles can be shared without leaking personal or sensitive data const std::unordered_set g_UnloggedEntries = { "lobby.password", - "lobby.buddies", + "lobby.buddies", // The buddy list of older versions, now stored in the server roster and removed on migration. "userreport.id" // authentication token for GDPR personal data requests };