diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 3b3da1b9b7f..52bed0290f7 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -72,7 +72,8 @@
#define INIT_ORDER_PROFILER 100
#define INIT_ORDER_FAIL2TOPIC 99
#define INIT_ORDER_TITLE 98
-#define INIT_ORDER_GARBAGE 95
+#define INIT_ORDER_GARBAGE 96
+#define INIT_ORDER_BCCM 95
#define INIT_ORDER_DBCORE 94
#define INIT_ORDER_STATPANELS 93
#define INIT_ORDER_BLACKBOX 92
diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm
index d9e75ef92df..f571aa807c7 100644
--- a/code/__HELPERS/text.dm
+++ b/code/__HELPERS/text.dm
@@ -18,6 +18,23 @@
/proc/format_table_name(table as text)
return CONFIG_GET(string/feedback_tableprefix) + table
+/proc/sql_sanitize_text(text)
+ text = replacetext(text, "'", "''")
+ text = replacetext(text, ";", "")
+ text = replacetext(text, "&", "")
+ return text
+
+/proc/new_sql_sanitize_text(text)
+ text = replacetext(text, "'", "")
+ text = replacetext(text, ";", "")
+ text = replacetext(text, "&", "")
+ text = replacetext(text, "`", "")
+ return text
+
+/proc/remove_all_spaces(text)
+ text = replacetext(text, " ", "")
+ return text
+
/*
* Text sanitization
*/
diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm
index 21b1d4e8760..ce75fdc82d1 100644
--- a/code/controllers/configuration/configuration.dm
+++ b/code/controllers/configuration/configuration.dm
@@ -23,6 +23,7 @@
// var/policy
var/static/regex/ic_filter_regex
+ var/bccm = TRUE
/datum/controller/configuration/proc/admin_reload()
if(IsAdminAdvancedProcCall())
diff --git a/code/controllers/subsystem/bccm.dm b/code/controllers/subsystem/bccm.dm
new file mode 100644
index 00000000000..5497d3fdee5
--- /dev/null
+++ b/code/controllers/subsystem/bccm.dm
@@ -0,0 +1,386 @@
+// BCCM (Ban Counter Counter Measures system) ((Name subject to change)), originally inspired by EAMS (Epic Anti-Multiaccount System), by Epicus
+//version 1.0.2
+
+/datum/bccm_info
+ var/is_loaded = FALSE
+ var/is_whitelisted = FALSE
+
+ var/ip
+ var/ip_as
+ var/ip_mobile
+ var/ip_proxy
+ var/ip_hosting
+
+/client
+ var/datum/bccm_info/bccm_info = new
+
+SUBSYSTEM_DEF(bccm)
+ name = "BCCM"
+ init_order = INIT_ORDER_BCCM
+ flags = SS_NO_FIRE
+
+ var/max_error_count = 4
+
+ var/is_active = FALSE
+ var/error_counter = 0
+
+ var/list/tgui_panel_asn_data = list()
+ var/list/tgui_panel_wl_data = list()
+
+ var/list/client/postponed_client_queue = new
+
+/datum/controller/subsystem/bccm/Initialize(timeofday)
+ if(!config.bccm)
+ return ..()
+/*
+ if(!sqlenabled)
+ log_sql("BCCM could not be loaded without SQL enabled")
+ return ..()
+*/
+ Toggle()
+ return ..()
+
+/datum/controller/subsystem/bccm/stat_entry(msg)
+ return "[is_active ? "ACTIVE" : "OFFLINE"]"
+
+/datum/controller/subsystem/bccm/proc/Toggle(mob/user)
+ if (!initialized && user)
+ return
+
+ if(!is_active && !SSdbcore.Connect())
+ log_sql("BCCM could not be loaded because the DB connection could not be established.")
+ return
+
+ is_active = !is_active
+ log_sql("BCCM is [is_active ? "enabled" : "disabled"]!")
+
+ . = is_active
+ if(!.)
+ return
+
+ tgui_panel_asn_data = GetAsnBanlistDatabase()
+ tgui_panel_wl_data = GetWhitelistDatabase()
+
+ var/list/clients_to_check = postponed_client_queue.Copy()
+ postponed_client_queue.Cut()
+ for (var/client/C in clients_to_check)
+ CollectClientData(C)
+ HandleClientAccessCheck(C, postponed = TRUE)
+ HandleASNbanCheck(C, postponed = TRUE)
+ CHECK_TICK
+
+/datum/controller/subsystem/bccm/proc/CheckDBCon()
+ if(is_active && SSdbcore.Connect())
+ return TRUE
+
+ is_active = FALSE
+ log_and_message_admins("A Database error has occured. BCCM is automatically disabled.")
+ return FALSE
+
+
+/datum/controller/subsystem/bccm/proc/CollectClientData(client/C)
+ ASSERT(istype(C))
+
+ var/_ip_addr = C.address
+
+ if(!is_active)
+ postponed_client_queue.Add(C)
+ return
+
+ if(!CheckDBCon())
+ return
+
+ C.bccm_info.is_whitelisted = CheckWhitelist(C.ckey)
+
+ if(!_ip_addr || _ip_addr == "127.0.0.1")
+ return
+
+ var/list/response = GetAPIresponse(_ip_addr, C)
+
+ if(!response)
+ return
+
+ C.bccm_info.ip = _ip_addr
+ C.bccm_info.ip_as = response["as"]
+ C.bccm_info.ip_mobile = response["mobile"]
+ C.bccm_info.ip_proxy = response["proxy"]
+ C.bccm_info.ip_hosting = response["hosting"]
+
+ C.bccm_info.is_loaded = TRUE
+ return
+
+/datum/controller/subsystem/bccm/proc/GetAPIresponse(ip, client/C = null)
+ var/list/response = LoadCachedData(ip)
+
+ if(response && C)
+ log_access("BCCM data for [C] ([ip]) is loaded from cache!")
+
+ while(!response && is_active && error_counter < max_error_count)
+ var/list/http = world.Export("http://ip-api.com/json/[ip]?fields=17025024")
+
+ if(!http)
+ if(C)
+ log_and_message_admins("BCCM: API connection failed, could not check [C], retrying.")
+ else
+ log_and_message_admins("BCCM: API connection failed, could not check [ip], retrying.")
+ error_counter += 1
+ sleep(2)
+ continue
+
+ var/raw_response = file2text(http["CONTENT"])
+
+ try
+ response = json_decode(raw_response)
+ catch (var/exception/e)
+ log_and_message_admins("BCCM: JSON decode error, could not check [C]. JSON decode error: [e.name]")
+ return
+
+ if(response["status"] == "fail")
+ log_and_message_admins("BCCM: Request error, could not check [C]. CheckIP response: [response["message"]]")
+ return
+
+ if(C)
+ log_access("BCCM data for [C]([ip]) is loaded from external API!")
+ CacheData(ip, raw_response)
+
+ if(error_counter >= max_error_count && is_active)
+ log_and_message_admins("BCCM was disabled due to connection errors!")
+ is_active = FALSE
+ return
+
+ return response
+
+/datum/controller/subsystem/bccm/proc/CheckForAccess(client/C)
+ ASSERT(istype(C))
+
+ if(!is_active)
+ return TRUE
+
+ if(!CheckDBCon())
+ return TRUE
+
+ if(!C.address || C.holder)
+ return TRUE
+
+ if(C.bccm_info.is_whitelisted)
+ return TRUE
+
+ if(C.bccm_info.is_loaded)
+ if(!C.bccm_info.ip_proxy && !C.bccm_info.ip_hosting)
+ return TRUE
+ return FALSE
+
+ log_and_message_admins("BCCM failed to load info for [C.ckey].")
+ return TRUE
+
+/datum/controller/subsystem/bccm/proc/CheckWhitelist(ckey)
+ . = FALSE
+
+ if(!CheckDBCon())
+ return
+
+ var/datum/db_query/query = SSdbcore.NewQuery("SELECT ckey FROM bccm_whitelist WHERE ckey = '[ckey]'")
+ query.Execute()
+
+ if(query.NextRow())
+ . = TRUE
+
+ qdel(query)
+
+ return
+
+/datum/controller/subsystem/bccm/proc/CheckASNban(client/C)
+ ASSERT(istype(C))
+
+ . = TRUE
+
+ if(!is_active)
+ return
+
+ if(!CheckDBCon())
+ return
+
+ var/datum/db_query/query = SSdbcore.NewQuery("SELECT `asn` FROM bccm_asn_ban WHERE asn = '[C.bccm_info.ip_as]'")
+ query.Execute()
+
+ if(query.NextRow())
+ . = FALSE
+
+ qdel(query)
+
+ return
+
+/datum/controller/subsystem/bccm/proc/LoadCachedData(ip)
+ ASSERT(istext(ip))
+
+ if(!CheckDBCon())
+ return FALSE
+
+ var/datum/db_query/_Cache_select_query = SSdbcore.NewQuery("SELECT response FROM bccm_ip_cache WHERE ip = '[ip]'")
+ _Cache_select_query.Execute()
+
+ if(!_Cache_select_query.NextRow())
+ . = FALSE
+ else
+ . = json_decode(_Cache_select_query.item[1])
+
+ qdel(_Cache_select_query)
+ return
+
+/datum/controller/subsystem/bccm/proc/CacheData(ip, raw_response)
+ ASSERT(istext(ip))
+ ASSERT(istext(raw_response))
+
+ if(!CheckDBCon())
+ return FALSE
+
+ var/datum/db_query/_Cache_insert_query = SSdbcore.NewQuery("INSERT INTO bccm_ip_cache (`ip`, `response`) VALUES ('[ip]', '[raw_response]')")
+ _Cache_insert_query.Execute()
+ qdel(_Cache_insert_query)
+
+ return TRUE
+
+/datum/controller/subsystem/bccm/proc/AddToWhitelist(ckey_input, client/Admin)
+ ASSERT(istype(Admin))
+
+ if(!CheckDBCon())
+ return
+
+ var/ckey = new_sql_sanitize_text(ckey(ckey_input))
+
+ if(!ckey)
+ return
+
+ var/datum/db_query/_Whitelist_Query = SSdbcore.NewQuery("INSERT INTO bccm_whitelist (`ckey`, `a_ckey`, `timestamp`) VALUES ('[ckey]', '[Admin.ckey]', Now())")
+ _Whitelist_Query.Execute()
+ qdel(_Whitelist_Query)
+
+ tgui_panel_wl_data = GetWhitelistDatabase()
+ log_and_message_admins("added [ckey] to BCCM whitelist.")
+
+ return TRUE
+
+/datum/controller/subsystem/bccm/proc/RemoveFromWhitelist(ckey, client/Admin)
+ if(!CheckDBCon())
+ return FALSE
+
+ if(!CheckWhitelist(ckey))
+ return
+
+ var/datum/db_query/_Whitelist_Query = SSdbcore.NewQuery("DELETE FROM bccm_whitelist WHERE `ckey` = '[ckey]'")
+ _Whitelist_Query.Execute()
+ qdel(_Whitelist_Query)
+
+ tgui_panel_wl_data = GetWhitelistDatabase()
+ log_and_message_admins("removed [ckey] from BCCM whitelist.", Admin.mob)
+
+ return TRUE
+
+/datum/controller/subsystem/bccm/proc/GetWhitelistDatabase()
+ var/datum/db_query/_Whitelist_DB_Select_Query = SSdbcore.NewQuery("SELECT `ckey`, `a_ckey`, `timestamp` from bccm_whitelist")
+ _Whitelist_DB_Select_Query.Execute()
+
+ var/list/result = list()
+
+ while(_Whitelist_DB_Select_Query.NextRow())
+ var/list/row = list()
+ row["ckey"] = _Whitelist_DB_Select_Query.item[1]
+ row["a_ckey"] = _Whitelist_DB_Select_Query.item[2]
+ row["timestamp"] = _Whitelist_DB_Select_Query.item[3]
+
+ result["displayData"] += list(row)
+
+ qdel(_Whitelist_DB_Select_Query)
+
+ return result
+
+/datum/controller/subsystem/bccm/proc/AddASNban(address, client/Admin)
+ if(!CheckDBCon())
+ return
+
+ if(!check_rights(R_SERVER, TRUE, Admin))
+ return
+
+ var/ip = remove_all_spaces(new_sql_sanitize_text(address))
+
+ if(length(ip) > 16)
+ return
+
+ var/list/response = GetAPIresponse(ip)
+
+ var/ip_as = response["as"]
+
+ var/datum/db_query/_ASban_Insert_Query = SSdbcore.NewQuery("INSERT INTO bccm_asn_ban (`ip`, `asn`, `a_ckey`, `timestamp`) VALUES ('[ip]', '[ip_as]', '[Admin.ckey]', Now())")
+ _ASban_Insert_Query.Execute()
+ qdel(_ASban_Insert_Query)
+
+ tgui_panel_asn_data = GetAsnBanlistDatabase()
+ log_and_message_admins("has added '[ip_as]' to the BCCM ASN banlist.", Admin)
+
+ return TRUE
+
+/datum/controller/subsystem/bccm/proc/RemoveASNban(ip_as, client/Admin)
+ if(!CheckDBCon())
+ return
+
+ if(!check_rights(R_SERVER, TRUE, Admin))
+ return
+
+ var/datum/db_query/_ASban_Delete_Query = SSdbcore.NewQuery("DELETE FROM bccm_asn_ban WHERE `asn` = '[ip_as]'")
+ _ASban_Delete_Query.Execute()
+ qdel(_ASban_Delete_Query)
+
+ tgui_panel_asn_data = GetAsnBanlistDatabase()
+ log_and_message_admins("has removed '[ip_as]' from the BCCM ASN banlist.", Admin)
+
+ return TRUE
+
+
+/datum/controller/subsystem/bccm/proc/GetAsnBanlistDatabase()
+ var/datum/db_query/_ASN_Banlist_Select_Query = SSdbcore.NewQuery("SELECT `asn`, `timestamp`, `a_ckey` from bccm_asn_ban")
+ _ASN_Banlist_Select_Query.Execute()
+
+ var/list/result = list()
+
+ while(_ASN_Banlist_Select_Query.NextRow())
+ var/list/row = list()
+ row["asn"] = _ASN_Banlist_Select_Query.item[1]
+ row["timestamp"] = _ASN_Banlist_Select_Query.item[2]
+ row["a_ckey"] = _ASN_Banlist_Select_Query.item[3]
+
+ result["displayData"] += list(row)
+
+ qdel(_ASN_Banlist_Select_Query)
+
+ return result
+
+
+/datum/controller/subsystem/bccm/proc/HandleClientAccessCheck(client/C, postponed = 0)
+ if(!SSbccm.CheckForAccess(C) && !(C.ckey in GLOB.admin_datums))
+ if(!postponed)
+ C.log_client_to_db_connection_log()
+ log_and_message_admins(span_notice("BCCM: Failed Login: [C.key]/[C.ckey]([C.address])([C.computer_id]) failed to pass BCCM check."))
+ qdel(C)
+ return
+
+/datum/controller/subsystem/bccm/proc/HandleASNbanCheck(client/C, postponed = 0)
+ if(!SSbccm.CheckASNban(C) && !(C.ckey in GLOB.admin_datums))
+ if(!postponed)
+ C.log_client_to_db_connection_log()
+ log_and_message_admins(span_notice("BCCM: Failed Login: [C.key]/[C.ckey]([C.address])([C.computer_id]) failed to pass ASN ban check."))
+ qdel(C)
+ return
+
+/client/proc/BCCM_toggle()
+ set category = "Server"
+ set name = "Toggle BCCM"
+
+ if(!check_rights(R_SERVER))
+ return
+
+ if(!SSdbcore.Connect())
+ to_chat(usr, span_notice("The Database is not connected!"))
+ return
+
+ var/bccm_status = SSbccm.Toggle()
+ log_and_message_admins("has [bccm_status ? "enabled" : "disabled"] the BCCM system!")
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 2d55d4acbe0..107f450863b 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -91,7 +91,9 @@ GLOBAL_PROTECT(admin_verbs_admin)
/datum/admins/proc/open_borgopanel,
/datum/admins/proc/toggle_sleep,
/datum/admins/proc/toggle_sleep_area,
- /datum/admins/proc/toggle_faction_join
+ /datum/admins/proc/toggle_faction_join,
+ /client/proc/BCCM_toggle,
+ /client/proc/BCCM_WhitelistPanel
)
GLOBAL_LIST_INIT(admin_verbs_ban, list(/client/proc/unban_panel, /client/proc/DB_ban_panel, /client/proc/stickybanpanel))
GLOBAL_PROTECT(admin_verbs_ban)
@@ -143,7 +145,8 @@ GLOBAL_LIST_INIT(admin_verbs_server, world.AVerbsServer())
/client/proc/toggle_random_events,
/client/proc/forcerandomrotate,
/client/proc/adminchangemap,
- /client/proc/toggle_hub
+ /client/proc/toggle_hub,
+ /client/proc/BCCM_ASNPanel
)
GLOBAL_PROTECT(admin_verbs_server)
GLOBAL_LIST_INIT(admin_verbs_debug, world.AVerbsDebug())
diff --git a/code/modules/admin/bccm/tgui_asn.dm b/code/modules/admin/bccm/tgui_asn.dm
new file mode 100644
index 00000000000..1a7eabd859a
--- /dev/null
+++ b/code/modules/admin/bccm/tgui_asn.dm
@@ -0,0 +1,82 @@
+/client/proc/BCCM_ASNPanel()
+ set category = "Server"
+ set name = "BCCM ASN Panel"
+
+ if(!SSdbcore.Connect())
+ to_chat(usr, span_warning("Failed to establish database connection"))
+ return
+
+ if(!check_rights(R_SERVER))
+ return
+
+ new /datum/bccm_asn_panel(src)
+
+/datum/bccm_asn_panel
+ var/client/holder // client of who is holding this
+
+/datum/bccm_asn_panel/New(user)
+ if(user)
+ setup(user)
+ else
+ qdel(src)
+ return
+
+/datum/bccm_asn_panel/proc/setup(user) // client or mob
+ if(!SSdbcore.Connect())
+ to_chat(holder, span_warning("Failed to establish database connection"))
+ qdel(src)
+ return
+
+ if(istype(user, /client))
+ var/client/user_client = user
+ holder = user_client
+ else
+ var/mob/user_mob = user
+ holder = user_mob.client
+
+ if(!check_rights(R_SERVER, TRUE, holder))
+ qdel(src)
+ return
+
+ ui_interact(holder.mob)
+
+/datum/bccm_asn_panel/ui_state(mob/user)
+ return GLOB.admin_state // admin only
+
+/datum/bccm_asn_panel/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "BCCMASNPanel")
+ ui.open()
+
+/datum/bccm_asn_panel/ui_data(mob/user, ui_key)
+ . = SSbccm.tgui_panel_asn_data
+
+/datum/bccm_asn_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ if(..())
+ return
+ switch(action)
+ if("asn_remove_entry")
+ if(!params["asn"])
+ return TRUE
+ if(SSbccm.RemoveASNban(params["asn"], holder))
+ SStgui.update_uis(src)
+ else
+ return TRUE
+ if("asn_add_entry")
+ if(!params["ip"])
+ return TRUE
+ if(SSbccm.AddASNban(params["ip"], holder))
+ SStgui.update_uis(src)
+ else
+ return TRUE
+
+ if(!length(SSbccm.tgui_panel_asn_data))
+ qdel(src) //For some unknown reason it refuses to update UI when it goes from 1 to 0 entries, so last item gets stuck. I can't fix it now, maybe later. ~Tsuru
+ return
+
+ SStgui.update_user_uis(holder.mob)
+ return TRUE
+
+/datum/bccm_asn_panel/ui_close(mob/user)
+ qdel(src)
diff --git a/code/modules/admin/bccm/tgui_whitelist.dm b/code/modules/admin/bccm/tgui_whitelist.dm
new file mode 100644
index 00000000000..baaf51ffb6c
--- /dev/null
+++ b/code/modules/admin/bccm/tgui_whitelist.dm
@@ -0,0 +1,82 @@
+/client/proc/BCCM_WhitelistPanel()
+ set category = "Server"
+ set name = "BCCM WL Panel"
+
+ if(!SSdbcore.Connect())
+ to_chat(usr, span_warning("Failed to establish database connection"))
+ return
+
+ if(!check_rights(R_BAN))
+ return
+
+ new /datum/bccm_wl_panel(src)
+
+/datum/bccm_wl_panel
+ var/client/holder // client of who is holding this
+
+/datum/bccm_wl_panel/New(user)
+ if(user)
+ setup(user)
+ else
+ qdel(src)
+ return
+
+/datum/bccm_wl_panel/proc/setup(user) // client or mob
+ if(!SSdbcore.Connect())
+ to_chat(holder, span_warning("Failed to establish database connection"))
+ qdel(src)
+ return
+
+ if(istype(user, /client))
+ var/client/user_client = user
+ holder = user_client
+ else
+ var/mob/user_mob = user
+ holder = user_mob.client
+
+ if(!check_rights(R_BAN, TRUE, holder))
+ qdel(src)
+ return
+
+ ui_interact(holder.mob)
+
+/datum/bccm_wl_panel/ui_state(mob/user)
+ return GLOB.admin_state // admin only
+
+/datum/bccm_wl_panel/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "BCCMWhitelistPanel")
+ ui.open()
+
+/datum/bccm_wl_panel/ui_data(mob/user, ui_key)
+ . = SSbccm.tgui_panel_wl_data
+
+/datum/bccm_wl_panel/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ if(..())
+ return
+ switch(action)
+ if("wl_remove_entry")
+ if(!params["ckey"])
+ return TRUE
+ if(SSbccm.RemoveFromWhitelist(params["ckey"], holder))
+ SStgui.update_uis(src)
+ else
+ return TRUE
+ if("wl_add_ckey")
+ if(!params["ckey"])
+ return TRUE
+ if(SSbccm.AddToWhitelist(params["ckey"], holder))
+ SStgui.update_uis(src)
+ else
+ return TRUE
+
+ if(!length(SSbccm.tgui_panel_wl_data))
+ qdel(src) //Same as ASN. ~Tsuru
+ return
+
+ SStgui.update_user_uis(holder.mob)
+ return TRUE
+
+/datum/bccm_wl_panel/ui_close(mob/user)
+ qdel(src)
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 5c615145d98..eba7f45623c 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -261,6 +261,9 @@ GLOBAL_LIST_INIT(warning_ckeys, list())
else
new /datum/admins(autorank, ckey)
//CITADEL EDIT
+ SSbccm.CollectClientData(src)
+ SSbccm.HandleClientAccessCheck(src)
+ SSbccm.HandleASNbanCheck(src)
if(check_rights_for(src, R_DEBUG)) //check if autoadmin gave us it
debug_tools_allowed = TRUE
if(!debug_tools_allowed)
@@ -694,6 +697,15 @@ GLOBAL_LIST_INIT(warning_ckeys, list())
player_age = -1
. = player_age
+/client/proc/log_client_to_db_connection_log()
+ var/sql_ip = sql_sanitize_text(src.address)
+ var/sql_computerid = sql_sanitize_text(src.computer_id)
+ var/sql_ckey = sql_sanitize_text(src.ckey)
+ var/serverip = "[world.internet_address]:[world.port]"
+
+ var/datum/db_query/query_accesslog = SSdbcore.NewQuery("INSERT INTO `erro_connection_log`(`id`,`datetime`,`serverip`,`ckey`,`ip`,`computerid`) VALUES(null,Now(),'[serverip]','[sql_ckey]','[sql_ip]','[sql_computerid]');")
+ query_accesslog.Execute()
+ qdel(query_accesslog)
/client/proc/findJoinDate()
var/list/http = world.Export("http://byond.com/members/[ckey]?format=text")
if(!http)
diff --git a/code/modules/client/verbs/ooc.dm b/code/modules/client/verbs/ooc.dm
index 9bf75b1fc3a..f20f177d245 100644
--- a/code/modules/client/verbs/ooc.dm
+++ b/code/modules/client/verbs/ooc.dm
@@ -22,6 +22,12 @@ GLOBAL_VAR_INIT(normal_ooc_colour, "#002eb8")
if(prefs.muted & MUTE_OOC)
to_chat(src, span_danger("You cannot use OOC (muted)."))
return
+ var/static/regex/slurs = regex("nigg|fag|tranny|dyke|kike|pedo|loli|shota", "i")
+ if(findtext(msg, slurs))
+ to_chat(src, "Slurs are not allowed.")
+ log_admin("[key_name(src)] has triggered the slur filter (OOC): [msg].")
+ message_admins("[key_name_admin(src)] has triggered the slur filter (OOC): [msg].")
+ return 0
if(jobban_isbanned(src.mob, "OOC"))
to_chat(src, span_danger("You have been banned from OOC."))
return
diff --git a/hailmary.dme b/hailmary.dme
index 1b25d0bd690..cbfb75769c6 100644
--- a/hailmary.dme
+++ b/hailmary.dme
@@ -325,6 +325,7 @@
#include "code\controllers\subsystem\atoms.dm"
#include "code\controllers\subsystem\augury.dm"
#include "code\controllers\subsystem\autotransfer.dm"
+#include "code\controllers\subsystem\bccm.dm"
#include "code\controllers\subsystem\blackbox.dm"
#include "code\controllers\subsystem\callback.dm"
#include "code\controllers\subsystem\chat.dm"
@@ -1481,6 +1482,8 @@
#include "code\modules\admin\stickyban.dm"
#include "code\modules\admin\topic.dm"
#include "code\modules\admin\whitelist.dm"
+#include "code\modules\admin\bccm\tgui_asn.dm"
+#include "code\modules\admin\bccm\tgui_whitelist.dm"
#include "code\modules\admin\callproc\callproc.dm"
#include "code\modules\admin\DB_ban\functions.dm"
#include "code\modules\admin\verbs\adminhelp.dm"
diff --git a/tgui/packages/tgui/interfaces/BCCMASNPanel.tsx b/tgui/packages/tgui/interfaces/BCCMASNPanel.tsx
new file mode 100644
index 00000000000..913ccf74d74
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/BCCMASNPanel.tsx
@@ -0,0 +1,104 @@
+import { useBackend, useLocalState } from '../backend';
+import { Section, Flex, Stack, Button, Box, Input, NoticeBox } from '../components';
+import { Window } from '../layouts';
+
+type BCCMDisplayData = {
+ a_ckey: string;
+ timestamp: string;
+ asn: string;
+};
+
+type Data = {
+ displayData: Array;
+};
+
+export const BCCMASNPanel = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { displayData } = data;
+ const [inputIP, setinputIP] = useLocalState(context, 'inputIPkey', '');
+ return (
+
+
+
+
+ {((displayData?.length || 0) !== 0 && (
+
+
+
+
+ ASN
+
+
+ TIMESTAMP
+
+
+ ADMIN CKEY
+
+
+
+
+
+ {displayData.map((displayRow, index) => {
+ return (
+
+
+
+
+ {displayRow.asn}
+
+
+ {displayRow.timestamp}
+
+
+ {displayRow.a_ckey}
+
+
+
+
+
+
+ );
+ })}
+
+ )) || No ASN Ban entries to display. }
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/BCCMWhitelistPanel.tsx b/tgui/packages/tgui/interfaces/BCCMWhitelistPanel.tsx
new file mode 100644
index 00000000000..6f83759a6ea
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/BCCMWhitelistPanel.tsx
@@ -0,0 +1,108 @@
+import { useBackend, useLocalState } from '../backend';
+import { Section, Flex, Stack, Button, Box, Input, NoticeBox } from '../components';
+import { Window } from '../layouts';
+
+type BCCMDisplayData = {
+ ckey: string;
+ timestamp: string;
+ a_ckey: string;
+};
+
+type Data = {
+ displayData: Array;
+};
+
+export const BCCMWhitelistPanel = (props, context) => {
+ const { act, data } = useBackend(context);
+ const { displayData } = data;
+ const [inputWLCkey, setInputWLCkey] = useLocalState(
+ context,
+ 'inputWLCkey',
+ ''
+ );
+ return (
+
+
+
+
+ {((displayData?.length || 0)!== 0 && (
+
+
+
+
+ CKEY
+
+
+ TIMESTAMP
+
+
+ ADMIN CKEY
+
+
+
+
+
+ {displayData.map((displayRow, index) => {
+ return (
+
+
+
+
+ {displayRow.ckey}
+
+
+ {displayRow.timestamp}
+
+
+ {displayRow.a_ckey}
+
+
+
+
+
+
+ );
+ })}
+
+ )) || No whitelist entries to display. }
+
+
+
+ );
+};