Enhanced Ban

This commit is contained in:
linmeideli committed 2026-08-18 00:11:41 +08:00
1 parent 40bbc2027b
commit f8eeeaae8b
23 files changed
+403 -121

No files matched your search

@@ -150,6 +150,7 @@
"System.Runtime.CompilerServices.Unsafe": "4.5.3"
}
}
}
},
".NETStandard,Version=v2.0/win-x64": {}
}
}
+90 -16
View File
@@ -166,7 +166,6 @@ public sealed class LanguageService
"command.note.set": "Note set: {0}",
"command.color.invalid": "Invalid color ID. Valid range: 0–17.",
"command.color.set": "Color changed to {0} ({1}).",
"command.title.too_long": "Title too long. Max 12 characters.",
"command.title.cleared": "Title removed.",
"command.title.set": "Title set: {0}",
@@ -205,7 +204,12 @@ public sealed class LanguageService
"command.note.description": "Set or clear a lobby note (host only).",
"command.note.usage": "note <text> | note clear",
"command.max.description": "Set max players for the lobby (host only).",
"command.max.usage": "max <1-15>"
"command.max.usage": "max <1-15>",
"ban.notice.header": "You are banned from this server.",
"ban.notice.reason": "Reason: {0}",
"ban.notice.unban": "Unban time: {0}",
"ban.notice.unban_permanent": "Unban time: Permanent",
"ban.notice.contact": "If you have any questions, please contact the administrator."
}
""";
@@ -260,7 +264,12 @@ public sealed class LanguageService
"command.note.description": "设置或清除房间备注(仅房主)。",
"command.note.usage": "note <文字> | note clear",
"command.max.description": "设置房间最大人数(仅房主)。",
"command.max.usage": "max <1-15>"
"command.max.usage": "max <1-15>",
"ban.notice.header": "您已被服务器封禁",
"ban.notice.reason": "理由:{0}",
"ban.notice.unban": "解封时间:{0}",
"ban.notice.unban_permanent": "解封时间:永久",
"ban.notice.contact": "如有问题请联系管理员"
}
""";
@@ -315,7 +324,12 @@ public sealed class LanguageService
"command.note.description": "設置或清除房間備註(僅房主)。",
"command.note.usage": "note <文字> | note clear",
"command.max.description": "設置房間最大人數(僅房主)。",
"command.max.usage": "max <1-15>"
"command.max.usage": "max <1-15>",
"ban.notice.header": "您已被伺服器封禁",
"ban.notice.reason": "理由:{0}",
"ban.notice.unban": "解封時間:{0}",
"ban.notice.unban_permanent": "解封時間:永久",
"ban.notice.contact": "如有問題請聯絡管理員"
}
""";
@@ -356,7 +370,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "이 명령어는 로비에서만 사용할 수 있습니다.",
"command.players.header": "=== 플레이어 ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "내 핑: {0}ms"
"command.ping.result": "내 핑: {0}ms",
"ban.notice.header": "이 서버에서 차단되었습니다.",
"ban.notice.reason": "사유: {0}",
"ban.notice.unban": "차단 해제 시간: {0}",
"ban.notice.unban_permanent": "차단 해제 시간: 영구",
"ban.notice.contact": "문의 사항이 있으면 관리자에게 연락해 주세요."
}
""";
@@ -397,7 +416,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Эта команда может использоваться только в лобби.",
"command.players.header": "=== Игроки ({0}) ===",
"command.players.entry": "{0} | {1} | {2}мс",
"command.ping.result": "Ваш пинг: {0}мс"
"command.ping.result": "Ваш пинг: {0}мс",
"ban.notice.header": "Вы забанены на этом сервере.",
"ban.notice.reason": "Причина: {0}",
"ban.notice.unban": "Время разбана: {0}",
"ban.notice.unban_permanent": "Время разбана: навсегда",
"ban.notice.contact": "Если у вас есть вопросы, обратитесь к администратору."
}
""";
@@ -438,7 +462,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Dieser Befehl kann nur in der Lobby verwendet werden.",
"command.players.header": "=== Spieler ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Dein Ping: {0}ms"
"command.ping.result": "Dein Ping: {0}ms",
"ban.notice.header": "Du wurdest von diesem Server gebannt.",
"ban.notice.reason": "Grund: {0}",
"ban.notice.unban": "Entbannungszeit: {0}",
"ban.notice.unban_permanent": "Entbannungszeit: Dauerhaft",
"ban.notice.contact": "Bei Fragen wende dich an den Administrator."
}
""";
@@ -479,7 +508,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Cette commande ne peut être utilisée que dans le lobby.",
"command.players.header": "=== Joueurs ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Votre ping : {0}ms"
"command.ping.result": "Votre ping : {0}ms",
"ban.notice.header": "Vous êtes banni de ce serveur.",
"ban.notice.reason": "Raison : {0}",
"ban.notice.unban": "Débannissement : {0}",
"ban.notice.unban_permanent": "Débannissement : Permanent",
"ban.notice.contact": "En cas de question, contactez un administrateur."
}
""";
@@ -520,7 +554,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "このコマンドはロビーでのみ使用できます。",
"command.players.header": "=== プレイヤー ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "あなたのPing: {0}ms"
"command.ping.result": "あなたのPing: {0}ms",
"ban.notice.header": "このサーバーからBANされています。",
"ban.notice.reason": "理由:{0}",
"ban.notice.unban": "BAN解除日時:{0}",
"ban.notice.unban_permanent": "BAN解除日時:永久",
"ban.notice.contact": "ご不明な点があれば管理者にお問い合わせください。"
}
""";
@@ -561,7 +600,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Este comando só pode ser usado no lobby.",
"command.players.header": "=== Jogadores ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Seu ping: {0}ms"
"command.ping.result": "Seu ping: {0}ms",
"ban.notice.header": "Você foi banido deste servidor.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Desbanimento: {0}",
"ban.notice.unban_permanent": "Desbanimento: Permanente",
"ban.notice.contact": "Em caso de dúvidas, contacte o administrador."
}
""";
@@ -600,7 +644,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Este comando só pode ser usado no lobby.",
"command.players.header": "=== Jogadores ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Seu ping: {0}ms"
"command.ping.result": "Seu ping: {0}ms",
"ban.notice.header": "Você foi banido deste servidor.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Desbanimento: {0}",
"ban.notice.unban_permanent": "Desbanimento: Permanente",
"ban.notice.contact": "Em caso de dúvidas, contate o administrador."
}
""";
@@ -639,7 +688,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Este comando solo puede usarse en el lobby.",
"command.players.header": "=== Jugadores ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Tu ping: {0}ms"
"command.ping.result": "Tu ping: {0}ms",
"ban.notice.header": "Has sido baneado de este servidor.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Fin del baneo: {0}",
"ban.notice.unban_permanent": "Fin del baneo: Permanente",
"ban.notice.contact": "Si tienes dudas, contacta con un administrador."
}
""";
@@ -678,7 +732,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Questo comando può essere usato solo nella lobby.",
"command.players.header": "=== Giocatori ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Il tuo ping: {0}ms"
"command.ping.result": "Il tuo ping: {0}ms",
"ban.notice.header": "Sei stato bannato da questo server.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Sbanno: {0}",
"ban.notice.unban_permanent": "Sbanno: Permanente",
"ban.notice.contact": "Per qualsiasi domanda, contatta l'amministratore."
}
""";
@@ -717,7 +776,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Dit commando kan alleen in de lobby worden gebruikt.",
"command.players.header": "=== Spelers ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Jouw ping: {0}ms"
"command.ping.result": "Jouw ping: {0}ms",
"ban.notice.header": "Je bent geband van deze server.",
"ban.notice.reason": "Reden: {0}",
"ban.notice.unban": "Ontbantijd: {0}",
"ban.notice.unban_permanent": "Ontbantijd: Permanent",
"ban.notice.contact": "Neem bij vragen contact op met de beheerder."
}
""";
@@ -756,7 +820,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Ang command na ito ay maaari lamang gamitin sa lobby.",
"command.players.header": "=== Mga Manlalaro ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Ang iyong ping: {0}ms"
"command.ping.result": "Ang iyong ping: {0}ms",
"ban.notice.header": "Banned ka sa server na ito.",
"ban.notice.reason": "Dahilan: {0}",
"ban.notice.unban": "Oras ng pag-unban: {0}",
"ban.notice.unban_permanent": "Oras ng pag-unban: Permanente",
"ban.notice.contact": "Kung may tanong, makipag-ugnayan sa administrator."
}
""";
@@ -795,7 +864,12 @@ public sealed class LanguageService
"command.players.not_in_lobby": "Ní féidir an t-ordú seo a úsáid ach sa stocaireacht.",
"command.players.header": "=== Imreoirí ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Do phing: {0}ms"
"command.ping.result": "Do phing: {0}ms",
"ban.notice.header": "Tá cosc ort ar an bhfreastalaí seo.",
"ban.notice.reason": "Cúis: {0}",
"ban.notice.unban": "Am dí-chosc: {0}",
"ban.notice.unban_permanent": "Am dí-chosc: Buan",
"ban.notice.contact": "Má tá ceist agat, déan teagmháil le riarthóir."
}
""";
+2 -1
View File
@@ -139,6 +139,7 @@
"NextFast.Hazel.Abstractions": {
"type": "Project"
}
}
},
"net8.0/win-x64": {}
}
}
+4 -4
View File
@@ -487,7 +487,7 @@ namespace Empostor.Server.Http
return BadRequest(Err("Invalid IP"));
}
var entry = _bans.BanIp(ip, req.Reason ?? "Banned by admin");
var entry = _bans.BanIp(ip, req.Reason ?? "Banned by admin", BanStore.ParseDuration(req.Duration));
var kicked = 0;
foreach (var c in _clientManager.Clients.ToList())
{
@@ -521,7 +521,7 @@ namespace Empostor.Server.Http
return BadRequest(Err("FriendCode required"));
}
var entry = _bans.BanFriendCode(req.FriendCode, req.Reason ?? "Banned by admin");
var entry = _bans.BanFriendCode(req.FriendCode, req.Reason ?? "Banned by admin", BanStore.ParseDuration(req.Duration));
var kicked = 0;
foreach (var c in _clientManager.Clients.ToList())
{
@@ -901,9 +901,9 @@ namespace Empostor.Server.Http
public sealed record ClientIdReq(int ClientId, string? Reason = null);
public sealed record BanIpReq(string Ip, string? Reason);
public sealed record BanIpReq(string Ip, string? Reason, string? Duration = null);
public sealed record BanFcReq(string FriendCode, string? Reason);
public sealed record BanFcReq(string FriendCode, string? Reason, string? Duration = null);
public sealed record UnbanReq(string Value);
@@ -998,6 +998,7 @@ internal static class AdminTemplateDefaults
<h3><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94L5.62 21a2 2 0 01-2.83-2.83l7.91-7.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg><span data-i18n="ban.title_ip">Ban IP</span></h3>
<div class="field"><label data-i18n="table.ip">IP</label><input id="bi-v" data-i18n-placeholder="ban.ip_placeholder" placeholder="1.2.3.4"></div>
<div class="field"><label data-i18n="table.reason">Reason</label><input id="bi-r" data-i18n-placeholder="ban.reason_placeholder" placeholder="Optional..."></div>
<div class="field"><label data-i18n="ban.duration">Duration</label><div class="row"><input id="bi-dv" type="number" min="1" value="1" style="flex:0 0 64px"><select id="bi-du" style="flex:1"><option value="h" data-i18n="ban.unit_hours">Hours (1-24)</option><option value="d" data-i18n="ban.unit_days">Days (1-30)</option><option value="mo" data-i18n="ban.unit_months">Months (1-12)</option><option value="y" data-i18n="ban.unit_years">Years (1-10)</option><option value="permanent" selected data-i18n="ban.unit_permanent">Permanent</option></select></div></div>
<button class="bd" onclick="doBanIp()"><span data-i18n="ban.button_ip">Ban IP</span></button>
<div id="bi-msg" class="msg"></div>
</div>
@@ -1005,6 +1006,7 @@ internal static class AdminTemplateDefaults
<h3><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94L5.62 21a2 2 0 01-2.83-2.83l7.91-7.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg><span data-i18n="ban.title_fc">Ban Friend Code</span></h3>
<div class="field"><label data-i18n="table.friend_code">Friend Code</label><input id="bf-v" data-i18n-placeholder="ban.fc_placeholder" placeholder="Name#1234"></div>
<div class="field"><label data-i18n="table.reason">Reason</label><input id="bf-r" data-i18n-placeholder="ban.reason_placeholder" placeholder="Optional..."></div>
<div class="field"><label data-i18n="ban.duration">Duration</label><div class="row"><input id="bf-dv" type="number" min="1" value="1" style="flex:0 0 64px"><select id="bf-du" style="flex:1"><option value="h" data-i18n="ban.unit_hours">Hours (1-24)</option><option value="d" data-i18n="ban.unit_days">Days (1-30)</option><option value="mo" data-i18n="ban.unit_months">Months (1-12)</option><option value="y" data-i18n="ban.unit_years">Years (1-10)</option><option value="permanent" selected data-i18n="ban.unit_permanent">Permanent</option></select></div></div>
<button class="bd" onclick="doBanFc()"><span data-i18n="ban.button_fc">Ban FC</span></button>
<div id="bf-msg" class="msg"></div>
</div>
@@ -1280,7 +1282,8 @@ internal static class AdminTemplateDefaults
document.getElementById('bl-fc').innerHTML = d.friendCodes.length ? d.friendCodes.map(b => bi(b, 'fc')).join('') : '<div class="empty">' + _('ban.none', 'None') + '</div>';
}
function bi(b, t) {
return `<div class="bi"><div><div class="bv">${e(b.value)}</div><div class="br2">${e(b.reason)}</div></div><div class="bt">${new Date(b.bannedAt).toLocaleString()}</div><button class="bsm" style="background:rgba(248,81,73,.15);color:var(--r);border:1px solid rgba(248,81,73,.3)" onclick="doUnban('${t}','${e(b.value)}')">${_('ban.unban', 'Unban')}</button></div>`;
const until = b.bannedUntil ? new Date(b.bannedUntil).toLocaleString() : _('ban.permanent', 'Permanent');
return `<div class="bi"><div><div class="bv">${e(b.value)}</div><div class="br2">${e(b.reason)}</div></div><div class="bt">${new Date(b.bannedAt).toLocaleString()}<br><span style="opacity:.7">${_('ban.until', 'Until')}: ${until}</span></div><button class="bsm" style="background:rgba(248,81,73,.15);color:var(--r);border:1px solid rgba(248,81,73,.3)" onclick="doUnban('${t}','${e(b.value)}')">${_('ban.unban', 'Unban')}</button></div>`;
}
async function fGamesEnd() {
@@ -1503,17 +1506,29 @@ internal static class AdminTemplateDefaults
fKickList();
}
function banDuration(vId, uId) {
const u = document.getElementById(uId).value;
if (u === 'permanent') return 'permanent';
let v = parseInt(document.getElementById(vId).value, 10);
if (isNaN(v) || v < 1) v = 1;
const max = { h: 24, d: 30, mo: 12, y: 10 }[u] || 24;
if (v > max) v = max;
return v + u;
}
async function doBanIp() {
const v = document.getElementById('bi-v').value.trim(), r = document.getElementById('bi-r').value.trim();
if (!v) return msg('bi-msg', false, _('alert.ip_required', 'IP required'));
const { ok, data } = await api('POST', '/api/admin/ban/ip', { ip: v, reason: r });
const duration = banDuration('bi-dv', 'bi-du');
const { ok, data } = await api('POST', '/api/admin/ban/ip', { ip: v, reason: r, duration });
msg('bi-msg', ok, ok ? _('alert.banned', 'Banned {0} ({1} disconnected)').replace('{0}', data.banned).replace('{1}', data.disconnected) : (data.error ?? 'Error'));
}
async function doBanFc() {
const v = document.getElementById('bf-v').value.trim(), r = document.getElementById('bf-r').value.trim();
if (!v) return msg('bf-msg', false, _('alert.fc_required', 'FC required'));
const { ok, data } = await api('POST', '/api/admin/ban/fc', { friendCode: v, reason: r });
const duration = banDuration('bf-dv', 'bf-du');
const { ok, data } = await api('POST', '/api/admin/ban/fc', { friendCode: v, reason: r, duration });
msg('bf-msg', ok, ok ? _('alert.banned', 'Banned {0} ({1} disconnected)').replace('{0}', data.banned).replace('{1}', data.disconnected) : (data.error ?? 'Error'));
}
+7 -2
View File
@@ -34,5 +34,10 @@
"command.players.not_in_lobby": "Dieser Befehl kann nur in der Lobby verwendet werden.",
"command.players.header": "=== Spieler ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Dein Ping: {0}ms"
}
"command.ping.result": "Dein Ping: {0}ms",
"ban.notice.header": "Du wurdest von diesem Server gebannt.",
"ban.notice.reason": "Grund: {0}",
"ban.notice.unban": "Entbannungszeit: {0}",
"ban.notice.unban_permanent": "Entbannungszeit: Dauerhaft",
"ban.notice.contact": "Bei Fragen wende dich an den Administrator."
}
+7 -3
View File
@@ -10,7 +10,6 @@
"command.note.set": "Note set: {0}",
"command.color.invalid": "Invalid color ID. Valid range: 0–17.",
"command.color.set": "Color changed to {0} ({1}).",
"command.title.too_long": "Title too long. Max 12 characters.",
"command.title.cleared": "Title removed.",
"command.title.set": "Title set: {0}",
@@ -66,5 +65,10 @@
"command.ban.cannot_ban_self": "You cannot ban yourself.",
"command.ban.banned": "{0} has been banned. Reason: {1}",
"command.ban.description": "Ban a player by friend code or name. Host only.",
"command.ban.usage_cmd": "ban <player> [reason]"
}
"command.ban.usage_cmd": "ban <player> [reason]",
"ban.notice.header": "You are banned from this server.",
"ban.notice.reason": "Reason: {0}",
"ban.notice.unban": "Unban time: {0}",
"ban.notice.unban_permanent": "Unban time: Permanent",
"ban.notice.contact": "If you have any questions, please contact the administrator."
}
+7 -2
View File
@@ -32,5 +32,10 @@
"command.players.not_in_lobby": "Este comando solo puede usarse en el lobby.",
"command.players.header": "=== Jugadores ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Tu ping: {0}ms"
}
"command.ping.result": "Tu ping: {0}ms",
"ban.notice.header": "Has sido baneado de este servidor.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Fin del baneo: {0}",
"ban.notice.unban_permanent": "Fin del baneo: Permanente",
"ban.notice.contact": "Si tienes dudas, contacta con un administrador."
}
+7 -2
View File
@@ -32,5 +32,10 @@
"command.players.not_in_lobby": "Ang command na ito ay maaari lamang gamitin sa lobby.",
"command.players.header": "=== Mga Manlalaro ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Ang iyong ping: {0}ms"
}
"command.ping.result": "Ang iyong ping: {0}ms",
"ban.notice.header": "Banned ka sa server na ito.",
"ban.notice.reason": "Dahilan: {0}",
"ban.notice.unban": "Oras ng pag-unban: {0}",
"ban.notice.unban_permanent": "Oras ng pag-unban: Permanente",
"ban.notice.contact": "Kung may tanong, makipag-ugnayan sa administrator."
}
+7 -2
View File
@@ -34,5 +34,10 @@
"command.players.not_in_lobby": "Cette commande ne peut être utilisée que dans le lobby.",
"command.players.header": "=== Joueurs ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Votre ping : {0}ms"
}
"command.ping.result": "Votre ping : {0}ms",
"ban.notice.header": "Vous êtes banni de ce serveur.",
"ban.notice.reason": "Raison : {0}",
"ban.notice.unban": "Débannissement : {0}",
"ban.notice.unban_permanent": "Débannissement : Permanent",
"ban.notice.contact": "En cas de question, contactez un administrateur."
}
+7 -2
View File
@@ -32,5 +32,10 @@
"command.players.not_in_lobby": "Ní féidir an t-ordú seo a úsáid ach sa stocaireacht.",
"command.players.header": "=== Imreoirí ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Do phing: {0}ms"
}
"command.ping.result": "Do phing: {0}ms",
"ban.notice.header": "Tá cosc ort ar an bhfreastalaí seo.",
"ban.notice.reason": "Cúis: {0}",
"ban.notice.unban": "Am dí-chosc: {0}",
"ban.notice.unban_permanent": "Am dí-chosc: Buan",
"ban.notice.contact": "Má tá ceist agat, déan teagmháil le riarthóir."
}
+7 -2
View File
@@ -32,5 +32,10 @@
"command.players.not_in_lobby": "Questo comando può essere usato solo nella lobby.",
"command.players.header": "=== Giocatori ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Il tuo ping: {0}ms"
}
"command.ping.result": "Il tuo ping: {0}ms",
"ban.notice.header": "Sei stato bannato da questo server.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Sbanno: {0}",
"ban.notice.unban_permanent": "Sbanno: Permanente",
"ban.notice.contact": "Per qualsiasi domanda, contatta l'amministratore."
}
+7 -2
View File
@@ -34,5 +34,10 @@
"command.players.not_in_lobby": "このコマンドはロビーでのみ使用できます。",
"command.players.header": "=== プレイヤー ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "あなたのPing: {0}ms"
}
"command.ping.result": "あなたのPing: {0}ms",
"ban.notice.header": "このサーバーからBANされています。",
"ban.notice.reason": "理由:{0}",
"ban.notice.unban": "BAN解除日時:{0}",
"ban.notice.unban_permanent": "BAN解除日時:永久",
"ban.notice.contact": "ご不明な点があれば管理者にお問い合わせください。"
}
+7 -2
View File
@@ -34,5 +34,10 @@
"command.players.not_in_lobby": "이 명령어는 로비에서만 사용할 수 있습니다.",
"command.players.header": "=== 플레이어 ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "내 핑: {0}ms"
}
"command.ping.result": "내 핑: {0}ms",
"ban.notice.header": "이 서버에서 차단되었습니다.",
"ban.notice.reason": "사유: {0}",
"ban.notice.unban": "차단 해제 시간: {0}",
"ban.notice.unban_permanent": "차단 해제 시간: 영구",
"ban.notice.contact": "문의 사항이 있으면 관리자에게 연락해 주세요."
}
+7 -2
View File
@@ -32,5 +32,10 @@
"command.players.not_in_lobby": "Dit commando kan alleen in de lobby worden gebruikt.",
"command.players.header": "=== Spelers ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Jouw ping: {0}ms"
}
"command.ping.result": "Jouw ping: {0}ms",
"ban.notice.header": "Je bent geband van deze server.",
"ban.notice.reason": "Reden: {0}",
"ban.notice.unban": "Ontbantijd: {0}",
"ban.notice.unban_permanent": "Ontbantijd: Permanent",
"ban.notice.contact": "Neem bij vragen contact op met de beheerder."
}
+7 -2
View File
@@ -34,5 +34,10 @@
"command.players.not_in_lobby": "Este comando só pode ser usado no lobby.",
"command.players.header": "=== Jogadores ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Seu ping: {0}ms"
}
"command.ping.result": "Seu ping: {0}ms",
"ban.notice.header": "Você foi banido deste servidor.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Desbanimento: {0}",
"ban.notice.unban_permanent": "Desbanimento: Permanente",
"ban.notice.contact": "Em caso de dúvidas, contacte o administrador."
}
+7 -2
View File
@@ -32,5 +32,10 @@
"command.players.not_in_lobby": "Este comando só pode ser usado no lobby.",
"command.players.header": "=== Jogadores ({0}) ===",
"command.players.entry": "{0} | {1} | {2}ms",
"command.ping.result": "Seu ping: {0}ms"
}
"command.ping.result": "Seu ping: {0}ms",
"ban.notice.header": "Você foi banido deste servidor.",
"ban.notice.reason": "Motivo: {0}",
"ban.notice.unban": "Desbanimento: {0}",
"ban.notice.unban_permanent": "Desbanimento: Permanente",
"ban.notice.contact": "Em caso de dúvidas, contate o administrador."
}
+7 -2
View File
@@ -34,5 +34,10 @@
"command.players.not_in_lobby": "Эта команда может использоваться только в лобби.",
"command.players.header": "=== Игроки ({0}) ===",
"command.players.entry": "{0} | {1} | {2}мс",
"command.ping.result": "Ваш пинг: {0}мс"
}
"command.ping.result": "Ваш пинг: {0}мс",
"ban.notice.header": "Вы забанены на этом сервере.",
"ban.notice.reason": "Причина: {0}",
"ban.notice.unban": "Время разбана: {0}",
"ban.notice.unban_permanent": "Время разбана: навсегда",
"ban.notice.contact": "Если у вас есть вопросы, обратитесь к администратору."
}
+7 -2
View File
@@ -65,5 +65,10 @@
"command.ban.cannot_ban_self": "你不能封禁自己。",
"command.ban.banned": "{0} 已被封禁。原因:{1}",
"command.ban.description": "通过好友代码或名称封禁玩家(仅房主)。",
"command.ban.usage_cmd": "ban <玩家> [原因]"
}
"command.ban.usage_cmd": "ban <玩家> [原因]",
"ban.notice.header": "您已被服务器封禁",
"ban.notice.reason": "理由:{0}",
"ban.notice.unban": "解封时间:{0}",
"ban.notice.unban_permanent": "解封时间:永久",
"ban.notice.contact": "如有问题请联系管理员"
}
+7 -2
View File
@@ -48,5 +48,10 @@
"command.note.description": "設置或清除房間備註(僅房主)。",
"command.note.usage": "note <文字> | note clear",
"command.max.description": "設置房間最大人數(僅房主)。",
"command.max.usage": "max <1-15>"
}
"command.max.usage": "max <1-15>",
"ban.notice.header": "您已被伺服器封禁",
"ban.notice.reason": "理由:{0}",
"ban.notice.unban": "解封時間:{0}",
"ban.notice.unban_permanent": "解封時間:永久",
"ban.notice.contact": "如有問題請聯絡管理員"
}
+8 -45
View File
@@ -27,9 +27,6 @@
"nav.extend": "Extend",
"nav.system": "System",
"nav.player_logs": "Player Logs",
"nav.statistics": "Statistics",
"nav.chat_filter": "Chat Filter",
"nav.discord": "Discord",
"nav.hplp": "HPLP",
"nav.reload_lang": "Reload Lang",
@@ -85,6 +82,14 @@
"ban.banned_fcs": "Banned Friend Codes",
"ban.none": "None",
"ban.unban": "Unban",
"ban.duration": "Duration",
"ban.unit_hours": "Hours (1-24)",
"ban.unit_days": "Days (1-30)",
"ban.unit_months": "Months (1-12)",
"ban.unit_years": "Years (1-10)",
"ban.unit_permanent": "Permanent",
"ban.permanent": "Permanent",
"ban.until": "Until",
"message.title": "Message to Game",
"message.code_placeholder": "ABCDEF",
@@ -185,48 +190,6 @@
"table.player": "Player",
"table.detail": "Detail",
"stats.title": "Player Statistics",
"stats.refresh": "Refresh",
"stats.reset": "Reset All",
"stats.rank": "#",
"stats.games": "Games",
"stats.wins": "Wins",
"stats.losses": "Losses",
"stats.impostor_wins": "Imp. Wins",
"stats.kills": "Kills",
"stats.deaths": "Deaths",
"stats.tasks": "Tasks",
"stats.exiled": "Exiled",
"stats.empty": "Statistics not enabled in config.",
"stats.no_players": "No player stats recorded yet.",
"stats.confirm_reset": "Reset all player statistics? This cannot be undone.",
"chatfilter.title": "Chat Filter",
"chatfilter.enabled": "Enable Filtering",
"chatfilter.block_message": "Block messages (uncheck = log only)",
"chatfilter.words_title": "Blocked Words",
"chatfilter.words_placeholder": "Add a word...",
"chatfilter.add_word": "Add",
"chatfilter.no_words": "No blocked words added.",
"chatfilter.spam_title": "Spam Protection",
"chatfilter.threshold": "Message threshold",
"chatfilter.window": "Window (seconds)",
"chatfilter.save": "Save Settings",
"chatfilter.saved": "Settings saved.",
"chatfilter.word_added": "Word added.",
"chatfilter.word_removed": "Word removed.",
"discord.title": "Discord Webhook",
"discord.save": "Save Settings",
"discord.matchmaker_url": "Matchmaker Webhook URL",
"discord.matchmaker_url_placeholder": "https://discord.com/api/webhooks/...",
"discord.matchmaker_desc": "Game Created, Game Started, Game Ended, Game Destroyed",
"discord.admin_url": "Admin Webhook URL",
"discord.admin_url_placeholder": "https://discord.com/api/webhooks/...",
"discord.admin_desc": "Player Banned, Player Reported",
"discord.hint": "Leave a URL empty to disable its notifications. Leave both empty to disable all webhooks.",
"discord.saved": "Discord webhook settings saved.",
"hplp.title": "HPLP (Public Lobby List)",
"hplp.save": "Save Settings",
"hplp.enabled": "Enable HPLP endpoint (GET /x-api/games)",
@@ -1,6 +1,8 @@
using System;
using System.Threading.Tasks;
using Empostor.Api.Events;
using Empostor.Api.Events.Client;
using Empostor.Api.Languages;
using Microsoft.Extensions.Logging;
namespace Empostor.Server.Service.Admin.Ban;
@@ -9,11 +11,13 @@ public sealed class BanEnforcementListener : IEventListener
{
private readonly ILogger<BanEnforcementListener> _logger;
private readonly BanStore _bans;
private readonly LanguageService _language;
public BanEnforcementListener(ILogger<BanEnforcementListener> logger, BanStore bans)
public BanEnforcementListener(ILogger<BanEnforcementListener> logger, BanStore bans, LanguageService language)
{
_logger = logger;
_bans = bans;
_language = language;
}
[EventListener]
@@ -23,18 +27,34 @@ public sealed class BanEnforcementListener : IEventListener
var ip = client.Connection?.EndPoint?.Address;
// IP 封禁检查
if (ip != null && _bans.IsIpBanned(ip))
if (ip != null && _bans.GetIpBan(ip) is { } ipBan)
{
_logger.LogWarning("BanRejecting banned IP {Ip} ({Name})", ip, client.Name);
await client.DisconnectAsync(DisconnectReason.Banned, "You are banned from this server.");
await client.DisconnectAsync(DisconnectReason.Custom, BuildMessage(client.Language, ipBan.Reason, ipBan.BannedUntil));
return;
}
// FriendCode 封禁检查
if (_bans.IsFriendCodeBanned(client.FriendCode))
if (_bans.GetFriendCodeBan(client.FriendCode) is { } fcBan)
{
_logger.LogWarning("BanRejecting banned FriendCode {FC} ({Name})", client.FriendCode, client.Name);
await client.DisconnectAsync(DisconnectReason.Banned, "You are banned from this server.");
await client.DisconnectAsync(DisconnectReason.Custom, BuildMessage(client.Language, fcBan.Reason, fcBan.BannedUntil));
}
}
private string BuildMessage(Language language, string reason, DateTime? bannedUntil)
{
var header = _language.Get("ban.notice.header", language).Get();
var reasonLine = _language.Get("ban.notice.reason", language)
.Format(string.IsNullOrWhiteSpace(reason) ? "-" : reason).Get();
var unbanLine = bannedUntil.HasValue
? _language.Get("ban.notice.unban", language).Format(FormatExpiry(bannedUntil.Value)).Get()
: _language.Get("ban.notice.unban_permanent", language).Get();
var contactLine = _language.Get("ban.notice.contact", language).Get();
return $"{header}\n{reasonLine}\n{unbanLine}\n{contactLine}";
}
private static string FormatExpiry(DateTime utc)
=> utc.ToLocalTime().ToString("yyyy-MM-dd HH:mm");
}
+149 -15
View File
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Empostor.Api.Service;
using Microsoft.Extensions.Logging;
@@ -20,14 +21,49 @@ namespace Empostor.Server.Service.Admin.Ban
Load();
}
public bool IsIpBanned(IPAddress ip) => _ips.ContainsKey(Normalize(ip));
public bool IsIpBanned(IPAddress ip) => GetIpBan(ip) != null;
public bool IsFriendCodeBanned(string? fc) => fc != null && _friendCodes.ContainsKey(fc);
public bool IsFriendCodeBanned(string? fc) => GetFriendCodeBan(fc) != null;
public BanEntry BanIp(IPAddress ip, string reason)
public BanEntry? GetIpBan(IPAddress ip)
{
var key = Normalize(ip);
var entry = new BanEntry { Value = key, Reason = reason, BannedAt = DateTime.UtcNow };
if (!_ips.TryGetValue(key, out var entry))
{
return null;
}
if (!entry.IsExpired)
{
return entry;
}
_ips.TryRemove(key, out _);
SaveFireAndForget();
return null;
}
public BanEntry? GetFriendCodeBan(string? fc)
{
if (fc == null || !_friendCodes.TryGetValue(fc, out var entry))
{
return null;
}
if (!entry.IsExpired)
{
return entry;
}
_friendCodes.TryRemove(fc, out _);
SaveFireAndForget();
return null;
}
public BanEntry BanIp(IPAddress ip, string reason, DateTime? bannedUntil = null)
{
var key = Normalize(ip);
var entry = new BanEntry { Value = key, Reason = reason, BannedAt = DateTime.UtcNow, BannedUntil = bannedUntil };
_ips[key] = entry;
SaveFireAndForget();
return entry;
@@ -44,9 +80,9 @@ namespace Empostor.Server.Service.Admin.Ban
return r;
}
public BanEntry BanFriendCode(string fc, string reason)
public BanEntry BanFriendCode(string fc, string reason, DateTime? bannedUntil = null)
{
var entry = new BanEntry { Value = fc, Reason = reason, BannedAt = DateTime.UtcNow };
var entry = new BanEntry { Value = fc, Reason = reason, BannedAt = DateTime.UtcNow, BannedUntil = bannedUntil };
_friendCodes[fc] = entry;
SaveFireAndForget();
return entry;
@@ -64,18 +100,85 @@ namespace Empostor.Server.Service.Admin.Ban
}
public IReadOnlyList<BanEntry> AllIpBans()
=> _ips.Values.OrderByDescending(b => b.BannedAt).ToList();
{
if (PruneExpired())
{
SaveFireAndForget();
}
return _ips.Values.OrderByDescending(b => b.BannedAt).ToList();
}
public IReadOnlyList<BanEntry> AllFriendCodeBans()
=> _friendCodes.Values.OrderByDescending(b => b.BannedAt).ToList();
public (int IpCount, int FcCount) Stats() => (_ips.Count, _friendCodes.Count);
protected override BanData GetSnapshot() => new()
{
Ips = new(_ips),
FriendCodes = new(_friendCodes),
};
if (PruneExpired())
{
SaveFireAndForget();
}
return _friendCodes.Values.OrderByDescending(b => b.BannedAt).ToList();
}
public (int IpCount, int FcCount) Stats()
{
if (PruneExpired())
{
SaveFireAndForget();
}
return (_ips.Count, _friendCodes.Count);
}
/// <summary>
/// Parses a ban duration string into a UTC expiry timestamp.
/// Supported formats: "permanent"/"forever" (or empty) for a permanent ban,
/// or "&lt;number&gt;&lt;unit&gt;" where unit is h (hours), d (days), mo (months) or y (years).
/// Returns null for a permanent ban.
/// </summary>
public static DateTime? ParseDuration(string? duration)
{
if (string.IsNullOrWhiteSpace(duration))
{
return null;
}
var value = duration.Trim();
if (value.Equals("permanent", StringComparison.OrdinalIgnoreCase)
|| value.Equals("forever", StringComparison.OrdinalIgnoreCase)
|| value.Equals("perm", StringComparison.OrdinalIgnoreCase))
{
return null;
}
var match = Regex.Match(value, @"^(\d+)(h|d|mo|y)$", RegexOptions.IgnoreCase);
if (!match.Success)
{
return null;
}
var amount = int.Parse(match.Groups[1].Value);
var unit = match.Groups[2].Value.ToLowerInvariant();
var now = DateTime.UtcNow;
return unit switch
{
"h" => now.AddHours(amount),
"d" => now.AddDays(amount),
"mo" => now.AddMonths(amount),
"y" => now.AddYears(amount),
_ => null,
};
}
protected override BanData GetSnapshot()
{
PruneExpired();
return new()
{
Ips = new(_ips),
FriendCodes = new(_friendCodes),
};
}
protected override void ApplySnapshot(BanData data)
{
@@ -83,6 +186,28 @@ namespace Empostor.Server.Service.Admin.Ban
_friendCodes = new(data.FriendCodes ?? new());
}
private bool PruneExpired()
{
var changed = false;
foreach (var (key, entry) in _ips)
{
if (entry.IsExpired && _ips.TryRemove(key, out _))
{
changed = true;
}
}
foreach (var (key, entry) in _friendCodes)
{
if (entry.IsExpired && _friendCodes.TryRemove(key, out _))
{
changed = true;
}
}
return changed;
}
private static string Normalize(IPAddress ip)
=> ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4().ToString() : ip.ToString();
@@ -106,5 +231,14 @@ namespace Empostor.Server.Service.Admin.Ban
[JsonPropertyName("bannedAt")]
public required DateTime BannedAt { get; init; }
[JsonPropertyName("bannedUntil")]
public DateTime? BannedUntil { get; init; }
[JsonIgnore]
public bool IsExpired => BannedUntil.HasValue && BannedUntil.Value <= DateTime.UtcNow;
[JsonIgnore]
public bool IsPermanent => !BannedUntil.HasValue;
}
}