diff --git a/data/migrations/61.lua b/data/migrations/61.lua new file mode 100644 index 000000000..7e9713165 --- /dev/null +++ b/data/migrations/61.lua @@ -0,0 +1,41 @@ +function onUpdateDatabase() + logger.info("Updating database to version 61 (add player_bans and player_ban_history)") + + db.query([[CREATE TABLE IF NOT EXISTS `player_bans` ( + `player_id` int(11) NOT NULL, + `reason` varchar(255) NOT NULL, + `banned_at` bigint(20) NOT NULL, + `expires_at` bigint(20) NOT NULL, + `banned_by` int(11) NOT NULL, + INDEX `banned_by` (`banned_by`), + CONSTRAINT `player_bans_pk` PRIMARY KEY (`player_id`), + CONSTRAINT `player_bans_players_fk` + FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `player_bans_players2_fk` + FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;]]) + + db.query([[CREATE TABLE IF NOT EXISTS `player_ban_history` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `player_id` int(11) NOT NULL, + `reason` varchar(255) NOT NULL, + `banned_at` bigint(20) NOT NULL, + `expired_at` bigint(20) NOT NULL, + `banned_by` int(11) NOT NULL, + INDEX `player_id` (`player_id`), + INDEX `banned_by` (`banned_by`), + CONSTRAINT `player_ban_history_pk` PRIMARY KEY (`id`), + CONSTRAINT `player_ban_history_players_fk` + FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `player_ban_history_players2_fk` + FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;]]) +end diff --git a/data/scripts/globalevents/server_initialization.lua b/data/scripts/globalevents/server_initialization.lua index e85a2c9b9..3a2b97927 100644 --- a/data/scripts/globalevents/server_initialization.lua +++ b/data/scripts/globalevents/server_initialization.lua @@ -31,6 +31,17 @@ local function moveExpiredBansToHistory() Result.free(resultId) end + + resultId = db.storeQuery("SELECT * FROM `player_bans` WHERE `expires_at` != 0 AND `expires_at` <= " .. os.time()) + if resultId then + repeat + local playerId = Result.getNumber(resultId, "player_id") + db.asyncQuery("INSERT INTO `player_ban_history` (`player_id`, `reason`, `banned_at`, `expired_at`, `banned_by`) VALUES (" .. playerId .. ", " .. db.escapeString(Result.getString(resultId, "reason")) .. ", " .. Result.getNumber(resultId, "banned_at") .. ", " .. Result.getNumber(resultId, "expires_at") .. ", " .. Result.getNumber(resultId, "banned_by") .. ")") + db.asyncQuery("DELETE FROM `player_bans` WHERE `player_id` = " .. playerId) + until not Result.next(resultId) + + Result.free(resultId) + end end -- Function to store towns in the database diff --git a/data/scripts/talkactions/gm/player_ban.lua b/data/scripts/talkactions/gm/player_ban.lua new file mode 100644 index 000000000..65f597d50 --- /dev/null +++ b/data/scripts/talkactions/gm/player_ban.lua @@ -0,0 +1,55 @@ +local playerBan = TalkAction("/playerban") + +function playerBan.onSay(player, words, param) + -- create log + logCommand(player, words, param) + + local params = param:split(",") + if #params < 3 then + player:sendCancelMessage("Command requires 3 parameters: /playerban , , ") + return true + end + + local playerName = params[1]:trim() + local banDuration = tonumber(params[2]:trim()) + local banReason = params[3]:trim() + + if not banDuration or banDuration <= 0 then + player:sendCancelMessage("Ban duration must be a positive number.") + return true + end + + local resultId = db.storeQuery("SELECT `id` FROM `players` WHERE `name` = " .. db.escapeString(playerName)) + if resultId == false then + player:sendCancelMessage("Player not found.") + return true + end + + local targetGuid = Result.getNumber(resultId, "id") + Result.free(resultId) + + resultId = db.storeQuery("SELECT 1 FROM `player_bans` WHERE `player_id` = " .. targetGuid) + if resultId ~= false then + player:sendTextMessage(MESSAGE_ADMINISTRATOR, playerName .. " is already banned.") + Result.free(resultId) + return true + end + + local currentTime = os.time() + local expirationTime = currentTime + (banDuration * 24 * 60 * 60) + db.query(string.format("INSERT INTO `player_bans` (`player_id`, `reason`, `banned_at`, `expires_at`, `banned_by`) VALUES (%d, %s, %d, %d, %d)", targetGuid, db.escapeString(banReason), currentTime, expirationTime, player:getGuid())) + + local target = Player(playerName) + if target then + player:sendTextMessage(MESSAGE_ADMINISTRATOR, string.format("Character %s has been banned for %d days.", target:getName(), banDuration)) + target:remove() + Webhook.sendMessage("Player Banned", string.format("Character %s has been banned for %d days. Reason: %s (by: %s)", target:getName(), banDuration, banReason, player:getName()), WEBHOOK_COLOR_YELLOW, announcementChannels["serverAnnouncements"]) + else + player:sendTextMessage(MESSAGE_ADMINISTRATOR, string.format("Character %s has been banned for %d days.", playerName, banDuration)) + end + return true +end + +playerBan:separator(" ") +playerBan:groupType("gamemaster") +playerBan:register() diff --git a/data/scripts/talkactions/gm/unban.lua b/data/scripts/talkactions/gm/unban.lua index 3d1caba8b..0a680d01c 100644 --- a/data/scripts/talkactions/gm/unban.lua +++ b/data/scripts/talkactions/gm/unban.lua @@ -9,13 +9,14 @@ function unban.onSay(player, words, param) return true end - local resultId = db.storeQuery("SELECT `account_id`, `lastip` FROM `players` WHERE `name` = " .. db.escapeString(param)) + local resultId = db.storeQuery("SELECT `id`, `account_id`, `lastip` FROM `players` WHERE `name` = " .. db.escapeString(param)) if resultId == false then return true end db.asyncQuery("DELETE FROM `account_bans` WHERE `account_id` = " .. Result.getNumber(resultId, "account_id")) db.asyncQuery("DELETE FROM `ip_bans` WHERE `ip` = " .. Result.getNumber(resultId, "lastip")) + db.asyncQuery("DELETE FROM `player_bans` WHERE `player_id` = " .. Result.getNumber(resultId, "id")) Result.free(resultId) local text = param .. " has been unbanned." player:sendTextMessage(MESSAGE_ADMINISTRATOR, text) diff --git a/schema.sql b/schema.sql index 505cfb1ee..2567e41bb 100644 --- a/schema.sql +++ b/schema.sql @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS `server_config` ( CONSTRAINT `server_config_pk` PRIMARY KEY (`config`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '60'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); +INSERT INTO `server_config` (`config`, `value`) VALUES ('db_version', '61'), ('motd_hash', ''), ('motd_num', '0'), ('players_record', '0'); -- Table structure `accounts` CREATE TABLE IF NOT EXISTS `accounts` ( @@ -599,6 +599,46 @@ CREATE TABLE IF NOT EXISTS `player_statements` ( FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- Table structure `player_bans` +CREATE TABLE IF NOT EXISTS `player_bans` ( + `player_id` int(11) NOT NULL, + `reason` varchar(255) NOT NULL, + `banned_at` bigint(20) NOT NULL, + `expires_at` bigint(20) NOT NULL, + `banned_by` int(11) NOT NULL, + INDEX `banned_by` (`banned_by`), + CONSTRAINT `player_bans_pk` PRIMARY KEY (`player_id`), + CONSTRAINT `player_bans_players_fk` + FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `player_bans_players2_fk` + FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Table structure `player_ban_history` +CREATE TABLE IF NOT EXISTS `player_ban_history` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `player_id` int(11) NOT NULL, + `reason` varchar(255) NOT NULL, + `banned_at` bigint(20) NOT NULL, + `expired_at` bigint(20) NOT NULL, + `banned_by` int(11) NOT NULL, + INDEX `player_id` (`player_id`), + INDEX `banned_by` (`banned_by`), + CONSTRAINT `player_ban_history_pk` PRIMARY KEY (`id`), + CONSTRAINT `player_ban_history_players_fk` + FOREIGN KEY (`player_id`) REFERENCES `players` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE, + CONSTRAINT `player_ban_history_players2_fk` + FOREIGN KEY (`banned_by`) REFERENCES `players` (`id`) + ON DELETE CASCADE + ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- Table structure `player_deaths` CREATE TABLE IF NOT EXISTS `player_deaths` ( `player_id` int(11) NOT NULL, diff --git a/src/creatures/players/management/ban.cpp b/src/creatures/players/management/ban.cpp index 4ef3a955d..6e8ffd604 100644 --- a/src/creatures/players/management/ban.cpp +++ b/src/creatures/players/management/ban.cpp @@ -113,6 +113,36 @@ bool IOBan::isIpBanned(uint32_t clientIP, BanInfo &banInfo) { return true; } +bool IOBan::isPlayerBanned(uint32_t playerId, BanInfo &banInfo) { + Database &db = Database::getInstance(); + + std::ostringstream query; + query << "SELECT `reason`, `expires_at`, `banned_at`, `banned_by`, (SELECT `name` FROM `players` WHERE `id` = `banned_by`) AS `name` FROM `player_bans` WHERE `player_id` = " << playerId; + + const DBResult_ptr result = db.storeQuery(query.str()); + if (!result) { + return false; + } + + const auto expiresAt = result->getNumber("expires_at"); + if (expiresAt != 0 && time(nullptr) > expiresAt) { + // Move the ban to history if it has expired + query.str(std::string()); + query << "INSERT INTO `player_ban_history` (`player_id`, `reason`, `banned_at`, `expired_at`, `banned_by`) VALUES (" << playerId << ',' << db.escapeString(result->getString("reason")) << ',' << result->getNumber("banned_at") << ',' << expiresAt << ',' << result->getNumber("banned_by") << ')'; + g_databaseTasks().execute(query.str()); + + query.str(std::string()); + query << "DELETE FROM `player_bans` WHERE `player_id` = " << playerId; + g_databaseTasks().execute(query.str()); + return false; + } + + banInfo.expiresAt = expiresAt; + banInfo.reason = result->getString("reason"); + banInfo.bannedBy = result->getString("name"); + return true; +} + bool IOBan::isPlayerNamelocked(uint32_t playerId) { std::ostringstream query; query << "SELECT 1 FROM `player_namelocks` WHERE `player_id` = " << playerId; diff --git a/src/creatures/players/management/ban.hpp b/src/creatures/players/management/ban.hpp index 35c9c4126..feda4dccf 100644 --- a/src/creatures/players/management/ban.hpp +++ b/src/creatures/players/management/ban.hpp @@ -47,5 +47,6 @@ class IOBan { public: static bool isAccountBanned(uint32_t accountId, BanInfo &banInfo); static bool isIpBanned(uint32_t clientIP, BanInfo &banInfo); + static bool isPlayerBanned(uint32_t playerId, BanInfo &banInfo); static bool isPlayerNamelocked(uint32_t playerId); }; diff --git a/src/server/network/protocol/protocolgame.cpp b/src/server/network/protocol/protocolgame.cpp index 6e80e18a9..c1269b6d4 100644 --- a/src/server/network/protocol/protocolgame.cpp +++ b/src/server/network/protocol/protocolgame.cpp @@ -676,6 +676,24 @@ void ProtocolGame::login(const std::string &name, uint32_t accountId, OperatingS disconnectClient(ss.str()); return; } + + if (IOBan::isPlayerBanned(player->getGUID(), banInfo)) { + if (banInfo.reason.empty()) { + banInfo.reason = "(none)"; + } + + std::ostringstream ss; + if (banInfo.expiresAt > 0) { + ss << "Your character has been banned until " << formatDateShort(banInfo.expiresAt) << " by " << banInfo.bannedBy << ".\n\nReason specified:\n" + << banInfo.reason; + } else { + ss << "Your character has been permanently banned by " << banInfo.bannedBy << ".\n\nReason specified:\n" + << banInfo.reason; + } + + disconnectClient(ss.str()); + return; + } } WaitingList &waitingList = WaitingList::getInstance();