Index: ps/trunk/binaries/data/mods/public/gui/lobby/lobby.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/lobby/lobby.js (revision 20039) +++ ps/trunk/binaries/data/mods/public/gui/lobby/lobby.js (revision 20040) @@ -1,1430 +1,1456 @@ /** * Used for the gamelist-filtering. */ const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes); /** * Used for the gamelist-filtering. */ const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes); /** * A symbol which is prepended to the username of moderators. */ const g_ModeratorPrefix = "@"; /** * Current username. Cannot contain whitespace. */ const g_Username = Engine.LobbyGetNick(); /** * Lobby server address to construct host JID. */ const g_LobbyServer = Engine.ConfigDB_GetValue("user", "lobby.server"); /** * Current games will be listed in these colors. */ const g_GameColors = { "init": "0 219 0", "waiting": "255 127 0", "running": "219 0 0" }; /** * Initial sorting order of the gamelist. */ const g_GameStatusOrder = ["init", "waiting", "running"]; /** * The playerlist will be assembled using these values. */ const g_PlayerStatuses = { "available": { "color": "0 219 0", "status": translate("Online") }, "away": { "color": "229 76 13", "status": translate("Away") }, "playing": { "color": "200 0 0", "status": translate("Busy") }, "offline": { "color": "0 0 0", "status": translate("Offline") }, "unknown": { "color": "178 178 178", "status": translateWithContext("lobby presence", "Unknown") } }; const g_RoleNames = { "moderator": translate("Moderator"), "participant": translate("Player"), "visitor": translate("Muted Player") }; /** * Color for error messages in the chat. */ const g_SystemColor = "150 0 0"; /** * Color for private messages in the chat. */ const g_PrivateMessageColor = "0 150 0"; /** * Used for highlighting the sender of chat messages. */ const g_SenderFont = "sans-bold-13"; /** * Color to highlight chat commands in the explanation. */ const g_ChatCommandColor = "200 200 255"; /** * All chat messages received since init (i.e. after lobby join and after returning from a game). */ var g_ChatMessages = []; /** * Rating of the current user. * Contains the number or an empty string in case the user has no rating. */ var g_UserRating = ""; /** * All games currently running. */ var g_GameList = {}; /** * Used to restore the selection after updating the playerlist. */ var g_SelectedPlayer = ""; /** * Used to restore the selection after updating the gamelist. */ var g_SelectedGameIP = ""; /** * Used to restore the selection after updating the gamelist. */ var g_SelectedGamePort = ""; /** * Whether the current user has been kicked or banned. */ var g_Kicked = false; /** - * Notifications sent by XmppClient.cpp + * Processing of notifications sent by XmppClient.cpp. + * + * @returns true if the playerlist GUI must be updated. */ var g_NetMessageTypes = { "system": { // Three cases are handled in prelobby.js - "registered": msg => { - }, - "connected": msg => { - }, + "registered": msg => false, + "connected": msg => false, "disconnected": msg => { updateGameList(); updateLeaderboard(); - updatePlayerList(); Engine.GetGUIObjectByName("chatInput").hidden = true; for (let button of ["host", "leaderboard", "userprofile", "toggleBuddy"]) Engine.GetGUIObjectByName(button + "Button").enabled = false; Engine.GetGUIObjectByName("chatInput").hidden = true; if (!g_Kicked) addChatMessage({ "from": "system", "time": msg.time, "text": translate("Disconnected.") + " " + msg.text }); + return true; }, "error": msg => { addChatMessage({ "from": "system", "time": msg.time, "text": msg.text }); + return false; } }, "chat": { "subject": msg => { updateSubject(msg.text); + return false; }, "join": msg => { addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has joined."), { "nick": msg.text }), "time": msg.time, "isSpecial": true }); + return true; }, "leave": msg => { addChatMessage({ "text": "/special " + sprintf(translate("%(nick)s has left."), { "nick": msg.text }), "time": msg.time, "isSpecial": true }); if (msg.text == g_Username) Engine.DisconnectXmppClient(); + + return true; }, - "presence": msg => { - }, + "presence": msg => true, "role": msg => { Engine.GetGUIObjectByName("chatInput").hidden = Engine.LobbyGetPlayerRole(g_Username) == "visitor"; let me = g_Username == msg.text; let role = Engine.LobbyGetPlayerRole(msg.text); let txt = role == "visitor" ? me ? translate("You have been muted.") : translate("%(nick)s has been muted.") : role == "moderator" ? me ? translate("You are now a moderator.") : translate("%(nick)s is now a moderator.") : msg.data == "visitor" ? me ? translate("You have been unmuted.") : translate("%(nick)s has been unmuted.") : me ? translate("You are not a moderator anymore.") : translate("%(nick)s is not a moderator anymore."); addChatMessage({ "text": "/special " + sprintf(txt, { "nick": msg.text }), "time": msg.time, "isSpecial": true }); if (g_SelectedPlayer == msg.text) updateUserRoleText(g_SelectedPlayer); + + return false; }, "nick": msg => { addChatMessage({ "text": "/special " + sprintf(translate("%(oldnick)s is now known as %(newnick)s."), { "oldnick": msg.text, "newnick": msg.data }), "time": msg.time, "isSpecial": true }); + return true; }, "kicked": msg => { handleKick(false, msg.text, msg.data || "", msg.time); + return true; }, "banned": msg => { handleKick(true, msg.text, msg.data || "", msg.time); + return true; }, "room-message": msg => { addChatMessage({ "from": escapeText(msg.from), "text": escapeText(msg.text), "time": msg.time }); + return false; }, "private-message": msg => { // Announcements and the Message of the Day are sent by the server directly if (!msg.from) messageBox( 400, 250, msg.text.trim(), translate("Notice") ); // We intend to not support private messages between users if (!msg.from || Engine.LobbyGetPlayerRole(msg.from) == "moderator") // some XMPP clients send trailing whitespace addChatMessage({ "from": escapeText(msg.from || "system"), "text": escapeText(msg.text.trim()), "time": msg.time, "private" : true }); + return false; } }, "game": { - "gamelist": msg => updateGameList(), - "profile": msg => updateProfile(), - "leaderboard": msg => updateLeaderboard(), - "ratinglist": msg => updatePlayerList() + "gamelist": msg => { + updateGameList(); + return false; + }, + "profile": msg => { + updateProfile(); + return false; + }, + "leaderboard": msg => { + updateLeaderboard(); + return false; + }, + "ratinglist": msg => { + return true; + } } }; /** * Commands that can be entered by clients via chat input. * A handler returns true if the user input should be sent as a chat message. */ var g_ChatCommands = { "away": { "description": translate("Set your state to 'Away'."), "handler": args => { Engine.LobbySetPlayerPresence("away"); return false; } }, "back": { "description": translate("Set your state to 'Online'."), "handler": args => { Engine.LobbySetPlayerPresence("available"); return false; } }, "kick": { "description": translate("Kick a specified user from the lobby. Usage: /kick nick reason"), "handler": args => { Engine.LobbyKick(args[0] || "", args[1] || ""); return false; }, "moderatorOnly": true }, "ban": { "description": translate("Ban a specified user from the lobby. Usage: /ban nick reason"), "handler": args => { Engine.LobbyBan(args[0] || "", args[1] || ""); return false; }, "moderatorOnly": true }, "help": { "description": translate("Show this help."), "handler": args => { let isModerator = Engine.LobbyGetPlayerRole(g_Username) == "moderator"; let text = translate("Chat commands:"); for (let command in g_ChatCommands) if (!g_ChatCommands[command].moderatorOnly || isModerator) // Translation: Chat command help format text += "\n" + sprintf(translate("%(command)s - %(description)s"), { "command": '[color="' + g_ChatCommandColor + '"]' + command + '[/color]', "description": g_ChatCommands[command].description }); addChatMessage({ "from": "system", "text": text }); return false; } }, "me": { "description": translate("Send a chat message about yourself. Example: /me goes swimming."), "handler": args => true }, "say": { "description": translate("Send text as a chat message (even if it starts with slash). Example: /say /help is a great command."), "handler": args => true }, "clear": { "description": translate("Clear all chat scrollback."), "handler": args => { clearChatMessages(); return false; } }, "quit": { "description": translate("Return to the main menu."), "handler": args => { returnToMainMenu(); return false; } } }; /** * Called after the XmppConnection succeeded and when returning from a game. * * @param {Object} attribs */ function init(attribs) { if (!g_Settings) { returnToMainMenu(); return; } initMusic(); global.music.setState(global.music.states.MENU); initGameFilters(); Engine.LobbySetPlayerPresence("available"); // When rejoining the lobby after a game, we don't need to process presence changes Engine.LobbyClearPresenceUpdates(); updatePlayerList(); updateSubject(Engine.LobbyGetRoomSubject()); updateLobbyColumns(); updateToggleBuddy(); Engine.GetGUIObjectByName("chatInput").tooltip = colorizeAutocompleteHotkey(); } function updateLobbyColumns() { let gameRating = Engine.ConfigDB_GetValue("user", "lobby.columns.gamerating") == "true"; // Only show the selected columns let gamesBox = Engine.GetGUIObjectByName("gamesBox"); gamesBox.hidden_mapType = gameRating; gamesBox.hidden_gameRating = !gameRating; // Only show the filters of selected columns let mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter"); mapTypeFilter.hidden = gameRating; let gameRatingFilter = Engine.GetGUIObjectByName("gameRatingFilter"); gameRatingFilter.hidden = !gameRating; // Keep filters right above the according column let playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter"); let size = playersNumberFilter.size; size.rleft = gameRating ? 74: 90; size.rright = gameRating ? 84: 100; playersNumberFilter.size = size; } function returnToMainMenu() { Engine.StopXmppClient(); Engine.SwitchGuiPage("page_pregame.xml"); } function initGameFilters() { let mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); mapSizeFilter.list = [translateWithContext("map size", "Any")].concat(g_MapSizes.Name); mapSizeFilter.list_data = [""].concat(g_MapSizes.Tiles); let playersArray = Array(g_MaxPlayers).fill(0).map((v, i) => i + 1); // 1, 2, ... MaxPlayers let playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter"); playersNumberFilter.list = [translateWithContext("player number", "Any")].concat(playersArray); playersNumberFilter.list_data = [""].concat(playersArray); let mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter"); mapTypeFilter.list = [translateWithContext("map", "Any")].concat(g_MapTypes.Title); mapTypeFilter.list_data = [""].concat(g_MapTypes.Name); let gameRatingOptions = ["<1000", "<1100","<1200",">1200",">1300",">1400",">1500"].reverse(); gameRatingOptions = prepareForDropdown(gameRatingOptions.map(r => ({ "value": r, "label": sprintf( r[0] == ">" ? translateWithContext("gamelist filter", "> %(rating)s") : translateWithContext("gamelist filter", "< %(rating)s"), { "rating": r.substr(1) }) }))); let gameRatingFilter = Engine.GetGUIObjectByName("gameRatingFilter"); gameRatingFilter.list = [translateWithContext("map", "Any")].concat(gameRatingOptions.label); gameRatingFilter.list_data = [""].concat(gameRatingOptions.value); resetFilters(); } function resetFilters() { Engine.GetGUIObjectByName("mapSizeFilter").selected = 0; Engine.GetGUIObjectByName("playersNumberFilter").selected = 0; Engine.GetGUIObjectByName("mapTypeFilter").selected = g_MapTypes.Default; Engine.GetGUIObjectByName("gameRatingFilter").selected = 0; Engine.GetGUIObjectByName("filterOpenGames").checked = false; applyFilters(); } function applyFilters() { updateGameList(); updateGameSelection(); } /** * Filter a game based on the status of the filter dropdowns. * * @param {Object} game * @returns {boolean} - True if game should not be displayed. */ function filterGame(game) { let mapSizeFilter = Engine.GetGUIObjectByName("mapSizeFilter"); let playersNumberFilter = Engine.GetGUIObjectByName("playersNumberFilter"); let mapTypeFilter = Engine.GetGUIObjectByName("mapTypeFilter"); let gameRatingFilter = Engine.GetGUIObjectByName("gameRatingFilter"); let filterOpenGames = Engine.GetGUIObjectByName("filterOpenGames"); // We assume index 0 means display all for any given filter. if (mapSizeFilter.selected != 0 && game.mapSize != mapSizeFilter.list_data[mapSizeFilter.selected]) return true; if (playersNumberFilter.selected != 0 && game.maxnbp != playersNumberFilter.list_data[playersNumberFilter.selected]) return true; if (mapTypeFilter.selected != 0 && game.mapType != mapTypeFilter.list_data[mapTypeFilter.selected]) return true; if (filterOpenGames.checked && (game.nbp >= game.maxnbp || game.state != "init")) return true; if (gameRatingFilter.selected > 0) { let selected = gameRatingFilter.list_data[gameRatingFilter.selected]; if (selected.startsWith(">") && +selected.substr(1) >= game.gameRating || selected.startsWith("<") && +selected.substr(1) <= game.gameRating) return true; } return false; } function handleKick(banned, nick, reason, time) { let kickString = nick == g_Username ? banned ? translate("You have been banned from the lobby!") : translate("You have been kicked from the lobby!") : banned ? translate("%(nick)s has been banned from the lobby.") : translate("%(nick)s has been kicked from the lobby."); if (reason) reason = sprintf(translateWithContext("lobby kick", "Reason: %(reason)s"), { "reason": reason }); if (nick != g_Username) { addChatMessage({ "text": "/special " + sprintf(kickString, { "nick": nick }) + " " + reason, "time": time, "isSpecial": true }); return; } addChatMessage({ "from": "system", "time": time, "text": kickString + " " + reason, }); g_Kicked = true; Engine.DisconnectXmppClient(); messageBox( 400, 250, kickString + "\n" + reason, banned ? translate("BANNED") : translate("KICKED") ); } /** * Update the subject GUI object. * * @param {string} newSubject */ function updateSubject(newSubject) { Engine.GetGUIObjectByName("subject").caption = newSubject; // If the subject is only whitespace, hide it and reposition the logo. let subjectBox = Engine.GetGUIObjectByName("subjectBox"); subjectBox.hidden = !newSubject.trim(); let logo = Engine.GetGUIObjectByName("logo"); if (subjectBox.hidden) logo.size = "50%-110 50%-50 50%+110 50%+50"; else logo.size = "50%-110 40 50%+110 140"; } /** * Update the caption of the toggle buddy button. */ function updateToggleBuddy() { let playerList = Engine.GetGUIObjectByName("playersBox"); let playerName = playerList.list[playerList.selected]; let toggleBuddyButton = Engine.GetGUIObjectByName("toggleBuddyButton"); toggleBuddyButton.caption = g_Buddies.indexOf(playerName) != -1 ? translate("Unmark as Buddy") : translate("Mark as Buddy"); toggleBuddyButton.enabled = playerName && playerName != g_Username; } /** * Do a full update of the player listing, including ratings from cached C++ information. */ function updatePlayerList() { let playersBox = Engine.GetGUIObjectByName("playersBox"); let sortBy = playersBox.selected_column || "name"; let sortOrder = playersBox.selected_column_order || 1; let buddyStatusList = []; let playerList = []; let presenceList = []; let nickList = []; let ratingList = []; let cleanPlayerList = Engine.GetPlayerList().map(player => { player.isBuddy = g_Buddies.indexOf(player.name) != -1; return player; }).sort((a, b) => { let sortA, sortB; let statusOrder = Object.keys(g_PlayerStatuses); let statusA = statusOrder.indexOf(a.presence) + a.name.toLowerCase(); let statusB = statusOrder.indexOf(b.presence) + b.name.toLowerCase(); switch (sortBy) { case 'buddy': sortA = (a.isBuddy ? 1 : 2) + statusA; sortB = (b.isBuddy ? 1 : 2) + statusB; break; case 'rating': sortA = +a.rating; sortB = +b.rating; break; case 'status': sortA = statusA; sortB = statusB; break; case 'name': default: sortA = a.name.toLowerCase(); sortB = b.name.toLowerCase(); break; } if (sortA < sortB) return -sortOrder; if (sortA > sortB) return +sortOrder; return 0; }); // Colorize list entries for (let player of cleanPlayerList) { if (player.rating && player.name == g_Username) g_UserRating = player.rating; let rating = player.rating ? (" " + player.rating).substr(-5) : " -"; let presence = g_PlayerStatuses[player.presence] ? player.presence : "unknown"; if (presence == "unknown") warn("Unknown presence:" + player.presence); let statusColor = g_PlayerStatuses[presence].color; let coloredName = colorPlayerName((player.role == "moderator" ? g_ModeratorPrefix : "") + player.name); let coloredPresence = '[color="' + statusColor + '"]' + g_PlayerStatuses[presence].status + "[/color]"; let coloredRating = '[color="' + statusColor + '"]' + rating + "[/color]"; buddyStatusList.push(player.isBuddy ? '[color="' + statusColor + '"]' + g_BuddySymbol + '[/color]' : ""); playerList.push(coloredName); presenceList.push(coloredPresence); ratingList.push(coloredRating); nickList.push(player.name); } playersBox.list_buddy = buddyStatusList; playersBox.list_name = playerList; playersBox.list_status = presenceList; playersBox.list_rating = ratingList; playersBox.list = nickList; playersBox.selected = playersBox.list.indexOf(g_SelectedPlayer); } /** * Toggle buddy state for a player in playerlist within the user config */ function toggleBuddy() { let playerList = Engine.GetGUIObjectByName("playersBox"); let name = playerList.list[playerList.selected]; if (!name || name == g_Username || name.indexOf(g_BuddyListDelimiter) != -1) return; let index = g_Buddies.indexOf(name); if (index != -1) g_Buddies.splice(index, 1); else g_Buddies.push(name); updateToggleBuddy(); // Don't save empty strings to the config file let buddies = g_Buddies.filter(nick => nick).join(g_BuddyListDelimiter) || g_BuddyListDelimiter; Engine.ConfigDB_CreateValue("user", "lobby.buddies", buddies); Engine.ConfigDB_WriteValueToFile("user", "lobby.buddies", buddies, "config/user.cfg"); updatePlayerList(); updateGameList(); } /** * Select the game where the selected player is currently playing, observing or offline. * Selects in that order to account for players that occur in multiple games. */ function selectGameFromPlayername() { if (!g_SelectedPlayer) return; let gameList = Engine.GetGUIObjectByName("gamesBox"); let foundAsObserver = false; for (let i = 0; i < g_GameList.length; ++i) for (let player of stringifiedTeamListToPlayerData(g_GameList[i].players)) { let nick = splitRatingFromNick(player.Name)[0]; if (g_SelectedPlayer != nick) continue; if (player.Team == "observer") { foundAsObserver = true; gameList.selected = i; } else if (!player.Offline) { gameList.selected = i; return; } else if (!foundAsObserver) gameList.selected = i; } } function onPlayerListSelection() { let playerList = Engine.GetGUIObjectByName("playersBox"); if (playerList.selected == playerList.list.indexOf(g_SelectedPlayer)) return; g_SelectedPlayer = playerList.list[playerList.selected]; lookupSelectedUserProfile("playersBox"); updateToggleBuddy(); selectGameFromPlayername(); } function setLeaderboardVisibility(visible) { if (visible) Engine.SendGetBoardList(); lookupSelectedUserProfile(visible ? "leaderboardBox" : "playersBox"); Engine.GetGUIObjectByName("leaderboard").hidden = !visible; Engine.GetGUIObjectByName("fade").hidden = !visible; } function setUserProfileVisibility(visible) { Engine.GetGUIObjectByName("profileFetch").hidden = !visible; Engine.GetGUIObjectByName("fade").hidden = !visible; } /** * Display the profile of the player in the user profile window. */ function lookupUserProfile() { Engine.SendGetProfile(Engine.GetGUIObjectByName("fetchInput").caption); } /** * Display the profile of the selected player in the main window. * Displays N/A for all stats until updateProfile is called when the stats * are actually received from the bot. */ function lookupSelectedUserProfile(guiObjectName) { let playerList = Engine.GetGUIObjectByName(guiObjectName); let playerName = playerList.list[playerList.selected]; Engine.GetGUIObjectByName("profileArea").hidden = !playerName && !Engine.GetGUIObjectByName("usernameText").caption; if (!playerName) return; Engine.SendGetProfile(playerName); Engine.GetGUIObjectByName("usernameText").caption = playerName; Engine.GetGUIObjectByName("rankText").caption = translate("N/A"); Engine.GetGUIObjectByName("highestRatingText").caption = translate("N/A"); Engine.GetGUIObjectByName("totalGamesText").caption = translate("N/A"); Engine.GetGUIObjectByName("winsText").caption = translate("N/A"); Engine.GetGUIObjectByName("lossesText").caption = translate("N/A"); Engine.GetGUIObjectByName("ratioText").caption = translate("N/A"); updateUserRoleText(playerName); } function updateUserRoleText(playerName) { Engine.GetGUIObjectByName("roleText").caption = g_RoleNames[Engine.LobbyGetPlayerRole(playerName) || "participant"]; } /** * Update the profile of the selected player with data from the bot. */ function updateProfile() { let attributes = Engine.GetProfile()[0]; let user = colorPlayerName(attributes.player, attributes.rating); if (!Engine.GetGUIObjectByName("profileFetch").hidden) { let profileFound = attributes.rating != "-2"; Engine.GetGUIObjectByName("profileWindowArea").hidden = !profileFound; Engine.GetGUIObjectByName("profileErrorText").hidden = profileFound; if (!profileFound) { Engine.GetGUIObjectByName("profileErrorText").caption = sprintf( translate("Player \"%(nick)s\" not found."), { "nick": attributes.player } ); return; } Engine.GetGUIObjectByName("profileUsernameText").caption = user; Engine.GetGUIObjectByName("profileRankText").caption = attributes.rank; Engine.GetGUIObjectByName("profileHighestRatingText").caption = attributes.highestRating; Engine.GetGUIObjectByName("profileTotalGamesText").caption = attributes.totalGamesPlayed; Engine.GetGUIObjectByName("profileWinsText").caption = attributes.wins; Engine.GetGUIObjectByName("profileLossesText").caption = attributes.losses; Engine.GetGUIObjectByName("profileRatioText").caption = formatWinRate(attributes); return; } let playerList; if (!Engine.GetGUIObjectByName("leaderboard").hidden) playerList = Engine.GetGUIObjectByName("leaderboardBox"); else playerList = Engine.GetGUIObjectByName("playersBox"); if (attributes.rating == "-2") return; // Make sure the stats we have received coincide with the selected player. if (attributes.player != playerList.list[playerList.selected]) return; Engine.GetGUIObjectByName("usernameText").caption = user; Engine.GetGUIObjectByName("rankText").caption = attributes.rank; Engine.GetGUIObjectByName("highestRatingText").caption = attributes.highestRating; Engine.GetGUIObjectByName("totalGamesText").caption = attributes.totalGamesPlayed; Engine.GetGUIObjectByName("winsText").caption = attributes.wins; Engine.GetGUIObjectByName("lossesText").caption = attributes.losses; Engine.GetGUIObjectByName("ratioText").caption = formatWinRate(attributes); } /** * Update the leaderboard from data cached in C++. */ function updateLeaderboard() { let leaderboard = Engine.GetGUIObjectByName("leaderboardBox"); let boardList = Engine.GetBoardList().sort((a, b) => b.rating - a.rating); let list = []; let list_name = []; let list_rank = []; let list_rating = []; for (let i in boardList) { list_name.push(boardList[i].name); list_rating.push(boardList[i].rating); list_rank.push(+i+1); list.push(boardList[i].name); } leaderboard.list_name = list_name; leaderboard.list_rating = list_rating; leaderboard.list_rank = list_rank; leaderboard.list = list; if (leaderboard.selected >= leaderboard.list.length) leaderboard.selected = -1; } /** * Update the game listing from data cached in C++. */ function updateGameList() { let gamesBox = Engine.GetGUIObjectByName("gamesBox"); let sortBy = gamesBox.selected_column; let sortOrder = gamesBox.selected_column_order; if (gamesBox.selected > -1) { g_SelectedGameIP = g_GameList[gamesBox.selected].ip; g_SelectedGamePort = g_GameList[gamesBox.selected].port; } g_GameList = Engine.GetGameList().map(game => { game.hasBuddies = 0; // Compute average rating of participating players let playerRatings = []; for (let player of stringifiedTeamListToPlayerData(game.players)) { let [nick, rating] = splitRatingFromNick(player.Name); if (player.Team != "observer") playerRatings.push(rating || g_DefaultLobbyRating); // Sort games with playing buddies above games with spectating buddies if (game.hasBuddies < 2 && g_Buddies.indexOf(nick) != -1) game.hasBuddies = player.Team == "observer" ? 1 : 2; } game.gameRating = playerRatings.length ? Math.round(playerRatings.reduce((sum, current) => sum + current) / playerRatings.length) : g_DefaultLobbyRating; return game; }).filter(game => !filterGame(game)).sort((a, b) => { let sortA, sortB; switch (sortBy) { case 'name': sortA = g_GameStatusOrder.indexOf(a.state) + a.name.toLowerCase(); sortB = g_GameStatusOrder.indexOf(b.state) + b.name.toLowerCase(); break; case 'gameRating': case 'mapSize': case 'mapType': sortA = a[sortBy]; sortB = b[sortBy]; break; case 'buddy': sortA = String(b.hasBuddies) + g_GameStatusOrder.indexOf(a.state) + a.name.toLowerCase(); sortB = String(a.hasBuddies) + g_GameStatusOrder.indexOf(b.state) + b.name.toLowerCase(); break; case 'mapName': sortA = translate(a.niceMapName); sortB = translate(b.niceMapName); break; case 'nPlayers': sortA = a.maxnbp; sortB = b.maxnbp; break; } if (sortA < sortB) return -sortOrder; if (sortA > sortB) return +sortOrder; return 0; }); let list_buddy = []; let list_name = []; let list_mapName = []; let list_mapSize = []; let list_mapType = []; let list_nPlayers = []; let list_gameRating = []; let list = []; let list_data = []; let selectedGameIndex = -1; for (let i in g_GameList) { let game = g_GameList[i]; let gameName = escapeText(game.name); let mapTypeIdx = g_MapTypes.Name.indexOf(game.mapType); if (game.ip == g_SelectedGameIP && game.port == g_SelectedGamePort) selectedGameIndex = +i; list_buddy.push(game.hasBuddies ? '[color="' + g_GameColors[game.state] + '"]' + g_BuddySymbol + '[/color]' : ""); list_name.push('[color="' + g_GameColors[game.state] + '"]' + gameName); list_mapName.push(translateMapTitle(game.niceMapName)); list_mapSize.push(translateMapSize(game.mapSize)); list_mapType.push(g_MapTypes.Title[mapTypeIdx] || ""); list_nPlayers.push(game.nbp + "/" + game.maxnbp); list_gameRating.push(game.gameRating); list.push(gameName); list_data.push(i); } gamesBox.list_buddy = list_buddy; gamesBox.list_name = list_name; gamesBox.list_mapName = list_mapName; gamesBox.list_mapSize = list_mapSize; gamesBox.list_mapType = list_mapType; gamesBox.list_nPlayers = list_nPlayers; gamesBox.list_gameRating = list_gameRating; // Change these last, otherwise crash gamesBox.list = list; gamesBox.list_data = list_data; gamesBox.selected = selectedGameIndex; updateGameSelection(); } /** * Populate the game info area with information on the current game selection. */ function updateGameSelection() { let game = selectedGame(); Engine.GetGUIObjectByName("gameInfo").hidden = !game; Engine.GetGUIObjectByName("joinGameButton").hidden = !game; Engine.GetGUIObjectByName("gameInfoEmpty").hidden = game; if (!game) return; Engine.GetGUIObjectByName("sgMapName").caption = translateMapTitle(game.niceMapName); let sgGameStartTime = Engine.GetGUIObjectByName("sgGameStartTime"); let sgNbPlayers = Engine.GetGUIObjectByName("sgNbPlayers"); let sgPlayersNames = Engine.GetGUIObjectByName("sgPlayersNames"); let playersNamesSize = sgPlayersNames.size; playersNamesSize.top = game.startTime ? sgGameStartTime.size.bottom : sgNbPlayers.size.bottom; playersNamesSize.rtop = game.startTime ? sgGameStartTime.size.rbottom : sgNbPlayers.size.rbottom; sgPlayersNames.size = playersNamesSize; sgGameStartTime.hidden = !game.startTime; if (game.startTime) sgGameStartTime.caption = sprintf( // Translation: %(time)s is the hour and minute here. translate("Game started at %(time)s"), { "time": Engine.FormatMillisecondsIntoDateStringLocal(+game.startTime*1000, translate("HH:mm")) }); sgNbPlayers.caption = sprintf( translate("Players: %(current)s/%(total)s"), { "current": game.nbp, "total": game.maxnbp }); sgPlayersNames.caption = formatPlayerInfo(stringifiedTeamListToPlayerData(game.players)); Engine.GetGUIObjectByName("sgMapSize").caption = translateMapSize(game.mapSize); let mapTypeIdx = g_MapTypes.Name.indexOf(game.mapType); Engine.GetGUIObjectByName("sgMapType").caption = g_MapTypes.Title[mapTypeIdx] || ""; let mapData = getMapDescriptionAndPreview(game.mapType, game.mapName); Engine.GetGUIObjectByName("sgMapDescription").caption = mapData.description; setMapPreviewImage("sgMapPreview", mapData.preview); } function selectedGame() { let gamesBox = Engine.GetGUIObjectByName("gamesBox"); if (gamesBox.selected < 0) return undefined; return g_GameList[gamesBox.list_data[gamesBox.selected]]; } /** * Immediately rejoin and join gamesetups. Otherwise confirm late-observer join attempt. */ function joinButton() { let game = selectedGame(); if (!game) return; let rating = getRejoinRating(game); let username = rating ? g_Username + " (" + rating + ")" : g_Username; if (game.state == "init" || stringifiedTeamListToPlayerData(game.players).some(player => player.Name == username)) joinSelectedGame(); else messageBox( 400, 200, translate("The game has already started. Do you want to join as observer?"), translate("Confirmation"), [translate("No"), translate("Yes")], [null, joinSelectedGame] ); } /** * Attempt to join the selected game without asking for confirmation. */ function joinSelectedGame() { let game = selectedGame(); if (!game) return; let ip; let port; if (game.stunIP) { ip = game.stunIP; port = game.stunPort; } else { ip = game.ip; port = game.port; } if (ip.split('.').length != 4) { addChatMessage({ "from": "system", "text": sprintf( translate("This game's address '%(ip)s' does not appear to be valid."), { "ip": game.ip } ) }); return; } Engine.PushGuiPage("page_gamesetup_mp.xml", { "multiplayerGameType": "join", "ip": ip, "port": port, "name": g_Username, "rating": getRejoinRating(game), "useSTUN": !!game.stunIP, "hostJID": game.hostUsername + "@" + g_LobbyServer + "/0ad" }); } /** * Rejoin games with the original playername, even if the rating changed meanwhile. */ function getRejoinRating(game) { for (let player of stringifiedTeamListToPlayerData(game.players)) { let [nick, rating] = splitRatingFromNick(player.Name); if (nick == g_Username) return rating; } return g_UserRating; } /** * Open the dialog box to enter the game name. */ function hostGame() { Engine.PushGuiPage("page_gamesetup_mp.xml", { "multiplayerGameType": "host", "name": g_Username, "rating": g_UserRating }); } /** * Processes GUI messages sent by the XmppClient. */ function onTick() { updateTimers(); + let updateList = false; + while (true) { let msg = Engine.LobbyGuiPollMessage(); if (!msg) break; if (!g_NetMessageTypes[msg.type]) { warn("Unrecognised message type: " + msg.type); continue; } if (!g_NetMessageTypes[msg.type][msg.level]) { warn("Unrecognised message level: " + msg.level); continue; } - g_NetMessageTypes[msg.type][msg.level](msg); - // To improve performance, only update the playerlist GUI when - // the last update in the current stack is processed - if (msg.type == "chat" && Engine.LobbyGetMucMessageCount() == 0) - updatePlayerList(); + if (g_NetMessageTypes[msg.type][msg.level](msg)) + updateList = true; } + + // To improve performance, only update the playerlist GUI when + // the last update in the current stack is processed + if (updateList) + updatePlayerList(); } /** * Executes a lobby command or sends GUI input directly as chat. */ function submitChatInput() { let input = Engine.GetGUIObjectByName("chatInput"); let text = input.caption; if (!text.length) return; if (handleChatCommand(text)) Engine.LobbySendMessage(text); input.caption = ""; } /** * Handle all '/' commands. * * @param {string} text - Text to be checked for commands. * @returns {boolean} true if the text should be sent via chat. */ function handleChatCommand(text) { if (text[0] != '/') return true; let [cmd, args] = ircSplit(text); args = ircSplit("/" + args); if (!g_ChatCommands[cmd]) { addChatMessage({ "from": "system", "text": sprintf( translate("The command '%(cmd)s' is not supported."), { "cmd": '[color="' + g_ChatCommandColor + '"]' + cmd + '[/color]' }) }); return false; } if (g_ChatCommands[cmd].moderatorOnly && Engine.LobbyGetPlayerRole(g_Username) != "moderator") { addChatMessage({ "from": "system", "text": sprintf( translate("The command '%(cmd)s' is restricted to moderators."), { "cmd": '[color="' + g_ChatCommandColor + '"]' + cmd + '[/color]' }) }); return false; } return g_ChatCommands[cmd].handler(args); } /** * Process and if appropriate, display a formatted message. * * @param {Object} msg - The message to be processed. */ function addChatMessage(msg) { if (msg.from) { if (Engine.LobbyGetPlayerRole(msg.from) == "moderator") msg.from = g_ModeratorPrefix + msg.from; // Highlight local user's nick if (g_Username != msg.from) { msg.text = msg.text.replace(g_Username, colorPlayerName(g_Username)); notifyUser(g_Username, msg.text); } } let formatted = ircFormat(msg); if (!formatted) return; g_ChatMessages.push(formatted); Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } /** * Splits given input into command and argument. */ function ircSplit(string) { let idx = string.indexOf(' '); if (idx != -1) return [string.substr(1,idx-1), string.substr(idx+1)]; return [string.substr(1), ""]; } /** * Format text in an IRC-like way. * * @param {Object} msg - Received chat message. * @returns {string} - Formatted text. */ function ircFormat(msg) { let formattedMessage = ""; let coloredFrom = msg.from && colorPlayerName(msg.from); // Handle commands allowed past handleChatCommand. if (msg.text[0] == '/') { let [command, message] = ircSplit(msg.text); switch (command) { case "me": { // Translation: IRC message prefix when the sender uses the /me command. let senderString = sprintf(translate("* %(sender)s"), { "sender": coloredFrom }); // Translation: IRC message issued using the ‘/me’ command. formattedMessage = sprintf(translate("%(sender)s %(action)s"), { "sender": senderFont(senderString), "action": message }); break; } case "say": { // Translation: IRC message prefix. let senderString = sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }); // Translation: IRC message. formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderFont(senderString), "message": message }); break; } case "special": { if (msg.isSpecial) // Translation: IRC system message. formattedMessage = senderFont(sprintf(translate("== %(message)s"), { "message": message })); else { // Translation: IRC message prefix. let senderString = sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }); // Translation: IRC message. formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderFont(senderString), "message": message }); } break; } } } else { let senderString; // Translation: IRC message prefix. if (msg.private) senderString = sprintf(translateWithContext("lobby private message", "(%(private)s) <%(sender)s>"), { "private": '[color="' + g_PrivateMessageColor + '"]' + translate("Private") + '[/color]', "sender": coloredFrom }); else senderString = sprintf(translate("<%(sender)s>"), { "sender": coloredFrom }); // Translation: IRC message. formattedMessage = sprintf(translate("%(sender)s %(message)s"), { "sender": senderFont(senderString), "message": msg.text }); } // Add chat message timestamp if (Engine.ConfigDB_GetValue("user", "chat.timestamp") != "true") return formattedMessage; // Translation: Time as shown in the multiplayer lobby (when you enable it in the options page). // For a list of symbols that you can use, see: // https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table let timeString = Engine.FormatMillisecondsIntoDateStringLocal(msg.time ? msg.time * 1000 : Date.now(), translate("HH:mm")); // Translation: Time prefix as shown in the multiplayer lobby (when you enable it in the options page). let timePrefixString = sprintf(translate("\\[%(time)s]"), { "time": timeString }); // Translation: IRC message format when there is a time prefix. return sprintf(translate("%(time)s %(message)s"), { "time": senderFont(timePrefixString), "message": formattedMessage }); } /** * Generate a (mostly) unique color for this player based on their name. * @see http://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-jquery-javascript * @param {string} playername */ function getPlayerColor(playername) { if (playername == "system") return g_SystemColor; // Generate a probably-unique hash for the player name and use that to create a color. let hash = 0; for (let i in playername) hash = playername.charCodeAt(i) + ((hash << 5) - hash); // First create the color in RGB then HSL, clamp the lightness so it's not too dark to read, and then convert back to RGB to display. // The reason for this roundabout method is this algorithm can generate values from 0 to 255 for RGB but only 0 to 100 for HSL; this gives // us much more variety if we generate in RGB. Unfortunately, enforcing that RGB values are a certain lightness is very difficult, so // we convert to HSL to do the computation. Since our GUI code only displays RGB colors, we have to convert back. let [h, s, l] = rgbToHsl(hash >> 24 & 0xFF, hash >> 16 & 0xFF, hash >> 8 & 0xFF); return hslToRgb(h, s, Math.max(0.7, l)).join(" "); } /** * Returns the given playername wrapped in an appropriate color-tag. * * @param {string} playername * @param {string} rating */ function colorPlayerName(playername, rating) { return '[color="' + getPlayerColor(playername.replace(g_ModeratorPrefix, "")) + '"]' + (rating ? sprintf( translate("%(nick)s (%(rating)s)"), { "nick": playername, "rating": rating }) : playername) + '[/color]'; } function senderFont(text) { return '[font="' + g_SenderFont + '"]' + text + "[/font]"; } function formatWinRate(attr) { if (!attr.totalGamesPlayed) return translateWithContext("Used for an undefined winning rate", "-"); return sprintf(translate("%(percentage)s%%"), { "percentage": (attr.wins / attr.totalGamesPlayed * 100).toFixed(2) }); } Index: ps/trunk/source/lobby/IXmppClient.h =================================================================== --- ps/trunk/source/lobby/IXmppClient.h (revision 20039) +++ ps/trunk/source/lobby/IXmppClient.h (revision 20040) @@ -1,67 +1,66 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef IXMPPCLIENT_H #define IXMPPCLIENT_H #include "scriptinterface/ScriptTypes.h" class ScriptInterface; namespace StunClient { class StunEndpoint; } class IXmppClient { public: static IXmppClient* create(const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, bool regOpt = false); virtual ~IXmppClient() {} virtual void connect() = 0; virtual void disconnect() = 0; virtual void recv() = 0; virtual void SendIqGetBoardList() = 0; virtual void SendIqGetProfile(const std::string& player) = 0; virtual void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; virtual void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; virtual void SendIqUnregisterGame() = 0; virtual void SendIqChangeStateGame(const std::string& nbp, const std::string& players) = 0; virtual void SetNick(const std::string& nick) = 0; virtual void GetNick(std::string& nick) = 0; virtual void kick(const std::string& nick, const std::string& reason) = 0; virtual void ban(const std::string& nick, const std::string& reason) = 0; virtual void SetPresence(const std::string& presence) = 0; virtual void GetPresence(const std::string& nickname, std::string& presence) = 0; virtual void GetRole(const std::string& nickname, std::string& role) = 0; virtual void GetSubject(std::string& subject) = 0; virtual void GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; virtual void ClearPresenceUpdates() = 0; - virtual int GetMucMessageCount() = 0; virtual void GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; virtual void GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; virtual void GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; virtual void GuiPollMessage(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; virtual void SendMUCMessage(const std::string& message) = 0; virtual void SendStunEndpointToHost(StunClient::StunEndpoint* stunEndpoint, const std::string& hostJID) = 0; }; extern IXmppClient *g_XmppClient; extern bool g_rankedGame; #endif // XMPPCLIENT_H Index: ps/trunk/source/lobby/XmppClient.cpp =================================================================== --- ps/trunk/source/lobby/XmppClient.cpp (revision 20039) +++ ps/trunk/source/lobby/XmppClient.cpp (revision 20040) @@ -1,1145 +1,1133 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "XmppClient.h" #include "StanzaExtensions.h" #ifdef WIN32 # include #endif #include "i18n/L10n.h" #include "lib/external_libraries/enet.h" #include "lib/utf8.h" #include "network/NetServer.h" #include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptInterface.h" //debug #if 1 #define DbgXMPP(x) #else #define DbgXMPP(x) std::cout << x << std::endl; static std::string tag_xml(const glooxwrapper::IQ& iq) { std::string ret; glooxwrapper::Tag* tag = iq.tag(); ret = tag->xml().to_string(); glooxwrapper::Tag::free(tag); return ret; } #endif static std::string tag_name(const glooxwrapper::IQ& iq) { std::string ret; glooxwrapper::Tag* tag = iq.tag(); ret = tag->name().to_string(); glooxwrapper::Tag::free(tag); return ret; } IXmppClient* IXmppClient::create(const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize,bool regOpt) { return new XmppClient(sUsername, sPassword, sRoom, sNick, historyRequestSize, regOpt); } /** * Construct the XMPP client. * * @param sUsername Username to login with of register. * @param sPassword Password to login with or register. * @param sRoom MUC room to join. * @param sNick Nick to join with. * @param historyRequestSize Number of stanzas of room history to request. * @param regOpt If we are just registering or not. */ XmppClient::XmppClient(const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize, bool regOpt) : m_client(NULL), m_mucRoom(NULL), m_registration(NULL), m_username(sUsername), m_password(sPassword), m_nick(sNick), m_initialLoadComplete(false), m_sessionManager() { // Read lobby configuration from default.cfg std::string sServer; std::string sXpartamupp; CFG_GET_VAL("lobby.server", sServer); CFG_GET_VAL("lobby.xpartamupp", sXpartamupp); m_xpartamuppId = sXpartamupp + "@" + sServer + "/CC"; glooxwrapper::JID clientJid(sUsername + "@" + sServer + "/0ad"); glooxwrapper::JID roomJid(sRoom + "@conference." + sServer + "/" + sNick); // If we are connecting, use the full jid and a password // If we are registering, only use the server name if (!regOpt) m_client = new glooxwrapper::Client(clientJid, sPassword); else m_client = new glooxwrapper::Client(sServer); // Disable TLS as we haven't set a certificate on the server yet m_client->setTls(gloox::TLSDisabled); // Disable use of the SASL PLAIN mechanism, to prevent leaking credentials // if the server doesn't list any supported SASL mechanism or the response // has been modified to exclude those. const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain; m_client->setSASLMechanisms(mechs); m_client->registerConnectionListener(this); m_client->setPresence(gloox::Presence::Available, -1); m_client->disco()->setVersion("Pyrogenesis", engine_version); m_client->disco()->setIdentity("client", "bot"); m_client->setCompression(false); m_client->registerStanzaExtension(new GameListQuery()); m_client->registerIqHandler(this, EXTGAMELISTQUERY); m_client->registerStanzaExtension(new BoardListQuery()); m_client->registerIqHandler(this, EXTBOARDLISTQUERY); m_client->registerStanzaExtension(new ProfileQuery()); m_client->registerIqHandler(this, EXTPROFILEQUERY); m_client->registerMessageHandler(this); // Uncomment to see the raw stanzas //m_client->getWrapped()->logInstance().registerLogHandler( gloox::LogLevelDebug, gloox::LogAreaAll, this ); if (!regOpt) { // Create a Multi User Chat Room m_mucRoom = new glooxwrapper::MUCRoom(m_client, roomJid, this, 0); // Get room history. m_mucRoom->setRequestHistory(historyRequestSize, gloox::MUCRoom::HistoryMaxStanzas); } else { // Registration m_registration = new glooxwrapper::Registration(m_client); m_registration->registerRegistrationHandler(this); } m_sessionManager = new glooxwrapper::SessionManager(m_client, this); // Register plugins to allow gloox parse them in incoming sessions m_sessionManager->registerPlugins(); } /** * Destroy the xmpp client */ XmppClient::~XmppClient() { DbgXMPP("XmppClient destroyed"); delete m_registration; delete m_mucRoom; // Workaround for memory leak in gloox 1.0/1.0.1 m_client->removePresenceExtension(gloox::ExtCaps); delete m_client; for (const glooxwrapper::Tag* const& t : m_GameList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_BoardList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_Profile) glooxwrapper::Tag::free(t); } /// Network void XmppClient::connect() { m_initialLoadComplete = false; m_client->connect(false); } void XmppClient::disconnect() { m_client->disconnect(); } void XmppClient::recv() { m_client->recv(1); } /** * Log (debug) Handler */ void XmppClient::handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message) { std::cout << "log: level: " << level << ", area: " << area << ", message: " << message << std::endl; } /***************************************************** * Connection handlers * *****************************************************/ /** * Handle connection */ void XmppClient::onConnect() { if (m_mucRoom) { CreateGUIMessage("system", "connected"); m_mucRoom->join(); } if (m_registration) m_registration->fetchRegistrationFields(); } /** * Handle disconnection */ void XmppClient::onDisconnect(gloox::ConnectionError error) { // Make sure we properly leave the room so that // everything works if we decide to come back later if (m_mucRoom) m_mucRoom->leave(); // Clear game, board and player lists. for (const glooxwrapper::Tag* const& t : m_GameList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_BoardList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_Profile) glooxwrapper::Tag::free(t); m_BoardList.clear(); m_GameList.clear(); m_PlayerMap.clear(); m_Profile.clear(); CreateGUIMessage("system", "disconnected", ConnectionErrorToString(error)); } /** * Handle TLS connection */ bool XmppClient::onTLSConnect(const glooxwrapper::CertInfo& info) { UNUSED2(info); DbgXMPP("onTLSConnect"); DbgXMPP( "status: " << info.status << "\nissuer: " << info.issuer << "\npeer: " << info.server << "\nprotocol: " << info.protocol << "\nmac: " << info.mac << "\ncipher: " << info.cipher << "\ncompression: " << info.compression ); return true; } /** * Handle MUC room errors */ void XmppClient::handleMUCError(glooxwrapper::MUCRoom*, gloox::StanzaError err) { CreateGUIMessage("system", "error", StanzaErrorToString(err)); } /***************************************************** * Requests to server * *****************************************************/ /** * Request the leaderboard data from the server. */ void XmppClient::SendIqGetBoardList() { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Send IQ BoardListQuery* b = new BoardListQuery(); b->m_Command = "getleaderboard"; glooxwrapper::IQ iq(gloox::IQ::Get, xpartamuppJid); iq.addExtension(b); DbgXMPP("SendIqGetBoardList [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Request the profile data from the server. */ void XmppClient::SendIqGetProfile(const std::string& player) { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Send IQ ProfileQuery* b = new ProfileQuery(); b->m_Command = player; glooxwrapper::IQ iq(gloox::IQ::Get, xpartamuppJid); iq.addExtension(b); DbgXMPP("SendIqGetProfile [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Send game report containing numerous game properties to the server. * * @param data A JS array of game statistics */ void XmppClient::SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Setup some base stanza attributes GameReport* game = new GameReport(); glooxwrapper::Tag* report = glooxwrapper::Tag::allocate("game"); // Iterate through all the properties reported and add them to the stanza. std::vector properties; scriptInterface.EnumeratePropertyNamesWithPrefix(data, "", properties); for (const std::string& p : properties) { std::wstring value; scriptInterface.GetProperty(data, p.c_str(), value); report->addAttribute(p, utf8_from_wstring(value)); } // Add stanza to IQ game->m_GameReport.emplace_back(report); // Send IQ glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid); iq.addExtension(game); DbgXMPP("SendGameReport [" << tag_xml(iq) << "]"); m_client->send(iq); }; /** * Send a request to register a game to the server. * * @param data A JS array of game attributes */ void XmppClient::SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Setup some base stanza attributes GameListQuery* g = new GameListQuery(); g->m_Command = "register"; glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game"); // Add a fake ip which will be overwritten by the ip stamp XMPP module on the server. game->addAttribute("ip", "fake"); // Iterate through all the properties reported and add them to the stanza. std::vector properties; scriptInterface.EnumeratePropertyNamesWithPrefix(data, "", properties); for (const std::string& p : properties) { std::wstring value; scriptInterface.GetProperty(data, p.c_str(), value); game->addAttribute(p, utf8_from_wstring(value)); } // Push the stanza onto the IQ g->m_GameList.emplace_back(game); // Send IQ glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid); iq.addExtension(g); DbgXMPP("SendIqRegisterGame [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Send a request to unregister a game to the server. */ void XmppClient::SendIqUnregisterGame() { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Send IQ GameListQuery* g = new GameListQuery(); g->m_Command = "unregister"; g->m_GameList.emplace_back(glooxwrapper::Tag::allocate("game")); glooxwrapper::IQ iq( gloox::IQ::Set, xpartamuppJid ); iq.addExtension(g); DbgXMPP("SendIqUnregisterGame [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Send a request to change the state of a registered game on the server. * * A game can either be in the 'running' or 'waiting' state - the server * decides which - but we need to update the current players that are * in-game so the server can make the calculation. */ void XmppClient::SendIqChangeStateGame(const std::string& nbp, const std::string& players) { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Send IQ GameListQuery* g = new GameListQuery(); g->m_Command = "changestate"; glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game"); game->addAttribute("nbp", nbp); game->addAttribute("players", players); g->m_GameList.emplace_back(game); glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid); iq.addExtension(g); DbgXMPP("SendIqChangeStateGame [" << tag_xml(iq) << "]"); m_client->send(iq); } /***************************************************** * Account registration * *****************************************************/ void XmppClient::handleRegistrationFields(const glooxwrapper::JID&, int fields, glooxwrapper::string) { glooxwrapper::RegistrationFields vals; vals.username = m_username; vals.password = m_password; m_registration->createAccount(fields, vals); } void XmppClient::handleRegistrationResult(const glooxwrapper::JID&, gloox::RegistrationResult result) { if (result == gloox::RegistrationSuccess) CreateGUIMessage("system", "registered"); else CreateGUIMessage("system", "error", RegistrationResultToString(result)); disconnect(); } void XmppClient::handleAlreadyRegistered(const glooxwrapper::JID&) { DbgXMPP("the account already exists"); } void XmppClient::handleDataForm(const glooxwrapper::JID&, const glooxwrapper::DataForm&) { DbgXMPP("dataForm received"); } void XmppClient::handleOOB(const glooxwrapper::JID&, const glooxwrapper::OOB&) { DbgXMPP("OOB registration requested"); } /***************************************************** * Requests from GUI * *****************************************************/ /** * Handle requests from the GUI for the list of players. * * @return A JS array containing all known players and their presences */ void XmppClient::GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); scriptInterface.Eval("([])", ret); // Convert the internal data structure to a Javascript object. for (const std::pair >& p : m_PlayerMap) { JS::RootedValue player(cx); scriptInterface.Eval("({})", &player); scriptInterface.SetProperty(player, "name", wstring_from_utf8(p.first)); scriptInterface.SetProperty(player, "presence", wstring_from_utf8(p.second[0])); scriptInterface.SetProperty(player, "rating", wstring_from_utf8(p.second[1])); scriptInterface.SetProperty(player, "role", wstring_from_utf8(p.second[2])); scriptInterface.CallFunctionVoid(ret, "push", player); } } /** * Handle requests from the GUI for the list of all active games. * * @return A JS array containing all known games */ void XmppClient::GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); scriptInterface.Eval("([])", ret); const char* stats[] = { "name", "ip", "port", "stunIP", "stunPort", "hostUsername", "state", "nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryCondition", "startTime" }; for(const glooxwrapper::Tag* const& t : m_GameList) { JS::RootedValue game(cx); scriptInterface.Eval("({})", &game); for (size_t i = 0; i < ARRAY_SIZE(stats); ++i) scriptInterface.SetProperty(game, stats[i], wstring_from_utf8(t->findAttribute(stats[i]).to_string())); scriptInterface.CallFunctionVoid(ret, "push", game); } } /** * Handle requests from the GUI for leaderboard data. * * @return A JS array containing all known leaderboard data */ void XmppClient::GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); scriptInterface.Eval("([])", ret); const char* attributes[] = { "name", "rank", "rating" }; for(const glooxwrapper::Tag* const& t : m_BoardList) { JS::RootedValue board(cx); scriptInterface.Eval("({})", &board); for (size_t i = 0; i < ARRAY_SIZE(attributes); ++i) scriptInterface.SetProperty(board, attributes[i], wstring_from_utf8(t->findAttribute(attributes[i]).to_string())); scriptInterface.CallFunctionVoid(ret, "push", board); } } /** * Handle requests from the GUI for profile data. * * @return A JS array containing the specific user's profile data */ void XmppClient::GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); scriptInterface.Eval("([])", ret); const char* stats[] = { "player", "rating", "totalGamesPlayed", "highestRating", "wins", "losses", "rank" }; for (const glooxwrapper::Tag* const& t : m_Profile) { JS::RootedValue profile(cx); scriptInterface.Eval("({})", &profile); for (size_t i = 0; i < ARRAY_SIZE(stats); ++i) scriptInterface.SetProperty(profile, stats[i], wstring_from_utf8(t->findAttribute(stats[i]).to_string())); scriptInterface.CallFunctionVoid(ret, "push", profile); } } /***************************************************** * Message interfaces * *****************************************************/ /** * Send GUI message queue when queried. */ void XmppClient::GuiPollMessage(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { if (m_GuiMessageQueue.empty()) { ret.setUndefined(); return; } GUIMessage message = m_GuiMessageQueue.front(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); scriptInterface.Eval("({})", ret); scriptInterface.SetProperty(ret, "type", message.type); if (!message.from.empty()) scriptInterface.SetProperty(ret, "from", message.from); if (!message.text.empty()) scriptInterface.SetProperty(ret, "text", message.text); if (!message.level.empty()) scriptInterface.SetProperty(ret, "level", message.level); if (!message.data.empty()) scriptInterface.SetProperty(ret, "data", message.data); scriptInterface.SetProperty(ret, "time", (double)message.time); m_GuiMessageQueue.pop_front(); } /** * Send a standard MUC textual message. */ void XmppClient::SendMUCMessage(const std::string& message) { m_mucRoom->send(message); } /** * Push a message onto the GUI queue. * * @param message Message to add to the queue */ void XmppClient::PushGuiMessage(XmppClient::GUIMessage message) { m_GuiMessageQueue.push_back(std::move(message)); } /** * Clears all presence updates from the message queue. * Used when rejoining the lobby, since we don't need to handle past presence changes. */ void XmppClient::ClearPresenceUpdates() { m_GuiMessageQueue.erase( std::remove_if(m_GuiMessageQueue.begin(), m_GuiMessageQueue.end(), [](XmppClient::GUIMessage& message) { return message.type == L"chat" && message.level == L"presence"; } ), m_GuiMessageQueue.end()); } /** - * Used in order to update the GUI only once when multiple updates are queued. - */ -int XmppClient::GetMucMessageCount() -{ - return std::count_if(m_GuiMessageQueue.begin(), m_GuiMessageQueue.end(), - [](XmppClient::GUIMessage& message) - { - return message.type == L"chat"; - }); -} - -/** * Handle a room message. */ void XmppClient::handleMUCMessage(glooxwrapper::MUCRoom*, const glooxwrapper::Message& msg, bool priv) { DbgXMPP(msg.from().resource() << " said " << msg.body()); GUIMessage message; message.type = L"chat"; message.level = priv ? L"private-message" : L"room-message"; message.from = wstring_from_utf8(msg.from().resource().to_string()); message.text = wstring_from_utf8(msg.body().to_string()); message.time = ComputeTimestamp(msg); PushGuiMessage(message); } /** * Handle a private message. */ void XmppClient::handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession *) { DbgXMPP("type " << msg.subtype() << ", subject " << msg.subject() << ", message " << msg.body() << ", thread id " << msg.thread()); GUIMessage message; message.type = L"chat"; message.level = L"private-message"; message.from = wstring_from_utf8(msg.from().username().to_string()); message.text = wstring_from_utf8(msg.body().to_string()); message.time = ComputeTimestamp(msg); PushGuiMessage(message); } /** * Handle portions of messages containing custom stanza extensions. */ bool XmppClient::handleIq(const glooxwrapper::IQ& iq) { DbgXMPP("handleIq [" << tag_xml(iq) << "]"); if (iq.subtype() == gloox::IQ::Result) { const GameListQuery* gq = iq.findExtension(EXTGAMELISTQUERY); const BoardListQuery* bq = iq.findExtension(EXTBOARDLISTQUERY); const ProfileQuery* pq = iq.findExtension(EXTPROFILEQUERY); if (gq) { for (const glooxwrapper::Tag* const& t : m_GameList) glooxwrapper::Tag::free(t); m_GameList.clear(); for (const glooxwrapper::Tag* const& t : gq->m_GameList) m_GameList.emplace_back(t->clone()); CreateGUIMessage("game", "gamelist"); } if (bq) { if (bq->m_Command == "boardlist") { for (const glooxwrapper::Tag* const& t : m_BoardList) glooxwrapper::Tag::free(t); m_BoardList.clear(); for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList) m_BoardList.emplace_back(t->clone()); CreateGUIMessage("game", "leaderboard"); } else if (bq->m_Command == "ratinglist") { for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList) { std::string name = t->findAttribute("name").to_string(); if (m_PlayerMap.find(name) != m_PlayerMap.end()) m_PlayerMap[name][1] = t->findAttribute("rating").to_string(); } CreateGUIMessage("game", "ratinglist"); } } if (pq) { for (const glooxwrapper::Tag* const& t : m_Profile) glooxwrapper::Tag::free(t); m_Profile.clear(); for (const glooxwrapper::Tag* const& t : pq->m_StanzaProfile) m_Profile.emplace_back(t->clone()); CreateGUIMessage("game", "profile"); } } else if (iq.subtype() == gloox::IQ::Error) { gloox::StanzaError err = iq.error_error(); CreateGUIMessage("system", "error", StanzaErrorToString(err)); } else { CreateGUIMessage("system", "error", g_L10n.Translate("unknown subtype (see logs)")); std::string tag = tag_name(iq); LOGMESSAGE("unknown subtype '%s'", tag.c_str()); } return true; } /** * Create a new detail message for the GUI. * * @param type General message type * @param level Detailed message type * @param text Body of the message * @param data Optional field, used for auxiliary data */ void XmppClient::CreateGUIMessage(const std::string& type, const std::string& level, const std::string& text, const std::string& data) { GUIMessage message; message.type = wstring_from_utf8(type); message.level = wstring_from_utf8(level); message.text = wstring_from_utf8(text); message.data = wstring_from_utf8(data); message.time = std::time(nullptr); PushGuiMessage(message); } /***************************************************** * Presence, nickname, and subject * *****************************************************/ /** * Update local data when a user changes presence. */ void XmppClient::handleMUCParticipantPresence(glooxwrapper::MUCRoom*, const glooxwrapper::MUCRoomParticipant participant, const glooxwrapper::Presence& presence) { std::string nick = participant.nick->resource().to_string(); gloox::Presence::PresenceType presenceType = presence.presence(); std::string presenceString, roleString; GetPresenceString(presenceType, presenceString); GetRoleString(participant.role, roleString); if (presenceType == gloox::Presence::Unavailable) { if (!participant.newNick.empty() && (participant.flags & (gloox::UserNickChanged | gloox::UserSelf))) { // we have a nick change std::string newNick = participant.newNick.to_string(); m_PlayerMap[newNick].resize(3); m_PlayerMap[newNick][0] = presenceString; m_PlayerMap[newNick][2] = roleString; CreateGUIMessage("chat", "nick", nick, participant.newNick.to_string()); DbgXMPP(nick << " is now known as " << participant.newNick.to_string()); } else if (participant.flags & gloox::UserKicked) { DbgXMPP(nick << " was kicked. Reason: " << participant.reason.to_string()); CreateGUIMessage("chat", "kicked", nick, participant.reason.to_string()); } else if (participant.flags & gloox::UserBanned) { DbgXMPP(nick << " was banned. Reason: " << participant.reason.to_string()); CreateGUIMessage("chat", "banned", nick, participant.reason.to_string()); } else { DbgXMPP(nick << " left the room (flags " << participant.flags << ")"); CreateGUIMessage("chat", "leave", nick); } m_PlayerMap.erase(nick); } else { /* During the initialization process, we recieve join messages for everyone * currently in the room. We don't want to display these, so we filter them * out. We will always be the last to join during initialization. */ if (!m_initialLoadComplete) { if (m_mucRoom->nick().to_string() == nick) m_initialLoadComplete = true; } else if (m_PlayerMap.find(nick) == m_PlayerMap.end()) CreateGUIMessage("chat", "join", nick); else if (m_PlayerMap[nick][2] != roleString) CreateGUIMessage("chat", "role", nick, m_PlayerMap[nick][2]); else CreateGUIMessage("chat", "presence", nick); DbgXMPP(nick << " is in the room, presence : " << (int)presenceType); m_PlayerMap[nick].resize(3); m_PlayerMap[nick][0] = presenceString; m_PlayerMap[nick][2] = roleString; } } /** * Update local cache when subject changes. */ void XmppClient::handleMUCSubject(glooxwrapper::MUCRoom*, const glooxwrapper::string& UNUSED(nick), const glooxwrapper::string& subject) { m_Subject = subject.c_str(); CreateGUIMessage("chat", "subject", m_Subject); } /** * Get current subject. * * @param topic Variable to store subject in. */ void XmppClient::GetSubject(std::string& subject) { subject = m_Subject; } /** * Request nick change, real change via mucRoomHandler. * * @param nick Desired nickname */ void XmppClient::SetNick(const std::string& nick) { m_mucRoom->setNick(nick); } /** * Get current nickname. * * @param nick Variable to store the nickname in. */ void XmppClient::GetNick(std::string& nick) { nick = m_mucRoom->nick().to_string(); } /** * Kick a player from the current room. * * @param nick Nickname to be kicked * @param reason Reason the player was kicked */ void XmppClient::kick(const std::string& nick, const std::string& reason) { m_mucRoom->kick(nick, reason); } /** * Ban a player from the current room. * * @param nick Nickname to be banned * @param reason Reason the player was banned */ void XmppClient::ban(const std::string& nick, const std::string& reason) { m_mucRoom->ban(nick, reason); } /** * Change the xmpp presence of the client. * * @param presence A string containing the desired presence */ void XmppClient::SetPresence(const std::string& presence) { #define IF(x,y) if (presence == x) m_mucRoom->setPresence(gloox::Presence::y) IF("available", Available); else IF("chat", Chat); else IF("away", Away); else IF("playing", DND); else IF("offline", Unavailable); // The others are not to be set #undef IF else LOGERROR("Unknown presence '%s'", presence.c_str()); } /** * Get the current xmpp presence of the given nick. * * @param nick Nickname to look up presence for * @param presence Variable to store the presence in */ void XmppClient::GetPresence(const std::string& nick, std::string& presence) { if (m_PlayerMap.find(nick) != m_PlayerMap.end()) presence = m_PlayerMap[nick][0]; else presence = "offline"; } /** * Get the current xmpp role of the given nick. * * @param nick Nickname to look up presence for * @param role Variable to store the role in */ void XmppClient::GetRole(const std::string& nick, std::string& role) { if (m_PlayerMap.find(nick) != m_PlayerMap.end()) role = m_PlayerMap[nick][2]; else role = ""; } /***************************************************** * Utilities * *****************************************************/ /** * Parse and return the timestamp of a historic chat message and return the current time for new chat messages. * Historic chat messages are implement as DelayedDelivers as specified in XEP-0203. * Hence, their timestamp MUST be in UTC and conform to the DateTime format XEP-0082. * * @returns Seconds since the epoch. */ std::time_t XmppClient::ComputeTimestamp(const glooxwrapper::Message& msg) const { // Only historic messages contain a timestamp! if (!msg.when()) return std::time(nullptr); // The locale is irrelevant, because the XMPP date format doesn't contain written month names return g_L10n.ParseDateTime(msg.when()->stamp().to_string(), "Y-M-d'T'H:m:sZ", Locale::getUS()) / 1000.0; } /** * Convert a gloox presence type to string. * * @param p Presence to be converted * @param presence Variable to store the converted presence string in */ void XmppClient::GetPresenceString(const gloox::Presence::PresenceType p, std::string& presence) const { switch(p) { #define CASE(x,y) case gloox::Presence::x: presence = y; break CASE(Available, "available"); CASE(Chat, "chat"); CASE(Away, "away"); CASE(DND, "playing"); CASE(XA, "away"); CASE(Unavailable, "offline"); CASE(Probe, "probe"); CASE(Error, "error"); CASE(Invalid, "invalid"); default: LOGERROR("Unknown presence type '%d'", (int)p); break; #undef CASE } } /** * Convert a gloox role type to string. * * @param p Role to be converted * @param presence Variable to store the converted role string in */ void XmppClient::GetRoleString(const gloox::MUCRoomRole r, std::string& role) const { switch(r) { #define CASE(X, Y) case gloox::X: role = Y; break CASE(RoleNone, "none"); CASE(RoleVisitor, "visitor"); CASE(RoleParticipant, "participant"); CASE(RoleModerator, "moderator"); CASE(RoleInvalid, "invalid"); default: LOGERROR("Unknown role type '%d'", (int)r); break; #undef CASE } } /** * Convert a gloox stanza error type to string. * Keep in sync with Gloox documentation * * @param err Error to be converted * @return Converted error string */ std::string XmppClient::StanzaErrorToString(gloox::StanzaError err) const { #define CASE(X, Y) case gloox::X: return Y #define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")" switch (err) { CASE(StanzaErrorUndefined, g_L10n.Translate("No error")); DEBUG_CASE(StanzaErrorBadRequest, "Server recieved malformed XML"); CASE(StanzaErrorConflict, g_L10n.Translate("Player already logged in")); DEBUG_CASE(StanzaErrorFeatureNotImplemented, "Server does not implement requested feature"); CASE(StanzaErrorForbidden, g_L10n.Translate("Forbidden")); DEBUG_CASE(StanzaErrorGone, "Unable to find message receipiant"); CASE(StanzaErrorInternalServerError, g_L10n.Translate("Internal server error")); DEBUG_CASE(StanzaErrorItemNotFound, "Message receipiant does not exist"); DEBUG_CASE(StanzaErrorJidMalformed, "JID (XMPP address) malformed"); DEBUG_CASE(StanzaErrorNotAcceptable, "Receipiant refused message. Possible policy issue"); CASE(StanzaErrorNotAllowed, g_L10n.Translate("Not allowed")); CASE(StanzaErrorNotAuthorized, g_L10n.Translate("Not authorized")); DEBUG_CASE(StanzaErrorNotModified, "Requested item has not changed since last request"); DEBUG_CASE(StanzaErrorPaymentRequired, "This server requires payment"); CASE(StanzaErrorRecipientUnavailable, g_L10n.Translate("Recipient temporarily unavailable")); DEBUG_CASE(StanzaErrorRedirect, "Request redirected"); CASE(StanzaErrorRegistrationRequired, g_L10n.Translate("Registration required")); DEBUG_CASE(StanzaErrorRemoteServerNotFound, "Remote server not found"); DEBUG_CASE(StanzaErrorRemoteServerTimeout, "Remote server timed out"); DEBUG_CASE(StanzaErrorResourceConstraint, "The recipient is unable to process the message due to resource constraints"); CASE(StanzaErrorServiceUnavailable, g_L10n.Translate("Service unavailable")); DEBUG_CASE(StanzaErrorSubscribtionRequired, "Service requires subscription"); DEBUG_CASE(StanzaErrorUnexpectedRequest, "Attempt to send from invalid stanza address"); DEBUG_CASE(StanzaErrorUnknownSender, "Invalid 'from' address"); default: return g_L10n.Translate("Unknown error"); } #undef DEBUG_CASE #undef CASE } /** * Convert a gloox connection error enum to string * Keep in sync with Gloox documentation * * @param err Error to be converted * @return Converted error string */ std::string XmppClient::ConnectionErrorToString(gloox::ConnectionError err) const { #define CASE(X, Y) case gloox::X: return Y #define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")" switch (err) { CASE(ConnNoError, g_L10n.Translate("No error")); CASE(ConnStreamError, g_L10n.Translate("Stream error")); CASE(ConnStreamVersionError, g_L10n.Translate("The incoming stream version is unsupported")); CASE(ConnStreamClosed, g_L10n.Translate("The stream has been closed by the server")); DEBUG_CASE(ConnProxyAuthRequired, "The HTTP/SOCKS5 proxy requires authentication"); DEBUG_CASE(ConnProxyAuthFailed, "HTTP/SOCKS5 proxy authentication failed"); DEBUG_CASE(ConnProxyNoSupportedAuth, "The HTTP/SOCKS5 proxy requires an unsupported authentication mechanism"); CASE(ConnIoError, g_L10n.Translate("An I/O error occured")); DEBUG_CASE(ConnParseError, "An XML parse error occured"); CASE(ConnConnectionRefused, g_L10n.Translate("The connection was refused by the server")); CASE(ConnDnsError, g_L10n.Translate("Resolving the server's hostname failed")); CASE(ConnOutOfMemory, g_L10n.Translate("This system is out of memory")); DEBUG_CASE(ConnNoSupportedAuth, "The authentication mechanisms the server offered are not supported or no authentication mechanisms were available"); CASE(ConnTlsFailed, g_L10n.Translate("The server's certificate could not be verified or the TLS handshake did not complete successfully")); CASE(ConnTlsNotAvailable, g_L10n.Translate("The server did not offer required TLS encryption")); DEBUG_CASE(ConnCompressionFailed, "Negotiation/initializing compression failed"); CASE(ConnAuthenticationFailed, g_L10n.Translate("Authentication failed. Incorrect password or account does not exist")); CASE(ConnUserDisconnected, g_L10n.Translate("The user or system requested a disconnect")); CASE(ConnNotConnected, g_L10n.Translate("There is no active connection")); default: return g_L10n.Translate("Unknown error"); } #undef DEBUG_CASE #undef CASE } /** * Convert a gloox registration result enum to string * Keep in sync with Gloox documentation * * @param err Enum to be converted * @return Converted string */ std::string XmppClient::RegistrationResultToString(gloox::RegistrationResult res) const { #define CASE(X, Y) case gloox::X: return Y #define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")" switch (res) { CASE(RegistrationSuccess, g_L10n.Translate("Success")); CASE(RegistrationNotAcceptable, g_L10n.Translate("Not all necessary information provided")); CASE(RegistrationConflict, g_L10n.Translate("Username already exists")); DEBUG_CASE(RegistrationNotAuthorized, "Account removal timeout or insufficiently secure channel for password change"); DEBUG_CASE(RegistrationBadRequest, "Server recieved incomplete request"); DEBUG_CASE(RegistrationForbidden, "Registration forbidden"); DEBUG_CASE(RegistrationRequired, "Account cannot be removed as it does not exist"); DEBUG_CASE(RegistrationUnexpectedRequest, "This client is unregistered with the server"); DEBUG_CASE(RegistrationNotAllowed, "Server does not permit password changes"); default: return ""; } #undef DEBUG_CASE #undef CASE } void XmppClient::SendStunEndpointToHost(StunClient::StunEndpoint* stunEndpoint, const std::string& hostJIDStr) { ENSURE(stunEndpoint); char ipStr[256] = "(error)"; ENetAddress addr; addr.host = ntohl(stunEndpoint->ip); enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr)); glooxwrapper::JID hostJID(hostJIDStr); glooxwrapper::Jingle::Session session = m_sessionManager->createSession(hostJID); session.sessionInitiate(ipStr, stunEndpoint->port); } void XmppClient::handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session* UNUSED(session), const glooxwrapper::Jingle::Session::Jingle* jingle) { if (action == gloox::Jingle::SessionInitiate) handleSessionInitiation(jingle); } void XmppClient::handleSessionInitiation(const glooxwrapper::Jingle::Session::Jingle* jingle) { glooxwrapper::Jingle::ICEUDP::Candidate candidate = jingle->getCandidate(); if (candidate.ip.empty()) { LOGERROR("Failed to retrieve Jingle candidate"); return; } g_NetServer->SendHolePunchingMessage(candidate.ip.to_string(), candidate.port); } Index: ps/trunk/source/lobby/XmppClient.h =================================================================== --- ps/trunk/source/lobby/XmppClient.h (revision 20039) +++ ps/trunk/source/lobby/XmppClient.h (revision 20040) @@ -1,169 +1,168 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef XXXMPPCLIENT_H #define XXXMPPCLIENT_H #include "IXmppClient.h" #include #include "glooxwrapper/glooxwrapper.h" #include "scriptinterface/ScriptVal.h" class ScriptInterface; namespace glooxwrapper { class Client; struct CertInfo; } class XmppClient : public IXmppClient, public glooxwrapper::ConnectionListener, public glooxwrapper::MUCRoomHandler, public glooxwrapper::IqHandler, public glooxwrapper::RegistrationHandler, public glooxwrapper::MessageHandler, public glooxwrapper::Jingle::SessionHandler { NONCOPYABLE(XmppClient); private: // Components glooxwrapper::Client* m_client; glooxwrapper::MUCRoom* m_mucRoom; glooxwrapper::Registration* m_registration; glooxwrapper::SessionManager* m_sessionManager; // Account infos std::string m_username; std::string m_password; std::string m_nick; std::string m_xpartamuppId; // State bool m_initialLoadComplete; public: // Basic XmppClient(const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, const bool regOpt = false); virtual ~XmppClient(); // Network void connect(); void disconnect(); void recv(); void SendIqGetBoardList(); void SendIqGetProfile(const std::string& player); void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data); void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data); void SendIqUnregisterGame(); void SendIqChangeStateGame(const std::string& nbp, const std::string& players); void SetNick(const std::string& nick); void GetNick(std::string& nick); void kick(const std::string& nick, const std::string& reason); void ban(const std::string& nick, const std::string& reason); void SetPresence(const std::string& presence); void GetPresence(const std::string& nickname, std::string& presence); void GetRole(const std::string& nickname, std::string& role); void GetSubject(std::string& subject); void GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret); void GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret); void GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret); void GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret); void SendStunEndpointToHost(StunClient::StunEndpoint* stunEndpoint, const std::string& hostJID); protected: /* Xmpp handlers */ /* MUC handlers */ virtual void handleMUCParticipantPresence(glooxwrapper::MUCRoom*, const glooxwrapper::MUCRoomParticipant, const glooxwrapper::Presence&); virtual void handleMUCError(glooxwrapper::MUCRoom*, gloox::StanzaError); virtual void handleMUCMessage(glooxwrapper::MUCRoom* room, const glooxwrapper::Message& msg, bool priv); virtual void handleMUCSubject(glooxwrapper::MUCRoom*, const glooxwrapper::string& nick, const glooxwrapper::string& subject); /* MUC handlers not supported by glooxwrapper */ // virtual bool handleMUCRoomCreation(glooxwrapper::MUCRoom*) {return false;} // virtual void handleMUCInviteDecline(glooxwrapper::MUCRoom*, const glooxwrapper::JID&, const std::string&) {} // virtual void handleMUCInfo(glooxwrapper::MUCRoom*, int, const std::string&, const glooxwrapper::DataForm*) {} // virtual void handleMUCItems(glooxwrapper::MUCRoom*, const std::list >&) {} /* Log handler */ virtual void handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message); /* ConnectionListener handlers*/ virtual void onConnect(); virtual void onDisconnect(gloox::ConnectionError e); virtual bool onTLSConnect(const glooxwrapper::CertInfo& info); /* Iq Handlers */ virtual bool handleIq(const glooxwrapper::IQ& iq); virtual void handleIqID(const glooxwrapper::IQ&, int) {} /* Registration Handlers */ virtual void handleRegistrationFields(const glooxwrapper::JID& /*from*/, int fields, glooxwrapper::string instructions ); virtual void handleRegistrationResult(const glooxwrapper::JID& /*from*/, gloox::RegistrationResult result); virtual void handleAlreadyRegistered(const glooxwrapper::JID& /*from*/); virtual void handleDataForm(const glooxwrapper::JID& /*from*/, const glooxwrapper::DataForm& /*form*/); virtual void handleOOB(const glooxwrapper::JID& /*from*/, const glooxwrapper::OOB& oob); /* Message Handler */ virtual void handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession* session); /* Session Handler */ virtual void handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session* UNUSED(session), const glooxwrapper::Jingle::Session::Jingle* jingle); virtual void handleSessionInitiation(const glooxwrapper::Jingle::Session::Jingle* jingle); // Helpers void GetPresenceString(const gloox::Presence::PresenceType p, std::string& presence) const; void GetRoleString(const gloox::MUCRoomRole r, std::string& role) const; std::string StanzaErrorToString(gloox::StanzaError err) const; std::string ConnectionErrorToString(gloox::ConnectionError err) const; std::string RegistrationResultToString(gloox::RegistrationResult res) const; std::time_t ComputeTimestamp(const glooxwrapper::Message& msg) const; public: /* Messages */ struct GUIMessage { std::wstring type; std::wstring level; std::wstring text; std::wstring data; std::wstring from; std::wstring message; std::time_t time; }; void GuiPollMessage(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret); void SendMUCMessage(const std::string& message); void ClearPresenceUpdates(); - int GetMucMessageCount(); protected: void PushGuiMessage(XmppClient::GUIMessage message); void CreateGUIMessage(const std::string& type, const std::string& level, const std::string& text = "", const std::string& data = ""); private: /// Map of players std::map > m_PlayerMap; /// List of games std::vector m_GameList; /// List of rankings std::vector m_BoardList; /// Profile data std::vector m_Profile; /// Queue of messages for the GUI std::deque m_GuiMessageQueue; /// Current room subject/topic. std::string m_Subject; }; #endif // XMPPCLIENT_H Index: ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp =================================================================== --- ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 20039) +++ ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 20040) @@ -1,364 +1,358 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_Lobby.h" #include "gui/GUIManager.h" #include "lib/utf8.h" #include "lobby/IXmppClient.h" #include "ps/Profile.h" #include "scriptinterface/ScriptInterface.h" #include "third_party/encryption/pkcs5_pbkdf2.h" #include "third_party/encryption/sha.h" void JSI_Lobby::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { // Lobby functions scriptInterface.RegisterFunction("HasXmppClient"); scriptInterface.RegisterFunction("IsRankedGame"); scriptInterface.RegisterFunction("SetRankedGame"); #if CONFIG2_LOBBY // Allow the lobby to be disabled scriptInterface.RegisterFunction("StartXmppClient"); scriptInterface.RegisterFunction("StartRegisterXmppClient"); scriptInterface.RegisterFunction("StopXmppClient"); scriptInterface.RegisterFunction("ConnectXmppClient"); scriptInterface.RegisterFunction("DisconnectXmppClient"); scriptInterface.RegisterFunction("SendGetBoardList"); scriptInterface.RegisterFunction("SendGetProfile"); scriptInterface.RegisterFunction("SendRegisterGame"); scriptInterface.RegisterFunction("SendGameReport"); scriptInterface.RegisterFunction("SendUnregisterGame"); scriptInterface.RegisterFunction("SendChangeStateGame"); scriptInterface.RegisterFunction("GetPlayerList"); scriptInterface.RegisterFunction("LobbyClearPresenceUpdates"); - scriptInterface.RegisterFunction("LobbyGetMucMessageCount"); scriptInterface.RegisterFunction("GetGameList"); scriptInterface.RegisterFunction("GetBoardList"); scriptInterface.RegisterFunction("GetProfile"); scriptInterface.RegisterFunction("LobbyGuiPollMessage"); scriptInterface.RegisterFunction("LobbySendMessage"); scriptInterface.RegisterFunction("LobbySetPlayerPresence"); scriptInterface.RegisterFunction("LobbySetNick"); scriptInterface.RegisterFunction("LobbyGetNick"); scriptInterface.RegisterFunction("LobbyKick"); scriptInterface.RegisterFunction("LobbyBan"); scriptInterface.RegisterFunction("LobbyGetPlayerPresence"); scriptInterface.RegisterFunction("LobbyGetPlayerRole"); scriptInterface.RegisterFunction("EncryptPassword"); scriptInterface.RegisterFunction("LobbyGetRoomSubject"); #endif // CONFIG2_LOBBY } bool JSI_Lobby::HasXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_XmppClient; } bool JSI_Lobby::IsRankedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_rankedGame; } void JSI_Lobby::SetRankedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool isRanked) { g_rankedGame = isRanked; } #if CONFIG2_LOBBY void JSI_Lobby::StartXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& username, const std::wstring& password, const std::wstring& room, const std::wstring& nick, int historyRequestSize) { ENSURE(!g_XmppClient); g_XmppClient = IXmppClient::create(utf8_from_wstring(username), utf8_from_wstring(password), utf8_from_wstring(room), utf8_from_wstring(nick), historyRequestSize); g_rankedGame = true; } void JSI_Lobby::StartRegisterXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& username, const std::wstring& password) { ENSURE(!g_XmppClient); g_XmppClient = IXmppClient::create(utf8_from_wstring(username), utf8_from_wstring(password), "", "", 0, true); } void JSI_Lobby::StopXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ENSURE(g_XmppClient); SAFE_DELETE(g_XmppClient); g_rankedGame = false; } void JSI_Lobby::ConnectXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ENSURE(g_XmppClient); g_XmppClient->connect(); } void JSI_Lobby::DisconnectXmppClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ENSURE(g_XmppClient); g_XmppClient->disconnect(); } void JSI_Lobby::SendGetBoardList(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_XmppClient) return; g_XmppClient->SendIqGetBoardList(); } void JSI_Lobby::SendGetProfile(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& player) { if (!g_XmppClient) return; g_XmppClient->SendIqGetProfile(utf8_from_wstring(player)); } void JSI_Lobby::SendGameReport(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data) { if (!g_XmppClient) return; g_XmppClient->SendIqGameReport(*(pCxPrivate->pScriptInterface), data); } void JSI_Lobby::SendRegisterGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data) { if (!g_XmppClient) return; g_XmppClient->SendIqRegisterGame(*(pCxPrivate->pScriptInterface), data); } void JSI_Lobby::SendUnregisterGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_XmppClient) return; g_XmppClient->SendIqUnregisterGame(); } void JSI_Lobby::SendChangeStateGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nbp, const std::wstring& players) { if (!g_XmppClient) return; g_XmppClient->SendIqChangeStateGame(utf8_from_wstring(nbp), utf8_from_wstring(players)); } JS::Value JSI_Lobby::GetPlayerList(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_XmppClient) return JS::UndefinedValue(); JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue playerList(cx); g_XmppClient->GUIGetPlayerList(*(pCxPrivate->pScriptInterface), &playerList); return playerList; } void JSI_Lobby::LobbyClearPresenceUpdates(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_XmppClient) return; g_XmppClient->ClearPresenceUpdates(); } -int JSI_Lobby::LobbyGetMucMessageCount(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) -{ - return g_XmppClient ? g_XmppClient->GetMucMessageCount() : 0; -} - JS::Value JSI_Lobby::GetGameList(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_XmppClient) return JS::UndefinedValue(); JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue gameList(cx); g_XmppClient->GUIGetGameList(*(pCxPrivate->pScriptInterface), &gameList); return gameList; } JS::Value JSI_Lobby::GetBoardList(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_XmppClient) return JS::UndefinedValue(); JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue boardList(cx); g_XmppClient->GUIGetBoardList(*(pCxPrivate->pScriptInterface), &boardList); return boardList; } JS::Value JSI_Lobby::GetProfile(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_XmppClient) return JS::UndefinedValue(); JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue profileFetch(cx); g_XmppClient->GUIGetProfile(*(pCxPrivate->pScriptInterface), &profileFetch); return profileFetch; } JS::Value JSI_Lobby::LobbyGuiPollMessage(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_XmppClient) return JS::UndefinedValue(); JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue poll(cx); g_XmppClient->GuiPollMessage(*(pCxPrivate->pScriptInterface), &poll); return poll; } void JSI_Lobby::LobbySendMessage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& message) { if (!g_XmppClient) return; g_XmppClient->SendMUCMessage(utf8_from_wstring(message)); } void JSI_Lobby::LobbySetPlayerPresence(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& presence) { if (!g_XmppClient) return; g_XmppClient->SetPresence(utf8_from_wstring(presence)); } void JSI_Lobby::LobbySetNick(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nick) { if (!g_XmppClient) return; g_XmppClient->SetNick(utf8_from_wstring(nick)); } std::wstring JSI_Lobby::LobbyGetNick(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_XmppClient) return L""; std::string nick; g_XmppClient->GetNick(nick); return wstring_from_utf8(nick); } void JSI_Lobby::LobbyKick(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nick, const std::wstring& reason) { if (!g_XmppClient) return; g_XmppClient->kick(utf8_from_wstring(nick), utf8_from_wstring(reason)); } void JSI_Lobby::LobbyBan(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nick, const std::wstring& reason) { if (!g_XmppClient) return; g_XmppClient->ban(utf8_from_wstring(nick), utf8_from_wstring(reason)); } std::wstring JSI_Lobby::LobbyGetPlayerPresence(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nickname) { if (!g_XmppClient) return L""; std::string presence; g_XmppClient->GetPresence(utf8_from_wstring(nickname), presence); return wstring_from_utf8(presence); } std::wstring JSI_Lobby::LobbyGetPlayerRole(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& nickname) { if (!g_XmppClient) return L""; std::string role; g_XmppClient->GetRole(utf8_from_wstring(nickname), role); return wstring_from_utf8(role); } // Non-public secure PBKDF2 hash function with salting and 1,337 iterations std::string JSI_Lobby::EncryptPassword(const std::string& password, const std::string& username) { const int DIGESTSIZE = SHA_DIGEST_SIZE; const int ITERATIONS = 1337; static const unsigned char salt_base[DIGESTSIZE] = { 244, 243, 249, 244, 32, 33, 34, 35, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 133, 123, 234, 123 }; // initialize the salt buffer unsigned char salt_buffer[DIGESTSIZE] = {0}; SHA256 hash; hash.update(salt_base, sizeof(salt_base)); hash.update(username.c_str(), username.length()); hash.finish(salt_buffer); // PBKDF2 to create the buffer unsigned char encrypted[DIGESTSIZE]; pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS); static const char base16[] = "0123456789ABCDEF"; char hex[2 * DIGESTSIZE]; for (int i = 0; i < DIGESTSIZE; ++i) { hex[i*2] = base16[encrypted[i] >> 4]; // 4 high bits hex[i*2 + 1] = base16[encrypted[i] & 0x0F]; // 4 low bits } return std::string(hex, sizeof(hex)); } std::wstring JSI_Lobby::EncryptPassword(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& pass, const std::wstring& user) { return wstring_from_utf8(JSI_Lobby::EncryptPassword(utf8_from_wstring(pass), utf8_from_wstring(user))); } std::wstring JSI_Lobby::LobbyGetRoomSubject(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_XmppClient) return L""; std::string subject; g_XmppClient->GetSubject(subject); return wstring_from_utf8(subject); } #endif Index: ps/trunk/source/lobby/scripting/JSInterface_Lobby.h =================================================================== --- ps/trunk/source/lobby/scripting/JSInterface_Lobby.h (revision 20039) +++ ps/trunk/source/lobby/scripting/JSInterface_Lobby.h (revision 20040) @@ -1,70 +1,69 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_JSI_LOBBY #define INCLUDED_JSI_LOBBY #include "lib/config2.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptVal.h" namespace JSI_Lobby { void RegisterScriptFunctions(const ScriptInterface& scriptInterface); bool HasXmppClient(ScriptInterface::CxPrivate* pCxPrivate); bool IsRankedGame(ScriptInterface::CxPrivate* pCxPrivate); void SetRankedGame(ScriptInterface::CxPrivate* pCxPrivate, bool isRanked); #if CONFIG2_LOBBY void StartXmppClient(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& username, const std::wstring& password, const std::wstring& room, const std::wstring& nick, int historyRequestSize); void StartRegisterXmppClient(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& username, const std::wstring& password); void StopXmppClient(ScriptInterface::CxPrivate* pCxPrivate); void ConnectXmppClient(ScriptInterface::CxPrivate* pCxPrivate); void DisconnectXmppClient(ScriptInterface::CxPrivate* pCxPrivate); void SendGetBoardList(ScriptInterface::CxPrivate* pCxPrivate); void SendGetProfile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& player); void SendGameReport(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data); void SendRegisterGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data); void SendUnregisterGame(ScriptInterface::CxPrivate* pCxPrivate); void SendChangeStateGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nbp, const std::wstring& players); JS::Value GetPlayerList(ScriptInterface::CxPrivate* pCxPrivate); void LobbyClearPresenceUpdates(ScriptInterface::CxPrivate* pCxPrivate); - int LobbyGetMucMessageCount(ScriptInterface::CxPrivate* pCxPrivate); JS::Value GetGameList(ScriptInterface::CxPrivate* pCxPrivate); JS::Value GetBoardList(ScriptInterface::CxPrivate* pCxPrivate); JS::Value GetProfile(ScriptInterface::CxPrivate* pCxPrivate); JS::Value LobbyGuiPollMessage(ScriptInterface::CxPrivate* pCxPrivate); void LobbySendMessage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& message); void LobbySetPlayerPresence(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& presence); void LobbySetNick(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nick); std::wstring LobbyGetNick(ScriptInterface::CxPrivate* pCxPrivate); void LobbyKick(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nick, const std::wstring& reason); void LobbyBan(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nick, const std::wstring& reason); std::wstring LobbyGetPlayerPresence(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nickname); std::wstring LobbyGetPlayerRole(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& nickname); std::wstring LobbyGetRoomSubject(ScriptInterface::CxPrivate* pCxPrivate); // Non-public secure PBKDF2 hash function with salting and 1,337 iterations std::string EncryptPassword(const std::string& password, const std::string& username); // Public hash interface. std::wstring EncryptPassword(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& pass, const std::wstring& user); #endif // CONFIG2_LOBBY } #endif