diff --git a/SQL/database_changelog.md b/SQL/database_changelog.md
index 48090d7f212..65743042c51 100644
--- a/SQL/database_changelog.md
+++ b/SQL/database_changelog.md
@@ -2,21 +2,36 @@ Any time you make a change to the schema files, remember to increment the databa
Make sure to also update `DB_MAJOR_VERSION` and `DB_MINOR_VERSION`, which can be found in `code/__DEFINES/subsystem.dm`.
-The latest database version is 5.29 (5.26 for /tg/); The query to update the schema revision table is:
+The latest database version is 5.30 (5.27 for /tg/); The query to update the schema revision table is:
```sql
-INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 29);
+INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 30);
```
or
```sql
-INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 29);
+INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 30);
```
In any query remember to add a prefix to the table names if you use one.
-
-----------------------------------------------------
-Version 5.29, 08 January 2024, by distributivgesetz
+Version 5.30, 26 April 2024, by zephyrtfa
+Add the ip intel table
+```sql
+DROP TABLE IF EXISTS `ipintel`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `ipintel` (
+ `ip` int(10) unsigned NOT NULL,
+ `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `intel` double NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ip`),
+ KEY `idx_ipintel` (`ip`,`intel`,`date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+```
+-----------------------------------------------------
+Version 5.29, 08 January 2024, by Useroth
Add a new table for age-checking purposes. Optional if you don't ever intend to use the age prompt.
```sql
diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql
index e7167f766df..fba7edfcb7a 100644
--- a/SQL/tgstation_schema.sql
+++ b/SQL/tgstation_schema.sql
@@ -220,6 +220,20 @@ CREATE TABLE `ipintel` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
+--
+-- Table structure for table `ipintel_whitelist`
+--
+
+DROP TABLE IF EXISTS `ipintel_whitelist`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `ipintel_whitelist` (
+ `ckey` varchar(32) NOT NULL,
+ `admin_ckey` varchar(32) NOT NULL,
+ PRIMARY KEY (`ckey`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+
--
-- Table structure for table `legacy_population`
--
diff --git a/code/__DEFINES/admin_verb.dm b/code/__DEFINES/admin_verb.dm
index cbf856bf436..7e811c12196 100644
--- a/code/__DEFINES/admin_verb.dm
+++ b/code/__DEFINES/admin_verb.dm
@@ -87,6 +87,7 @@ _ADMIN_VERB(verb_path_name, verb_permissions, verb_name, verb_desc, verb_categor
#define ADMIN_CATEGORY_OBJECT "Object"
#define ADMIN_CATEGORY_MAPPING "Mapping"
#define ADMIN_CATEGORY_PROFILE "Profile"
+#define ADMIN_CATEGORY_IPINTEL "IPIntel"
// Visibility flags
#define ADMIN_VERB_VISIBLITY_FLAG_MAPPING_DEBUG "Map-Debug"
diff --git a/code/__DEFINES/ipintel.dm b/code/__DEFINES/ipintel.dm
new file mode 100644
index 00000000000..9fbc14ae40d
--- /dev/null
+++ b/code/__DEFINES/ipintel.dm
@@ -0,0 +1,15 @@
+#define IPINTEL_RATE_LIMIT_MINUTE "minute"
+#define IPINTEL_RATE_LIMIT_DAY "day"
+
+/// An internal error occurred and the query cannot be processed
+#define IPINTEL_UNKNOWN_INTERNAL_ERROR "unknown_internal_error"
+/// An error occurred with the query and the result is unknown
+#define IPINTEL_UNKNOWN_QUERY_ERROR "unknown_query_error"
+/// Cannot query as we are rate limited for the rest of the day
+#define IPINTEL_RATE_LIMITED_DAY "rate_limited_day"
+/// Cannot query as we are rate limited for the rest of the minute
+#define IPINTEL_RATE_LIMITED_MINUTE "rate_limited_minute"
+/// The IP address is a VPN or bad IP
+#define IPINTEL_BAD_IP "bad_ip"
+/// The IP address is not a VPN or bad IP
+#define IPINTEL_GOOD_IP "good_ip"
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 402b104cabf..ca94ab48e96 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -20,7 +20,7 @@
*
* make sure you add an update to the schema_version stable in the db changelog
*/
-#define DB_MINOR_VERSION 26
+#define DB_MINOR_VERSION 27
//! ## Timing subsystem
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index b3ff39ef1fe..73906998e01 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -451,6 +451,9 @@
/datum/config_entry/flag/irc_first_connection_alert // do we notify the irc channel when somebody is connecting for the first time?
+/datum/config_entry/string/ipintel_base
+ default = "check.getipintel.net"
+
/datum/config_entry/string/ipintel_email
/datum/config_entry/string/ipintel_email/ValidateAndSet(str_val)
@@ -462,18 +465,26 @@
min_val = 0
max_val = 1
-/datum/config_entry/number/ipintel_save_good
- default = 12
- integer = FALSE
- min_val = 0
+/datum/config_entry/flag/ipintel_reject_rate_limited
+ default = TRUE
-/datum/config_entry/number/ipintel_save_bad
- default = 1
- integer = FALSE
- min_val = 0
+/datum/config_entry/flag/ipintel_reject_bad
+ default = TRUE
-/datum/config_entry/string/ipintel_domain
- default = "check.getipintel.net"
+/datum/config_entry/flag/ipintel_reject_unknown
+ default = FALSE
+
+/datum/config_entry/number/ipintel_rate_minute
+ default = 15
+
+/datum/config_entry/number/ipintel_rate_day
+ default = 500
+
+/datum/config_entry/number/ipintel_cache_length
+ default = 7
+
+/datum/config_entry/number/ipintel_exempt_playtime_living
+ default = 0
/datum/config_entry/flag/aggressive_changelog
diff --git a/code/controllers/subsystem/ipintel.dm b/code/controllers/subsystem/ipintel.dm
index 83cbbc4c27e..3f1c738b09c 100644
--- a/code/controllers/subsystem/ipintel.dm
+++ b/code/controllers/subsystem/ipintel.dm
@@ -1,13 +1,295 @@
SUBSYSTEM_DEF(ipintel)
name = "XKeyScore"
init_order = INIT_ORDER_XKEYSCORE
- flags = SS_NO_FIRE
- var/enabled = FALSE //disable at round start to avoid checking reconnects
- var/throttle = 0
- var/errors = 0
+ flags = SS_INIT_NO_NEED|SS_NO_FIRE
+ /// The threshold for probability to be considered a VPN and/or bad IP
+ var/probability_threshold
+ /// The email used in conjuction with https://check.getipintel.net/check.php
+ var/contact_email
+ /// Maximum number of queries per minute
+ var/max_queries_per_minute
+ /// Maximum number of queries per day
+ var/max_queries_per_day
+ /// Query base
+ var/query_base
+ /// The length of time (days) to cache IP intel
+ var/ipintel_cache_length
+ /// The living playtime (minutes) for players to be exempt from IPIntel checks
+ var/exempt_living_playtime
- var/list/cache = list()
+ /// Cache for previously queried IP addresses and those stored in the database
+ var/list/datum/ip_intel/cached_queries = list()
+ /// The store for rate limiting
+ var/list/rate_limits
+
+/// The ip intel for a given address
+/datum/ip_intel
+ /// If this intel was just queried, the status of the query
+ var/query_status
+ var/result
+ var/address
+ var/date
/datum/controller/subsystem/ipintel/Initialize()
- enabled = TRUE
+ var/list/fail_messages = list()
+
+ probability_threshold = CONFIG_GET(number/ipintel_rating_bad)
+ if(probability_threshold < 0 || probability_threshold > 1)
+ fail_messages += list("invalid probability threshold")
+
+ contact_email = CONFIG_GET(string/ipintel_email)
+ if(isnull(contact_email) || !findtext(contact_email, "@"))
+ fail_messages += list("invalid contact email")
+
+ var/max_queries_per_minute = CONFIG_GET(number/ipintel_rate_minute)
+ var/max_queries_per_day = CONFIG_GET(number/ipintel_rate_day)
+ if(max_queries_per_minute < 0 || max_queries_per_day < 0)
+ fail_messages += list("invalid rate limits")
+
+ var/query_base = CONFIG_GET(string/ipintel_base)
+ if(isnull(query_base))
+ fail_messages += list("invalid query base")
+
+ var/ipintel_cache_length = CONFIG_GET(number/ipintel_cache_length)
+ if(ipintel_cache_length < 0)
+ fail_messages += list("invalid cache length")
+
+ var/exempt_living_playtime = CONFIG_GET(number/ipintel_exempt_playtime_living)
+ if(exempt_living_playtime < 0)
+ fail_messages += list("invalid exempt living playtime")
+
+ if(length(fail_messages))
+ message_admins("IPIntel: Initialization failed check logs!")
+ logger.Log(LOG_CATEGORY_GAME_ACCESS, "IPIntel failed to initialize.", list(
+ "fail_messages" = fail_messages,
+ ))
+ return SS_INIT_FAILURE
+
return SS_INIT_SUCCESS
+
+/datum/controller/subsystem/ipintel/stat_entry(msg)
+ return "[..()] | D: [max_queries_per_day - rate_limits[IPINTEL_RATE_LIMIT_DAY]] | M: [max_queries_per_minute - rate_limits[IPINTEL_RATE_LIMIT_MINUTE]]"
+
+/datum/controller/subsystem/ipintel/proc/get_address_intel_state(address, probability_override)
+ var/datum/ip_intel/intel = query_address(address)
+ if(isnull(intel))
+ stack_trace("query_address did not return an ip intel response")
+ return IPINTEL_UNKNOWN_INTERNAL_ERROR
+
+ if(istext(intel))
+ return intel
+
+ if(!(intel.query_status in list("success", "cached")))
+ return IPINTEL_UNKNOWN_QUERY_ERROR
+ var/check_probability = probability_override || probability_threshold
+ if(intel.result >= check_probability)
+ return IPINTEL_BAD_IP
+ return IPINTEL_GOOD_IP
+
+/datum/controller/subsystem/ipintel/proc/is_rate_limited()
+ var/static/minute_key
+ var/expected_minute_key = floor(REALTIMEOFDAY / 1 MINUTES)
+
+ if(minute_key != expected_minute_key)
+ minute_key = expected_minute_key
+ rate_limits[IPINTEL_RATE_LIMIT_MINUTE] = 0
+
+ if(rate_limits[IPINTEL_RATE_LIMIT_MINUTE] >= max_queries_per_minute)
+ return IPINTEL_RATE_LIMITED_MINUTE
+ if(rate_limits[IPINTEL_RATE_LIMIT_DAY] >= max_queries_per_day)
+ return IPINTEL_RATE_LIMITED_DAY
+ return FALSE
+
+/datum/controller/subsystem/ipintel/proc/query_address(address, allow_cached = TRUE)
+ if(allow_cached && fetch_cached_ip_intel(address))
+ return cached_queries[address]
+ var/is_rate_limited = is_rate_limited()
+ if(is_rate_limited)
+ return is_rate_limited
+ if(!initialized)
+ return IPINTEL_UNKNOWN_INTERNAL_ERROR
+
+ rate_limits[IPINTEL_RATE_LIMIT_MINUTE] += 1
+ rate_limits[IPINTEL_RATE_LIMIT_DAY] += 1
+
+ var/query_base = "https://[src.query_base]/check.php?ip="
+ var/query = "[query_base][address]&contact=[contact_email]&flags=b&format=json"
+
+ var/datum/http_request/request = new
+ request.prepare(RUSTG_HTTP_METHOD_GET, query)
+ request.execute_blocking()
+ var/datum/http_response/response = request.into_response()
+ var/list/data = response.body
+
+ var/datum/ip_intel/intel = new
+ intel.query_status = data["status"]
+ if(intel.query_status != "success")
+ return intel
+ intel.result = data["result"]
+ intel.date = SQLtime()
+ intel.address = address
+ cached_queries[address] = intel
+ add_intel_to_database(intel)
+ return intel
+
+/datum/controller/subsystem/ipintel/proc/add_intel_to_database(datum/ip_intel/intel)
+ var/datum/db_query/query = SSdbcore.NewQuery(
+ "INSERT INTO [format_table_name("ipintel")] ( \
+ ip, \
+ intel, \
+ ) VALUES ( \
+ INET_ATON(:address) \
+ :result, \
+ )", list(
+ "address" = intel.address,
+ "result" = intel.result,
+ )
+ )
+ query.warn_execute()
+ query.sync()
+ qdel(query)
+
+/datum/controller/subsystem/ipintel/proc/fetch_cached_ip_intel(address)
+ var/date_restrictor
+ if(ipintel_cache_length > 0)
+ date_restrictor = " AND date > DATE_SUB(NOW(), INTERVAL [ipintel_cache_length] DAY)"
+ var/datum/db_query/query = SSdbcore.NewQuery(
+ "SELECT * FROM [format_table_name("ipintel")] WHERE ip = INET_ATON(:address)[date_restrictor]", list(
+ "address" = address
+ )
+ )
+ query.warn_execute()
+ query.sync()
+ if(query.status == DB_QUERY_BROKEN)
+ qdel(query)
+ return null
+
+ query.NextRow()
+ var/list/data = query.item
+ qdel(query)
+ if(isnull(data))
+ return null
+
+ var/datum/ip_intel/intel = new
+ intel.query_status = "cached"
+ intel.result = data["intel"]
+ intel.date = data["date"]
+ intel.address = address
+ return TRUE
+
+/datum/controller/subsystem/ipintel/proc/is_exempt(client/player)
+ if(exempt_living_playtime > 0)
+ var/list/play_records = player.prefs.exp
+ if (!play_records.len)
+ player.set_exp_from_db()
+ play_records = player.prefs.exp
+ if(length(play_records) && play_records[EXP_TYPE_LIVING] > exempt_living_playtime)
+ return TRUE
+ return FALSE
+
+/datum/controller/subsystem/ipintel/proc/is_whitelisted(ckey)
+ var/datum/db_query/query = SSdbcore.NewQuery(
+ "SELECT * FROM [format_table_name("ipintel_whitelist")] WHERE ckey = :ckey", list(
+ "ckey" = ckey
+ )
+ )
+ query.warn_execute()
+ query.sync()
+ if(query.status == DB_QUERY_BROKEN)
+ qdel(query)
+ return FALSE
+ query.NextRow()
+ return !!query.item // if they have a row, they are whitelisted
+
+ADMIN_VERB(ipintel_allow, R_BAN, "Whitelist Player VPN", "Allow a player to connect even if they are using a VPN.", ADMIN_CATEGORY_IPINTEL, ckey as text)
+ if(SSipintel.is_whitelisted(ckey))
+ to_chat(user, "Player is already whitelisted.")
+ return
+
+ var/datum/db_query/query = SSdbcore.NewQuery(
+ "INSERT INTO [format_table_name("ipintel_whitelist")] ( \
+ ckey, \
+ admin_ckey \
+ ) VALUES ( \
+ :ckey, \
+ :admin_ckey \
+ )", list(
+ "ckey" = ckey,
+ "admin_ckey" = user.ckey,
+ )
+ )
+ query.warn_execute()
+ query.sync()
+ qdel(query)
+ message_admins("IPINTEL: [key_name_admin(user)] has whitelisted '[ckey]'")
+
+ADMIN_VERB(ipintel_revoke, R_BAN, "Revoke Player VPN Whitelist", "Revoke a player's VPN whitelist.", ADMIN_CATEGORY_IPINTEL, ckey as text)
+ if(!SSipintel.is_whitelisted(ckey))
+ to_chat(user, "Player is not whitelisted.")
+ return
+ var/datum/db_query/query = SSdbcore.NewQuery(
+ "DELETE FROM [format_table_name("ipintel_whitelist")] WHERE ckey = :ckey", list(
+ "ckey" = ckey
+ )
+ )
+ query.warn_execute()
+ query.sync()
+ qdel(query)
+ message_admins("IPINTEL: [key_name_admin(user)] has revoked the VPN whitelist for '[ckey]'")
+
+/client/proc/check_ip_intel()
+ if(SSipintel.is_exempt(src) || SSipintel.is_whitelisted(ckey))
+ return
+
+ var/intel_state = SSipintel.get_address_intel_state(address)
+ var/reject_bad_intel = CONFIG_GET(flag/ipintel_reject_bad)
+ var/reject_unknown_intel = CONFIG_GET(flag/ipintel_reject_unknown)
+ var/reject_rate_limited = CONFIG_GET(flag/ipintel_reject_rate_limited)
+
+ var/connection_rejected = FALSE
+ var/datum/ip_intel/intel = SSipintel.cached_queries[address]
+ switch(intel_state)
+ if(IPINTEL_BAD_IP)
+ log_access("IPINTEL: [ckey] was flagged as a VPN with [intel.result * 100]% likelihood.")
+ if(reject_bad_intel)
+ to_chat_immediate(src, span_boldnotice("Your connection has been detected as a VPN."))
+ connection_rejected = TRUE
+ else
+ message_admins("IPINTEL: [key_name_admin(src)] has been flagged as a VPN with [intel.result * 100]% likelihood.")
+
+ if(IPINTEL_RATE_LIMITED_DAY, IPINTEL_RATE_LIMITED_MINUTE)
+ log_access("IPINTEL: [ckey] was unable to be checked due to the rate limit.")
+ if(reject_rate_limited)
+ to_chat_immediate(src, span_boldnotice("New connections are not being allowed at this time."))
+ connection_rejected = TRUE
+ else
+ message_admins("IPINTEL: [key_name_admin(src)] was unable to be checked due to rate limiting.")
+
+ if(IPINTEL_UNKNOWN_INTERNAL_ERROR, IPINTEL_UNKNOWN_QUERY_ERROR)
+ log_access("IPINTEL: [ckey] unable to be checked due to an error.")
+ if(reject_unknown_intel)
+ to_chat_immediate(src, span_boldnotice("Your connection cannot be processed at this time."))
+ connection_rejected = TRUE
+ else
+ message_admins("IPINTEL: [key_name_admin(src)] was unable to be checked due to an error.")
+
+ if(!connection_rejected)
+ return
+
+ var/list/contact_where = list()
+ var/forum_url = CONFIG_GET(string/forumurl)
+ if(forum_url)
+ contact_where += list("Forums")
+ var/appeal_url = CONFIG_GET(string/banappeals)
+ if(appeal_url)
+ contact_where += list("Ban Appeals")
+
+ var/message_string = "Your connection has been rejected at this time. If you believe this is in error or have any questions please contact an admin"
+ if(length(contact_where))
+ message_string += " at [english_list(contact_where)]"
+ else
+ message_string += " somehow."
+ message_string += "."
+
+ to_chat_immediate(src, span_userdanger(message_string))
+ qdel(src)
diff --git a/code/modules/admin/ipintel.dm b/code/modules/admin/ipintel.dm
deleted file mode 100644
index 7ca4ccbc320..00000000000
--- a/code/modules/admin/ipintel.dm
+++ /dev/null
@@ -1,136 +0,0 @@
-/datum/ipintel
- var/ip
- var/intel = 0
- var/cache = FALSE
- var/cacheminutesago = 0
- var/cachedate = ""
- var/cacherealtime = 0
-
-/datum/ipintel/New()
- cachedate = SQLtime()
- cacherealtime = world.realtime
-
-/datum/ipintel/proc/is_valid()
- . = FALSE
- if (intel < 0)
- return
- if (intel <= CONFIG_GET(number/ipintel_rating_bad))
- if (world.realtime < cacherealtime + (CONFIG_GET(number/ipintel_save_good) * 60 * 60 * 10))
- return TRUE
- else
- if (world.realtime < cacherealtime + (CONFIG_GET(number/ipintel_save_bad) * 60 * 60 * 10))
- return TRUE
-
-/proc/get_ip_intel(ip, bypasscache = FALSE, updatecache = TRUE)
- var/datum/ipintel/res = new()
- res.ip = ip
- . = res
- if (!ip || !CONFIG_GET(string/ipintel_email) || !SSipintel.enabled)
- return
- if (!bypasscache)
- var/datum/ipintel/cachedintel = SSipintel.cache[ip]
- if (cachedintel?.is_valid())
- cachedintel.cache = TRUE
- return cachedintel
-
- if(SSdbcore.Connect())
- var/rating_bad = CONFIG_GET(number/ipintel_rating_bad)
- var/datum/db_query/query_get_ip_intel = SSdbcore.NewQuery({"
- SELECT date, intel, TIMESTAMPDIFF(MINUTE,date,NOW())
- FROM [format_table_name("ipintel")]
- WHERE
- ip = INET_ATON(':ip')
- AND ((
- intel < :rating_bad
- AND
- date + INTERVAL :save_good HOUR > NOW()
- ) OR (
- intel >= :rating_bad
- AND
- date + INTERVAL :save_bad HOUR > NOW()
- ))
- "}, list("ip" = ip, "rating_bad" = rating_bad, "save_good" = CONFIG_GET(number/ipintel_save_good), "save_bad" = CONFIG_GET(number/ipintel_save_bad)))
- if(!query_get_ip_intel.Execute())
- qdel(query_get_ip_intel)
- return
- if (query_get_ip_intel.NextRow())
- res.cache = TRUE
- res.cachedate = query_get_ip_intel.item[1]
- res.intel = text2num(query_get_ip_intel.item[2])
- res.cacheminutesago = text2num(query_get_ip_intel.item[3])
- res.cacherealtime = world.realtime - (text2num(query_get_ip_intel.item[3])*10*60)
- SSipintel.cache[ip] = res
- qdel(query_get_ip_intel)
- return
- qdel(query_get_ip_intel)
- res.intel = ip_intel_query(ip)
- if (updatecache && res.intel >= 0)
- SSipintel.cache[ip] = res
- if(SSdbcore.Connect())
- var/datum/db_query/query_add_ip_intel = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("ipintel")] (ip, intel) VALUES (INET_ATON(:ip), :intel) ON DUPLICATE KEY UPDATE intel = VALUES(intel), date = NOW()",
- list("ip" = ip, "intel" = res.intel)
- )
- query_add_ip_intel.Execute()
- qdel(query_add_ip_intel)
-
-
-/proc/ip_intel_query(ip, retryed=0)
- . = -1 //default
- if (!ip)
- return
- if (SSipintel.throttle > world.timeofday)
- return
- if (!SSipintel.enabled)
- return
-
- var/list/http[] = world.Export("http://[CONFIG_GET(string/ipintel_domain)]/check.php?ip=[ip]&contact=[CONFIG_GET(string/ipintel_email)]&format=json&flags=f")
-
- if (http)
- var/status = text2num(http["STATUS"])
-
- if (status == 200)
- var/response = json_decode(file2text(http["CONTENT"]))
- if (response)
- if (response["status"] == "success")
- var/intelnum = text2num(response["result"])
- if (isnum(intelnum))
- return text2num(response["result"])
- else
- ipintel_handle_error("Bad intel from server: [response["result"]].", ip, retryed)
- if (!retryed)
- sleep(2.5 SECONDS)
- return .(ip, 1)
- else
- ipintel_handle_error("Bad response from server: [response["status"]].", ip, retryed)
- if (!retryed)
- sleep(2.5 SECONDS)
- return .(ip, 1)
-
- else if (status == 429)
- ipintel_handle_error("Error #429: We have exceeded the rate limit.", ip, 1)
- return
- else
- ipintel_handle_error("Unknown status code: [status].", ip, retryed)
- if (!retryed)
- sleep(2.5 SECONDS)
- return .(ip, 1)
- else
- ipintel_handle_error("Unable to connect to API.", ip, retryed)
- if (!retryed)
- sleep(2.5 SECONDS)
- return .(ip, 1)
-
-
-/proc/ipintel_handle_error(error, ip, retryed)
- if (retryed)
- SSipintel.errors++
- error += " Could not check [ip]. Disabling IPINTEL for [SSipintel.errors] minute[( SSipintel.errors == 1 ? "" : "s" )]"
- SSipintel.throttle = world.timeofday + (10 * 120 * SSipintel.errors)
- else
- error += " Attempting retry on [ip]."
- log_ipintel(error)
-
-/proc/log_ipintel(text)
- log_game("IPINTEL: [text]")
- debug_admins("IPINTEL: [text]")
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index b3e18ce5b0c..ce4922b9d84 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -860,15 +860,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
qdel(query_get_notes)
create_message("note", key, system_ckey, message, null, null, 0, 0, null, 0, 0)
-
-/client/proc/check_ip_intel()
- set waitfor = 0 //we sleep when getting the intel, no need to hold up the client connection while we sleep
- if (CONFIG_GET(string/ipintel_email))
- var/datum/ipintel/res = get_ip_intel(address)
- if (res.intel >= CONFIG_GET(number/ipintel_rating_bad))
- message_admins(span_adminnotice("Proxy Detection: [key_name_admin(src)] IP intel rated [res.intel*100]% likely to be a Proxy/VPN."))
- ip_intel = res.intel
-
/client/Click(atom/object, atom/location, control, params)
if(click_intercept_time)
if(click_intercept_time >= world.time)
diff --git a/config/config.txt b/config/config.txt
index 5a550aea314..ebaf538e7b6 100644
--- a/config/config.txt
+++ b/config/config.txt
@@ -153,16 +153,26 @@ GUEST_BAN
## IPINTEL:
## This allows you to detect likely proxies by checking ips against getipintel.net
-## Rating to warn at: (0.9 is good, 1 is 100% likely to be a spammer/proxy, 0.8 is 80%, etc) anything equal to or higher then this number triggers an admin warning
-#IPINTEL_RATING_BAD 0.9
+## Rating to warn at: (0.8 is good, 1 is 100% likely to be a spammer/proxy, 0.8 is 80%, etc) anything equal to or higher then this number triggers an admin warning
+#IPINTEL_RATING_BAD 0.8
## Contact email, (required to use the service, leaving blank or default disables IPINTEL)
#IPINTEL_EMAIL ch@nge.me
-## How long to save good matches (ipintel rate limits to 15 per minute and 500 per day. so this shouldn't be too low, getipintel.net suggests 6 hours, time is in hours) (Your ip will get banned if you go over 500 a day too many times)
-#IPINTEL_SAVE_GOOD 12
-## How long to save bad matches (these numbers can change as ips change hands, best not to save these for too long in case somebody gets a new ip used by a spammer/proxy before.)
-#IPINTEL_SAVE_BAD 3
-## Domain name to query (leave commented out for the default, only needed if you pay getipintel.net for more querys)
-#IPINTEL_DOMAIN check.getipintel.net
+## Query base, if you pay for more queries this is what you want to change.
+#IPINTEL_BASE check.getipintel.net
+## Maximum number of queries in a minute
+#IPINTEL_MAX_QUERY_MINUTE 15
+## Maximum number of queries in a day
+#IPINTEL_MAX_QUERY_DAY 500
+## Whether clients which cannot be checked due to a rate limit will be denied
+#IPINTEL_REJECT_RATE_LIMITED
+## Whether clients which are flagged as a VPN will be denied
+IPINTEL_REJECT_BAD
+## Whether clients which cannot be checked due to an error of some form will be denied
+#IPINTEL_REJECT_UNKNOWN
+## How long to store results in the cache before they must be retrieved again. IN DAYS.
+#IPINTEL_CACHE_LENGTH 7
+## How many minutes of living playtime to be automatically exempt from IPIntel. 0 for never.
+#IPINTEL_EXEMPT_PLAYTIME_LIVING 0
## Uncomment to allow web client connections
#ALLOW_WEBCLIENT
diff --git a/tgstation.dme b/tgstation.dme
index e3b896d2934..01d225fc5bb 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -117,6 +117,7 @@
#include "code\__DEFINES\instruments.dm"
#include "code\__DEFINES\interaction_flags.dm"
#include "code\__DEFINES\inventory.dm"
+#include "code\__DEFINES\ipintel.dm"
#include "code\__DEFINES\is_helpers.dm"
#include "code\__DEFINES\jobs.dm"
#include "code\__DEFINES\keybinding.dm"
@@ -2890,7 +2891,6 @@
#include "code\modules\admin\fun_balloon.dm"
#include "code\modules\admin\greyscale_modify_menu.dm"
#include "code\modules\admin\holder2.dm"
-#include "code\modules\admin\ipintel.dm"
#include "code\modules\admin\IsBanned.dm"
#include "code\modules\admin\known_alts.dm"
#include "code\modules\admin\outfit_editor.dm"