From a86c28e50ed66043f1a01db08730aac5455f7624 Mon Sep 17 00:00:00 2001 From: meowcer Date: Thu, 18 Jun 2026 17:38:56 -0300 Subject: [PATCH 1/4] Add getTimeFlagValue and sanitize_username Functions used in backend/websockets/chat.js --- backend/utils/utils.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/utils/utils.js b/backend/utils/utils.js index 6f939740..d4a188b3 100644 --- a/backend/utils/utils.js +++ b/backend/utils/utils.js @@ -820,6 +820,26 @@ function checkURLParam(mask, url) { return values; } +function getTimeFlagValue(flag) { // used in chat mutes + var timeSuffixMap = { + "h": 3600, + "d": 86400, + "w": 86400*7, + "m": 86400*30, + "y": 31556925.216 //average year length + }; + + return timeSuffixMap[flag]; +} + +function sanitize_username(username) { // used in chat blocking and muting + var validUsernameRegex = /^[^\s\x00-\x20]+$/; + if(typeof username != "string" || !username || !validUsernameRegex.test(username)) return null; + + // case-insensitive + return username.toUpperCase(); +} + module.exports = { trimHTML, create_date, @@ -852,4 +872,6 @@ module.exports = { checkDuplicateCookie, toHex64, toInt64, -}; \ No newline at end of file + getTimeFlagValue, + sanitize_username +}; From 0eafd9aa881dfe81c6b6b4ddfb2d4a80d8d4e88a Mon Sep 17 00:00:00 2001 From: meowcer Date: Thu, 18 Jun 2026 17:41:28 -0300 Subject: [PATCH 2/4] Add /muteuser command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mutes by username (local account username or uvias display name) instead of IP watch this work first try 🙏 (i'll test don't worry there's a good chance i missed stuff) --- backend/websockets/chat.js | 125 +++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 39 deletions(-) diff --git a/backend/websockets/chat.js b/backend/websockets/chat.js index 57bf442e..b94fac82 100644 --- a/backend/websockets/chat.js +++ b/backend/websockets/chat.js @@ -3,6 +3,8 @@ var html_tag_esc = utils.html_tag_esc; var san_nbr = utils.san_nbr; var calculateTimeDiff = utils.calculateTimeDiff; var create_date = utils.create_date; +var getTimeFlagValue = utils.getTimeFlagValue; +var sanitize_username = utils.sanitize_username; function sanitizeColor(col) { var masks = ["#XXXXXX", "#XXX"]; @@ -55,6 +57,7 @@ function sanitizeCustomMeta(meta) { var chat_ip_limits = {}; var tell_blocks = {}; var blocked_ips_by_world_id = {}; // id 0 = global +var blocked_users_by_world_id = {}; module.exports = async function(ws, data, send, broadcast, server, ctx) { var channel = ctx.channel; @@ -169,22 +172,32 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { var isTestMessage = false; var muteInfo = null; var worldChatMutes = blocked_ips_by_world_id[world.id]; + var worldChatUserMutes = blocked_users_by_world_id[world.id]; if(location == "global") { worldChatMutes = blocked_ips_by_world_id[0]; + worldChatUserMutes = blocked_users_by_world_id[0]; } if(worldChatMutes) { - muteInfo = worldChatMutes[ipHeaderAddr]; - if(muteInfo) { + muteInfo = { + id: worldChatMutes[ipHeaderAddr], + user: worldChatUserMutes[username_to_display] + }; + if(muteInfo.id || muteInfo.user) { isMuted = true; } } if(isMuted) { - var expTime = muteInfo[0]; - if(!expTime || typeof expTime != "number" || Date.now() >= expTime) { - isMuted = false; + var expIdTime = muteInfo.id; + var expUserTime = muteInfo.user; + if(!expIdTime || typeof expIdTime != "number" || Date.now() >= expIdTime) { + isMuted = muteInfo.user; // can still be muted by username delete worldChatMutes[ipHeaderAddr]; } + if(!expUserTime || typeof expUserTime != "number" || Date.now() >= expUserTime) { + isMuted = muteInfo.id; // can still be muted by id + delete worldChatUserMutes[username_to_display]; + } } var nick = ""; @@ -246,6 +259,7 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { [0, "unblockuser", ["username"], "unblock someone by username", "JohnDoe"], [0, "unblockall", null, "unblock all users", null], [0, "mute", ["id", "seconds", "[h/d/w/m/y]"], "mute a user completely", "1220 9999"], // check for permission + [0, "muteuser", ["username", "seconds", "[h/d/w/m/y]"], "mute a user by their username completely", "JohnDoe 9999"], // check for permission [0, "clearmutes", null, "unmute all clients"], // check for permission [0, "delete", ["id", "timestamp"], "delete a chat message", "1220 1693147307895"], // check for permission [0, "tell", ["id", "message"], "tell someone a secret message", "1220 The coordinates are (392, 392)"], @@ -282,7 +296,7 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { var desc = row[3]; var example = row[4]; - if(command == "mute" || command == "clearmutes" || command == "delete") { + if(command == "mute" || command == "muteuser" || command == "clearmutes" || command == "delete") { if(!user.staff && !is_owner) { continue; } @@ -391,16 +405,13 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { }, blockuser: function(username) { var blocks = ws.sdata.chat_blocks; - if(typeof username != "string" || !username) { + + var username_value = sanitize_username(username); + if (username_value == null) { serverChatResponse("Invalid username", location); return; } - if (!/^[^\s\x00-\x20]+$/.test(username)) return; - - // The case-insensitive value to be stored in chat_blocks. - var username_value = username.toUpperCase(); - // Ensure maximum block count not exceeded, and check if it already exists. if ((blocks.id.length + blocks.user.length) >= chatBlockLimit) return serverChatResponse("Too many blocked IDs/users", location); @@ -445,14 +456,13 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { }, unblockuser: function(username) { var blocks = ws.sdata.chat_blocks; - if(typeof username != "string" || !username) { + + var username_value = sanitize_username(username); + if (username_value == null) { serverChatResponse("Invalid username", location); return; } - // The case-insensitive value to be stored in chat_blocks. - var username_value = username.toUpperCase(); - var idx = blocks.user.indexOf(username_value); if(idx == -1) return; blocks.user.splice(idx, 1); @@ -615,27 +625,18 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { }, mute: function(id, time, flag) { if(!is_owner && !user.staff) return; + if(location == "global" && !user.staff) { + return serverChatResponse("You do not have permission to mute on global", location); + } + id = san_nbr(id); time = san_nbr(time); // in seconds - var timeSuffixMap = { - "h": 3600, - "d": 86400, - "w": 86400*7, - "m": 86400*30, - "y": 31556925.216 //average year length - }; - - if(flag in timeSuffixMap) { - time *= timeSuffixMap[flag]; - } else { - if(flag) { //invalid flag - return serverChatResponse("Invalid flag used for muting, must be h, d, w, m, or y.") - } - } - - if(location == "global" && !user.staff) { - return serverChatResponse("You do not have permission to mute on global", location); + var timeMultiplier = getTimeFlagValue(flag); + if (timeMultiplier == null) { + if (flag) return serverChatResponse("Invalid flag used for muting, must be h, d, w, m, or y."); + } else { + time *= timeMultiplier; } var muted_ip = getClientIPByChatID(id, location == "global"); @@ -653,26 +654,69 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { } if(!blocked_ips_by_world_id[mute_wid]) blocked_ips_by_world_id[mute_wid] = {}; blocked_ips_by_world_id[mute_wid][muted_ip] = [muteDate]; - return serverChatResponse("Muted client until " + create_date(muteDate), location); + return serverChatResponse("Muted client by IP until " + create_date(muteDate), location); } else { return serverChatResponse("Client not found", location); } }, + muteuser: function(username, time, flag) { + if(!is_owner && !user.staff) return; + if(location == "global" && !user.staff) { + return serverChatResponse("You do not have permission to mute on global", location); + } + + var username_value = sanitize_username(username); + if (username_value == null) { + serverChatResponse("Invalid username", location); + return; + } + + time = san_nbr(time); // in seconds + var timeMultiplier = getTimeFlagValue(flag); + if (timeMultiplier == null) { + if (flag) return serverChatResponse("Invalid flag used for muting, must be h, d, w, m, or y."); + } else { + time *= timeMultiplier; + } + + var muteDate = Date.now() + (time * 1000); + var mute_wid = null; + if(location == "global") { + mute_wid = 0; + } else if(location == "page") { + mute_wid = world.id; + } + if(mute_wid == null) { + return serverChatResponse("Invalid location", location); + } + if(!blocked_users_by_world_id[mute_wid]) blocked_users_by_world_id[mute_wid] = {}; + blocked_users_by_world_id[mute_wid][muted_ip] = [muteDate]; + return serverChatResponse("Muted client by username until " + create_date(muteDate), location); + }, clearmutes: function() { if(!is_owner && !user.staff) return; - var cnt = 0; + var ipCnt = 0; + var userCnt = 0; if(location == "global" && user.staff) { if(blocked_ips_by_world_id["0"]) { - cnt = Object.keys(blocked_ips_by_world_id["0"]).length; + ipCnt = Object.keys(blocked_ips_by_world_id["0"]).length; delete blocked_ips_by_world_id["0"]; } + if(blocked_users_by_world_id["0"]) { + userCnt = Object.keys(blocked_users_by_world_id["0"]).length; + delete blocked_users_by_world_id["0"]; + } } else { if(blocked_ips_by_world_id[world.id]) { - cnt = Object.keys(blocked_ips_by_world_id[world.id]).length; + ipCnt = Object.keys(blocked_ips_by_world_id[world.id]).length; delete blocked_ips_by_world_id[world.id]; } + if(blocked_users_by_world_id[world.id]) { + userCnt = Object.keys(blocked_users_by_world_id[world.id]).length; + delete blocked_users_by_world_id[world.id]; + } } - return serverChatResponse("Unmuted " + cnt + " user(s)", location); + return serverChatResponse("Unmuted " + ipCnt + " IP(s), " + userCnt + " user(s)", location); }, whoami: function() { var idstr = "Who Am I:\n"; @@ -799,6 +843,9 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { case "mute": com.mute(commandArgs[1], commandArgs[2], commandArgs[3]); return; + case "muteuser": + com.muteuser(commandArgs[1], commandArgs[2], commandArgs[3]); + return; case "clearmutes": com.clearmutes(); return; From d5ad8a563511218a0d4799a2ac625d09a6ab4853 Mon Sep 17 00:00:00 2001 From: meowcer Date: Thu, 18 Jun 2026 17:51:23 -0300 Subject: [PATCH 3/4] well oops fixed that let me go test some more --- backend/websockets/chat.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/websockets/chat.js b/backend/websockets/chat.js index b94fac82..f1b08fb4 100644 --- a/backend/websockets/chat.js +++ b/backend/websockets/chat.js @@ -933,8 +933,10 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { } if(isMuted) { - var expTime = muteInfo[0]; - serverChatResponse("You are temporarily muted (" + calculateTimeDiff(expTime - Date.now()) + ")", location); + var expIdTime = muteInfo.id; + var expUserTime = muteInfo.user; + var longestTime = Math.max(muteInfo.id, muteInfo.user); + serverChatResponse("You are temporarily muted (" + calculateTimeDiff(longestTime - Date.now()) + ")", location); return; } var websocketChatData = Object.assign({ From cc7db3f4fe1e413ab232c0220a262737b582febb Mon Sep 17 00:00:00 2001 From: meowcer Date: Thu, 18 Jun 2026 18:20:54 -0300 Subject: [PATCH 4/4] an iq too high? tested in local instance this time, everything should be working and feel free to test it too --- backend/websockets/chat.js | 53 +++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/backend/websockets/chat.js b/backend/websockets/chat.js index f1b08fb4..d809d669 100644 --- a/backend/websockets/chat.js +++ b/backend/websockets/chat.js @@ -170,7 +170,10 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { var isMuted = false; var isTestMessage = false; - var muteInfo = null; + var muteInfo = { + id: null, + user: null + }; var worldChatMutes = blocked_ips_by_world_id[world.id]; var worldChatUserMutes = blocked_users_by_world_id[world.id]; if(location == "global") { @@ -178,25 +181,29 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { worldChatUserMutes = blocked_users_by_world_id[0]; } if(worldChatMutes) { - muteInfo = { - id: worldChatMutes[ipHeaderAddr], - user: worldChatUserMutes[username_to_display] - }; - if(muteInfo.id || muteInfo.user) { - isMuted = true; - } + muteInfo.id = worldChatMutes[ipHeaderAddr]; + } + if(worldChatUserMutes) { + muteInfo.user = worldChatUserMutes[username_to_display]; + } + if(muteInfo.id || muteInfo.user) { + isMuted = true; } if(isMuted) { - var expIdTime = muteInfo.id; - var expUserTime = muteInfo.user; - if(!expIdTime || typeof expIdTime != "number" || Date.now() >= expIdTime) { - isMuted = muteInfo.user; // can still be muted by username - delete worldChatMutes[ipHeaderAddr]; + if (muteInfo.id) { + var expIdTime = muteInfo.id[0]; + if(!expIdTime || typeof expIdTime != "number" || Date.now() >= expIdTime) { + isMuted = Boolean(muteInfo.user); // can still be muted by username + delete worldChatMutes[ipHeaderAddr]; + } } - if(!expUserTime || typeof expUserTime != "number" || Date.now() >= expUserTime) { - isMuted = muteInfo.id; // can still be muted by id - delete worldChatUserMutes[username_to_display]; + if (muteInfo.user) { + var expUserTime = muteInfo.user[0]; + if(!expUserTime || typeof expUserTime != "number" || Date.now() >= expUserTime) { + isMuted = Boolean(muteInfo.id); // can still be muted by id + delete worldChatUserMutes[username_to_display]; + } } } @@ -690,7 +697,7 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { return serverChatResponse("Invalid location", location); } if(!blocked_users_by_world_id[mute_wid]) blocked_users_by_world_id[mute_wid] = {}; - blocked_users_by_world_id[mute_wid][muted_ip] = [muteDate]; + blocked_users_by_world_id[mute_wid][username] = [muteDate]; return serverChatResponse("Muted client by username until " + create_date(muteDate), location); }, clearmutes: function() { @@ -933,10 +940,14 @@ module.exports = async function(ws, data, send, broadcast, server, ctx) { } if(isMuted) { - var expIdTime = muteInfo.id; - var expUserTime = muteInfo.user; - var longestTime = Math.max(muteInfo.id, muteInfo.user); - serverChatResponse("You are temporarily muted (" + calculateTimeDiff(longestTime - Date.now()) + ")", location); + var expTime = 0; + if (muteInfo.id) { + expTime = muteInfo.id[0]; + } + if (muteInfo.user) { + expTime = Math.max(expTime, muteInfo.user[0]); + } + serverChatResponse("You are temporarily muted (" + calculateTimeDiff(expTime - Date.now()) + ")", location); return; } var websocketChatData = Object.assign({