diff --git a/citadel.dme b/citadel.dme index e2f32a76e37..9ed6274dc3a 100644 --- a/citadel.dme +++ b/citadel.dme @@ -589,6 +589,7 @@ #include "code\controllers\subsystem\assets.dm" #include "code\controllers\subsystem\atoms.dm" #include "code\controllers\subsystem\automata.dm" +#include "code\controllers\subsystem\bans.dm" #include "code\controllers\subsystem\bellies_vr.dm" #include "code\controllers\subsystem\chat.dm" #include "code\controllers\subsystem\chemistry.dm" @@ -2323,9 +2324,7 @@ #include "code\modules\admin\ToRban.dm" #include "code\modules\admin\admin_modal\admin_modal.dm" #include "code\modules\admin\admin_modal\modals\admin_narrate.dm" -#include "code\modules\admin\ban\ban_system.dm" -#include "code\modules\admin\ban\role_ban.dm" -#include "code\modules\admin\ban\server_ban.dm" +#include "code\modules\admin\ban\ban_system_legacy.dm" #include "code\modules\admin\callproc\callproc.dm" #include "code\modules\admin\DB ban\functions.dm" #include "code\modules\admin\functions\modify_traits.dm" @@ -2698,6 +2697,8 @@ #include "code\modules\client\viewport.dm" #include "code\modules\client\wrappers.dm" #include "code\modules\client\data\client_data.dm" +#include "code\modules\client\data\player_ban.dm" +#include "code\modules\client\data\player_ban_cache.dm" #include "code\modules\client\data\player_data.dm" #include "code\modules\client\game_preferences\game_preference_entry.dm" #include "code\modules\client\game_preferences\game_preference_middleware.dm" diff --git a/code/controllers/subsystem/bans.dm b/code/controllers/subsystem/bans.dm new file mode 100644 index 00000000000..81565b98fb2 --- /dev/null +++ b/code/controllers/subsystem/bans.dm @@ -0,0 +1,62 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2025 Citadel Station Developers *// + +/** + * Ban management subsystem. + * * Can hook PreInit() + * * Must function as long as DBCore is connected. + */ +SUBSYSTEM_DEF(bans) + name = "Bans" + subsystem_flags = SS_NO_FIRE | SS_NO_INIT + +// Blank for now. This will be the new banning system eventually. + +/** + * * While the system supports IP/CID jobbans, it is up to the API user to know what is reasonable + * to match on. So basically, don't do that. + * + * @params + * * match_player_id - match any ckey on a player id + * * match_ckey - match this ckey + * * match_ip - match this ip (leave null to not ban by IP) + * * match_computer_id - match `computerid` + * * duration - duration in minutes; 0 for permanent + * * reason - ban reason plaintext + * * type_class - BAN_TYPE_* define (internally currently "server", "role", "role_class") + * * type_id - ID within context of `type_class`. + * "server": nothing + * "role": "station-janitor", "station-chief-engineer", "special-changeling", ... + * "role_class": "station", "traveller", "antagonist", ... + * * admin_ckey - (optional) banning admin's ckey + * * admin_ip - (optional) banning admin's ip + * * admin_computer_id - (optional) banning admin's computerid + */ +// /datum/controller/subsystem/bans/proc/place_ban(match_player_id, match_ckey, match_ip, match_computer_id, duration, reason, type_class, type_id, admin_ckey, admin_ip, admin_computer_id) +// /datum/controller/subsystem/bans/proc/edit_ban(ban_id, edit_reason, set_reason, set_duration, admin_ckey, admin_ip, admin_computer_id) +// /datum/controller/subsystem/bans/proc/remove_ban(ban_id, remove_reason, admin_ckey, admin_ip, admin_computer_id) +// /datum/controller/subsystem/bans/proc/get_ban(ban_id) as /datum/player_ban + +/** + * Returns any currently active ban for given match criterion + * * It is not well defined what ban is returned if there's more than one active. + * + * @params + * * match_player_id - match any ban with given player ID + * * match_ckey + * * match_ip + * * match_computer_id + * * type_class - type class to check for + * * type_id - type id to check for + */ +// /datum/controller/subsystem/bans/proc/match_ban(match_player_id, match_ckey, match_ip, match_computer_id, type, type_id) as /datum/player_ban + +/** + * Scan bans to get a cache entry for a player. + */ +// /datum/controller/subsystem/bans/proc/scan_bans(match_player_id, match_ckey, match_ip, match_computer_id) as /datum/player_ban_cache + +/** + * Get all bans for a player. Paginated. + */ +// /datum/controller/subsystem/bans/proc/query_bans(match_player_id, match_ckey, match_ip, match_computer_id, active_only = FALSE, page = 1, per_page = 20) as /list diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index a1f03cba832..797b2dc3c44 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -69,7 +69,7 @@ var/global/floorIsLava = 0 Kick | Ban | Jobban | - [is_role_banned_ckey(M.ckey, role = BAN_ROLE_OOC)? "OOC Ban" : "OOC Ban"] | + [SSbans.t_is_role_banned_ckey(M.ckey, role = BAN_ROLE_OOC)? "OOC Ban" : "OOC Ban"] | Notes "} diff --git a/code/modules/admin/ban/ban_system.dm b/code/modules/admin/ban/ban_system.dm deleted file mode 100644 index 63790e2e1dd..00000000000 --- a/code/modules/admin/ban/ban_system.dm +++ /dev/null @@ -1,7 +0,0 @@ - -// placeholder file to replace DB_ban_record and other system stuff -// we're going to want unified -// /datum/controller/subsystem/bans/proc/place_ban() -// /datum/controller/subsystem/bans/proc/lift_ban() -// /datum/controller/subsystem/bans/proc/edit_ban() -// /datum/controller/subsystem/bans/proc/query_ban() diff --git a/code/modules/admin/ban/ban_system_legacy.dm b/code/modules/admin/ban/ban_system_legacy.dm new file mode 100644 index 00000000000..4e67d39fa4c --- /dev/null +++ b/code/modules/admin/ban/ban_system_legacy.dm @@ -0,0 +1,157 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2025 Citadel Station Developers *// + +// ban wrappers until we make new ban system + +/datum/controller/subsystem/bans/proc/t_place_system_ban(bantype, mob/banned_mob, duration = -1, reason, job = "", rounds = 0, banckey = null, banip = null, bancid = null) + if(!SSdbcore.Connect()) + return + + var/serverip = "[world.internet_address]:[world.port]" + var/bantype_pass = 0 + var/bantype_str + switch(bantype) + if(BANTYPE_PERMA) + bantype_str = "PERMABAN" + duration = -1 + bantype_pass = 1 + if(BANTYPE_TEMP) + bantype_str = "TEMPBAN" + bantype_pass = 1 + if(BANTYPE_JOB_PERMA) + bantype_str = "JOB_PERMABAN" + duration = -1 + bantype_pass = 1 + if(BANTYPE_JOB_TEMP) + bantype_str = "JOB_TEMPBAN" + bantype_pass = 1 + if( !bantype_pass ) return + if( !istext(reason) ) return + if( !isnum(duration) ) return + + var/ckey + var/computerid + var/ip + + if(ismob(banned_mob)) + ckey = banned_mob.ckey + if(banned_mob.client) + computerid = banned_mob.client.computer_id + ip = banned_mob.client.address + else if(banckey) + ckey = ckey(banckey) + computerid = bancid + ip = banip + + var/a_ckey + var/a_computerid + var/a_ip + + var/who + for(var/client/C in GLOB.clients) + if(!who) + who = "[C]" + else + who += ", [C]" + + var/adminwho + for(var/client/C in GLOB.admins) + if(!adminwho) + adminwho = "[C]" + else + adminwho += ", [C]" + + reason = sql_sanitize_text(reason) + + if(isnull(computerid)) + computerid = "" + if(isnull(ip)) + ip = "" + var/sql = "INSERT INTO [DB_PREFIX_TABLE_NAME("ban")] \ + (`id`,`bantime`,`serverip`,`bantype`,`reason`,`job`,`duration`,`rounds`,`expiration_time`,`ckey`,`computerid`,`ip`,`a_ckey`,`a_computerid`,`a_ip`,`who`,`adminwho`,`edits`,`unbanned`,`unbanned_datetime`,`unbanned_ckey`,`unbanned_computerid`,`unbanned_ip`) \ + VALUES (null, Now(), :serverip, :type, :reason, :job, :duration, :rounds, Now() + INTERVAL :duration MINUTE, :ckey, :cid, :ip, :a_ckey, :a_cid, :a_ip, :who, :adminwho, '', null, null, null, null, null)" + SSdbcore.RunQuery( + sql, + list( + "serverip" = serverip, + "type" = bantype_str, + "reason" = reason, + "job" = job, + "duration" = duration? duration : 0, + "rounds" = rounds? rounds : 0, + "ckey" = ckey, + "cid" = computerid, + "ip" = ip, + "a_ckey" = a_ckey, + "a_cid" = a_computerid, + "a_ip" = a_ip, + "who" = who, + "adminwho" = adminwho + ) + ) + to_chat(usr, "Ban saved to database.") + message_admins("[key_name_admin(usr)] has added a [bantype_str] for [ckey] [(job)?"([job])":""] [(duration > 0)?"([duration] minutes)":""] with the reason: \"[reason]\" to the ban database.",1) + . = TRUE + + // reload + jobban_loadbanfile() + + +/** + * master proc to check if someone's banned from a role + * + * @params + * * ckey - the player's ckey + * * character_id - the character's id, null for global account ban + * * role - role enum check [code/__DEFINES/admin/bans.dm] + * + * @return TRUE / FALSE for if they are banned right now + */ +/datum/controller/subsystem/bans/proc/t_is_role_banned_ckey(ckey, character_id, role) + // sanitize just in case + ckey = ckey(ckey) + + // isolate from proccall, this is sanitized + var/mob/old_usr = usr + usr = null + + . = FALSE + + switch(role) + if(BAN_ROLE_SERVER) + . = world.IsBanned(ckey) + else + // lol this is dumb, refactor jobbans when + for(var/str as anything in jobban_keylist) + if(findtext(str, "[ckey] - [role]") == 1) + . = TRUE + + // restore admin proccall + usr = old_usr + +/** + * ban someone from a role for a certain time, specified in minutes. + * + * BAN_ROLE_SERVER is not allowed here. + * + * @params + * * ckey - the player's ckey + * * character_id - the character's id, null for global account ban + * * role - role enum check [code/__DEFINES/admin/bans.dm] - BAN_ROLE_SERVER is not allowed here! + * * minutes - minutes from Now() - null for permanent + * * reason - why? + * * admin - the banning admin, if any + * + * @return TRUE / FALSE on success / failure + */ +/datum/controller/subsystem/bans/proc/t_role_ban_ckey(ckey, character_id, role, minutes, reason, datum/admins/admin) + ASSERT(isnull(minutes) || (isnum(minutes) && minutes > 0)) + // sanitize just in case + ckey = ckey(ckey) + + if(IsAdminAdvancedProcCall()) + return // use the panel! + + //? shitcode alert: for now, db bans *must* be anchored to an admin datum. + + . = admin?.DB_ban_record(isnull(minutes)? BANTYPE_JOB_PERMA : BANTYPE_JOB_TEMP, duration = isnull(minutes)? -1 : minutes, job = role, banckey = ckey, reason = reason) || FALSE diff --git a/code/modules/admin/ban/role_ban.dm b/code/modules/admin/ban/role_ban.dm deleted file mode 100644 index 89d9fb1d3ba..00000000000 --- a/code/modules/admin/ban/role_ban.dm +++ /dev/null @@ -1,71 +0,0 @@ -//? Role ban system -//? Rolebans are for taking functionality away from problematic players. -//? If you want a player off the server / there's no point in keeping then, -//? use server_ban.dm functions instead. - -/** - * master proc to check if someone's banned from a role - * - * @params - * * ckey - the player's ckey - * * character_id - the character's id, null for global account ban - * * role - role enum check [code/__DEFINES/admin/bans.dm] - * - * @return TRUE / FALSE for if they are banned right now - */ -/proc/is_role_banned_ckey(ckey, character_id, role) - // sanitize just in case - ckey = ckey(ckey) - - // isolate from proccall, this is sanitized - var/mob/old_usr = usr - usr = null - - . = FALSE - - switch(role) - if(BAN_ROLE_SERVER) - . = world.IsBanned(ckey) - else - // lol this is dumb, refactor jobbans when - for(var/str as anything in jobban_keylist) - if(findtext(str, "[ckey] - [role]") == 1) - . = TRUE - - // restore admin proccall - usr = old_usr - -/** - * ban someone from a role for a certain time, specified in minutes. - * - * BAN_ROLE_SERVER is not allowed here. - * - * @params - * * ckey - the player's ckey - * * character_id - the character's id, null for global account ban - * * role - role enum check [code/__DEFINES/admin/bans.dm] - BAN_ROLE_SERVER is not allowed here! - * * minutes - minutes from Now() - null for permanent - * * reason - why? - * * admin - the banning admin, if any - * - * @return TRUE / FALSE on success / failure - */ -/proc/role_ban_ckey(ckey, character_id, role, minutes, reason, datum/admins/admin) - ASSERT(isnull(minutes) || (isnum(minutes) && minutes > 0)) - // sanitize just in case - ckey = ckey(ckey) - - if(IsAdminAdvancedProcCall()) - return // use the panel! - - //? shitcode alert: for now, db bans *must* be anchored to an admin datum. - - . = admin?.DB_ban_record(isnull(minutes)? BANTYPE_JOB_PERMA : BANTYPE_JOB_TEMP, duration = isnull(minutes)? -1 : minutes, job = role, banckey = ckey, reason = reason) || FALSE - -// todo: query_role_banned_ckey(ckey) -// todo: why_role_banned_ckey(ckey) -// todo: time_role_banned_ckey(ckey) - -// todo: for the above, we should probably make the overhead not miserable by caching it in /datum/player_data. - -// todo: x_role_y_player(...) for player ids instead diff --git a/code/modules/admin/ban/server_ban.dm b/code/modules/admin/ban/server_ban.dm deleted file mode 100644 index 7a0c4237a41..00000000000 --- a/code/modules/admin/ban/server_ban.dm +++ /dev/null @@ -1,5 +0,0 @@ - -// this file is a placeholder - we'll need it on ban rework -// server bans are the complement to role bans -// rolebans take functions away from a problematic player -// server bans keep a player off the server diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 3cb00cf2ef1..1a57d3f9c57 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -845,7 +845,7 @@ // we'll add the function later when we overhaul banning return - if(is_role_banned_ckey(target_ckey, role = BAN_ROLE_OOC)) + if(SSbans.t_is_role_banned_ckey(target_ckey, role = BAN_ROLE_OOC)) to_chat(usr, SPAN_WARNING("[target_ckey] is already OOC banned. Use Unban-Panel to unban them.")) return @@ -857,7 +857,7 @@ var/reason = sanitize(input(usr, "Reason?", "OOC Ban") as text|null) if(!reason) return - role_ban_ckey(target_ckey, role = BAN_ROLE_OOC, minutes = minutes, reason = reason, admin = src) + SSbans.t_role_ban_ckey(target_ckey, role = BAN_ROLE_OOC, minutes = minutes, reason = reason, admin = src) // incase they switched mobs var/client/target_client = GLOB.directory[target_ckey] notes_add(target_ckey, "[usr.ckey] has banned has banned [target_ckey] from OOC. Reason: [reason]. This will be removed in [minutes] minutes.") @@ -869,7 +869,7 @@ var/reason = sanitize(input(usr, "Reason?", "OOC Ban") as text|null) if(!reason) return - role_ban_ckey(target_ckey, role = BAN_ROLE_OOC, reason = reason, admin = src) + SSbans.t_role_ban_ckey(target_ckey, role = BAN_ROLE_OOC, reason = reason, admin = src) // incase they switched mobs var/client/target_client = GLOB.directory[target_ckey] notes_add(target_ckey, "[usr.ckey] has banned has banned [target_ckey] from OOC. Reason: [reason].") diff --git a/code/modules/client/data/player_ban.dm b/code/modules/client/data/player_ban.dm new file mode 100644 index 00000000000..c1b086dd993 --- /dev/null +++ b/code/modules/client/data/player_ban.dm @@ -0,0 +1,14 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2025 Citadel Station Developers *// + +/datum/player_ban + var/ban_id + var/match_player_id + var/match_ckey + var/match_ip + var/match_computer_id + var/type_class + var/type_id + /// plaintext generated by subsystem + /// * not always filled out; if looking up metadata + var/list/edit_log diff --git a/code/modules/client/data/player_ban_cache.dm b/code/modules/client/data/player_ban_cache.dm new file mode 100644 index 00000000000..fdb282c21c7 --- /dev/null +++ b/code/modules/client/data/player_ban_cache.dm @@ -0,0 +1,15 @@ +//* This file is explicitly licensed under the MIT license. *// +//* Copyright (c) 2025 Citadel Station Developers *// + +/** + * Contains all active bans for a player for quick lookup. + * * Will only contain one ban per relevant thing; e.g. if someone's somehow triple-jobbanned + * from captain, it'll only have one. + */ +/datum/player_ban_cache + var/total_bans + var/datum/player_ban/is_server_banned + /// id = entry + var/alist/datum/player_ban/currently_banned_role_ids + /// class = entry + var/alist/datum/player_ban/currently_banned_role_classes diff --git a/code/modules/client/security.dm b/code/modules/client/security.dm index c540a229c92..7e9f92e99ea 100644 --- a/code/modules/client/security.dm +++ b/code/modules/client/security.dm @@ -20,11 +20,19 @@ * tell_user is implicit */ /client/proc/security_ban(message, time = -1) - var/time_displayed = time == -1? "" : "for [DisplayTimeText(time MINUTES)]" + var/time_displayed = time == -1? "(forever)" : "for [DisplayTimeText(time MINUTES)]" log_access("client security: banning [key_name(src)] [time == -1? "" : "for [time_displayed]"] | [message]") message_admins("client security: banning [key_name(src)] [time == -1? "" : "for [time_displayed]"] | [message]") add_system_note("client-security", "banned for [time_displayed]: [message]") - AddBan(ckey, computer_id, "client-security: [message]", minutes = time) + SSbans.t_place_system_ban( + time == -1 ? BANTYPE_PERMA : BANTYPE_TEMP, + null, + time, + "client-security: [message]", + banckey = ckey, + banip = address, + bancid = computer_id, + ) disconnection_message("Client Security - Autoban: [message]") qdel(src) diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm index 4bb061d0d29..1bbcc3f1576 100644 --- a/code/modules/client/verbs/ooc.dm +++ b/code/modules/client/verbs/ooc.dm @@ -74,7 +74,7 @@ to_chat(src, "You have OOC muted.") return - if(is_role_banned_ckey(ckey, role = BAN_ROLE_OOC)) + if(SSbans.t_is_role_banned_ckey(ckey, role = BAN_ROLE_OOC)) to_chat(src, SPAN_WARNING("You are banned from OOC and deadchat.")) return @@ -181,7 +181,7 @@ to_chat(src, "Guests may not use OOC.") return - if(is_role_banned_ckey(ckey, role = BAN_ROLE_OOC) && IS_DEAD(mob)) + if(SSbans.t_is_role_banned_ckey(ckey, role = BAN_ROLE_OOC) && IS_DEAD(mob)) to_chat(src, SPAN_WARNING("You are banned from typing in LOOC while dead, and deadchat.")) return diff --git a/code/modules/mob/emote.dm b/code/modules/mob/emote.dm index d5a7da211cf..52e67a4f28a 100644 --- a/code/modules/mob/emote.dm +++ b/code/modules/mob/emote.dm @@ -79,7 +79,7 @@ to_chat(src, "Deadchat is globally muted.") return - if(is_role_banned_ckey(ckey, role = BAN_ROLE_OOC)) + if(SSbans.t_is_role_banned_ckey(ckey, role = BAN_ROLE_OOC)) to_chat(src, SPAN_WARNING("You are banned from OOC and deadchat.")) return diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm index a34ccf27cc0..d2a8449b8bf 100644 --- a/code/modules/mob/say.dm +++ b/code/modules/mob/say.dm @@ -57,7 +57,7 @@ to_chat(src, "You have deadchat muted.") return - if(is_role_banned_ckey(ckey, role = BAN_ROLE_OOC)) + if(SSbans.t_is_role_banned_ckey(ckey, role = BAN_ROLE_OOC)) to_chat(src, SPAN_WARNING("You are banned from OOC and deadchat.")) return