world/Topic Refactor (#14850)

* world/Topic Refactor

* These arent needed anymore

* How could I forget this

* Biggest edit ever

* Forgot this

* Mini refactor
This commit is contained in:
AffectedArc07
2020-12-09 16:24:33 +00:00
committed by GitHub
parent c5d66be971
commit d6af52dd2b
14 changed files with 316 additions and 240 deletions

View File

@@ -117,6 +117,17 @@
if(R.selectable)
GLOB.selectable_robolimbs[R.company] = R
// Setup world topic handlers
for(var/topic_handler_type in subtypesof(/datum/world_topic_handler))
var/datum/world_topic_handler/wth = new topic_handler_type()
if(!wth.topic_key)
stack_trace("[wth.type] has no topic key!")
continue
if(GLOB.world_topic_handlers[wth.topic_key])
stack_trace("[wth.type] has the same topic key as [GLOB.world_topic_handlers[wth.topic_key]]! ([wth.topic_key])")
continue
GLOB.world_topic_handlers[wth.topic_key] = topic_handler_type
/* // Uncomment to debug chemical reaction list.
/client/verb/debug_chemical_list()

View File

@@ -55,242 +55,41 @@ GLOBAL_LIST_INIT(map_transition_config, MAP_TRANSITION_CONFIG)
investigate_reset() // This is part of the admin investigate system. PLEASE DONT SS THIS EITHER
makeDatumRefLists() // Setups up lists of datums and their subtypes
//world/Topic(href, href_list[])
// to_chat(world, "Received a Topic() call!")
// to_chat(world, "[href]")
// for(var/a in href_list)
// to_chat(world, "[a]")
// if(href_list["hello"])
// to_chat(world, "Hello world!")
// return "Hello world!"
// to_chat(world, "End of Topic() call.")
// ..()
/// List of all world topic spam prevention handlers. See code/modules/world_topic/_spam_prevention_handler.dm
GLOBAL_LIST_EMPTY(world_topic_spam_prevention_handlers)
/// List of all world topic handler datums. Populated inside makeDatumRefLists()
GLOBAL_LIST_EMPTY(world_topic_handlers)
GLOBAL_VAR_INIT(world_topic_spam_protect_ip, "0.0.0.0")
GLOBAL_VAR_INIT(world_topic_spam_protect_time, world.timeofday)
/world/Topic(T, addr, master, key)
TGS_TOPIC
log_misc("WORLD/TOPIC: \"[T]\", from:[addr], master:[master], key:[key]")
// Handle spam prevention
if(!GLOB.world_topic_spam_prevention_handlers[address])
GLOB.world_topic_spam_prevention_handlers[address] = new /datum/world_topic_spam_prevention_handler
var/datum/world_topic_spam_prevention_handler/sph = GLOB.world_topic_spam_prevention_handlers[address]
// Lock the user out and cancel their topic if needed
if(sph.check_lockout())
return
var/list/input = params2list(T)
var/key_valid = (config.comms_password && input["key"] == config.comms_password) //no password means no comms, not any password
if("ping" in input)
var/x = 1
for(var/client/C)
x++
return x
var/datum/world_topic_handler/wth
else if("players" in input)
var/n = 0
for(var/mob/M in GLOB.player_list)
if(M.client)
n++
return n
else if("status" in input)
var/list/s = list()
var/list/admins = list()
s["version"] = GLOB.game_version
s["mode"] = GLOB.master_mode
s["respawn"] = config ? GLOB.abandon_allowed : 0
s["enter"] = GLOB.enter_allowed
s["vote"] = config.allow_vote_mode
s["ai"] = config.allow_ai
s["host"] = host ? host : null
s["players"] = list()
s["roundtime"] = worldtime2text()
s["stationtime"] = station_time_timestamp()
s["oldstationtime"] = classic_worldtime2text() // more "consistent" indication of the round's running time
s["listed"] = "Public"
if(!hub_password)
s["listed"] = "Invisible"
var/player_count = 0
var/admin_count = 0
for(var/client/C in GLOB.clients)
if(C.holder)
if(C.holder.fakekey)
continue //so stealthmins aren't revealed by the hub
admin_count++
admins += list(list(C.key, C.holder.rank))
s["player[player_count]"] = C.key
player_count++
s["players"] = player_count
s["admins"] = admin_count
s["map_name"] = GLOB.map_name ? GLOB.map_name : "Unknown"
if(key_valid)
if(SSticker && SSticker.mode)
s["real_mode"] = SSticker.mode.name
s["security_level"] = get_security_level()
s["ticker_state"] = SSticker.current_state
if(SSshuttle && SSshuttle.emergency)
// Shuttle status, see /__DEFINES/stat.dm
s["shuttle_mode"] = SSshuttle.emergency.mode
// Shuttle timer, in seconds
s["shuttle_timer"] = SSshuttle.emergency.timeLeft()
for(var/i in 1 to admins.len)
var/list/A = admins[i]
s["admin[i - 1]"] = A[1]
s["adminrank[i - 1]"] = A[2]
return list2params(s)
else if("manifest" in input)
var/list/positions = list()
var/list/set_names = list(
"heads" = GLOB.command_positions,
"sec" = GLOB.security_positions,
"eng" = GLOB.engineering_positions,
"med" = GLOB.medical_positions,
"sci" = GLOB.science_positions,
"car" = GLOB.supply_positions,
"srv" = GLOB.service_positions,
"civ" = GLOB.civilian_positions,
"bot" = GLOB.nonhuman_positions
)
for(var/datum/data/record/t in GLOB.data_core.general)
var/name = t.fields["name"]
var/rank = t.fields["rank"]
var/real_rank = t.fields["real_rank"]
var/department = 0
for(var/k in set_names)
if(real_rank in set_names[k])
if(!positions[k])
positions[k] = list()
positions[k][name] = rank
department = 1
if(!department)
if(!positions["misc"])
positions["misc"] = list()
positions["misc"][name] = rank
return json_encode(positions)
else if("adminmsg" in input)
/*
We got an adminmsg from IRC bot lets split the input then validate the input.
expected output:
1. adminmsg = ckey of person the message is to
2. msg = contents of message, parems2list requires
3. validatationkey = the key the bot has, it should match the gameservers commspassword in it's configuration.
4. sender = the ircnick that send the message.
*/
if(!key_valid)
return keySpamProtect(addr)
var/client/C
for(var/client/K in GLOB.clients)
if(K.ckey == input["adminmsg"])
C = K
for(var/H in GLOB.world_topic_handlers)
if(H in input)
wth = GLOB.world_topic_handlers[H]
break
if(!C)
return "No client with that name on server"
var/message = "<font color='red'>Discord PM from <b><a href='?discord_msg=1'>[input["sender"]]</a></b>: [input["msg"]]</font>"
var/amessage = "<font color='blue'>Discord PM from <a href='?discord_msg=1'>[input["sender"]]</a> to <b>[key_name(C)]</b>: [input["msg"]]</font>"
if(!wth)
return
// THESE TWO VARS DO VERY DIFFERENT THINGS. DO NOT ATTEMPT TO COMBINE THEM
C.received_discord_pm = world.time
C.last_discord_pm_time = 0
SEND_SOUND(C, 'sound/effects/adminhelp.ogg')
to_chat(C, message)
for(var/client/A in GLOB.admins)
if(A != C)
to_chat(A, amessage)
return "Message Successful"
else if("notes" in input)
/*
We got a request for notes from the IRC Bot
expected output:
1. notes = ckey of person the notes lookup is for
2. validationkey = the key the bot has, it should match the gameservers commspassword in it's configuration.
*/
if(!key_valid)
return keySpamProtect(addr)
return show_player_info_irc(input["notes"])
else if("announce" in input)
if(config.comms_password)
if(input["key"] != config.comms_password)
return "Bad Key"
else
var/prtext = input["announce"]
var/pr_substring = copytext(prtext, 1, 23)
if(pr_substring == "Pull Request merged by")
GLOB.pending_server_update = TRUE
for(var/client/C in GLOB.clients)
to_chat(C, "<span class='announce'>PR: [prtext]</span>")
else if("kick" in input)
/*
We have a kick request over coms.
Only needed portion is the ckey
*/
if(!key_valid)
return keySpamProtect(addr)
var/client/C
for(var/client/K in GLOB.clients)
if(K.ckey == input["kick"])
C = K
break
if(!C)
return "No client with that name on server"
qdel(C)
return "Kick Successful"
else if("setlog" in input)
if(!key_valid)
return keySpamProtect(addr)
SetupLogs()
return "Logs set to current date"
else if("setlist" in input)
if(!key_valid)
return keySpamProtect(addr)
if(input["req"] == "public")
hub_password = initial(hub_password)
update_status()
return "Set listed status to public."
else
hub_password = ""
update_status()
return "Set listed status to invisible."
else if("hostannounce" in input)
if(!key_valid)
return keySpamProtect(addr)
GLOB.pending_server_update = TRUE
to_chat(world, "<hr><span style='color: #12A5F4'><b>Server Announcement:</b> [input["message"]]</span><hr>")
/proc/keySpamProtect(var/addr)
if(GLOB.world_topic_spam_protect_ip == addr && abs(GLOB.world_topic_spam_protect_time - world.time) < 50)
spawn(50)
GLOB.world_topic_spam_protect_time = world.time
return "Bad Key (Throttled)"
GLOB.world_topic_spam_protect_time = world.time
GLOB.world_topic_spam_protect_ip = addr
return "Bad Key"
// If we are here, the handler exists, so it needs to be invoked
wth = new wth()
return wth.invoke(input)
/world/Reboot(var/reason, var/feedback_c, var/feedback_r, var/time)
TgsReboot()

View File

@@ -202,18 +202,3 @@
output += ruler
usr << browse(output, "window=show_notes;size=900x500")
/proc/show_player_info_irc(var/key as text)
var/target_sql_ckey = ckey(key)
var/DBQuery/query_get_notes = GLOB.dbcon.NewQuery("SELECT timestamp, notetext, adminckey, server, crew_playtime FROM [format_table_name("notes")] WHERE ckey = '[target_sql_ckey]' ORDER BY timestamp")
if(!query_get_notes.Execute())
var/err = query_get_notes.ErrorMsg()
log_game("SQL ERROR obtaining timestamp, notetext, adminckey, server, crew_playtime from notes table. Error : \[[err]\]\n")
return
var/output = " Info on [key]%0D%0A"
while(query_get_notes.NextRow())
var/timestamp = query_get_notes.item[1]
var/notetext = query_get_notes.item[2]
var/adminckey = query_get_notes.item[3]
var/server = query_get_notes.item[4]
output += "[notetext]%0D%0Aby [adminckey] on [timestamp] (Server: [server])%0D%0A%0D%0A"
return output

View File

@@ -0,0 +1,5 @@
# NOTE
Please keep things in this directory standardised!
This means one topic handler per DM file, and the filename being the key. For an example, see `ping.dm`

View File

@@ -0,0 +1,63 @@
#define WORLD_TOPIC_STRIKES_THRESHOLD 5
#define WORLD_TOPIC_LOCKOUT_TIME 1 MINUTES
/datum/world_topic_spam_prevention_handler
/// Amount of strikes. [WORLD_TOPIC_STRIKES_THRESHOLD] strikes is a lockout of [WORLD_TOPIC_LOCKOUT_TIME]
var/strikes = 0
/// Time of last request
var/last_request = 0
/// Is this IP currently locked out
var/locked_out = FALSE
/// Unlock time
var/unlock_time = 0
/**
* Lockout handler
*
* Updates strikes and timers of the most recent client to topic the server
* including any relevant detail
*/
/datum/world_topic_spam_prevention_handler/proc/check_lockout()
// Check if they are already locked out
if(locked_out && (unlock_time >= world.time))
// Relock out for another minute if youre spamming
unlock_time = world.time + WORLD_TOPIC_LOCKOUT_TIME
return TRUE
// If they were locked out and are now allowed, unlock them
if(locked_out && (unlock_time < world.time))
strikes = 0
locked_out = FALSE
// Allow a grace period of 0.5 seconds per topic, or you get a strike
if(last_request + 5 > world.time)
strikes++
if(strikes >= WORLD_TOPIC_STRIKES_THRESHOLD)
locked_out = TRUE
unlock_time = world.time + WORLD_TOPIC_LOCKOUT_TIME
return TRUE
// If we got here, assume they arent locked out
last_request = world.time
return FALSE
/*
Uncomment this if you modify the topic limiter, trust me, youll need to test it
/client/verb/debug_limiter()
if(!GLOB.world_topic_spam_prevention_handlers[address])
GLOB.world_topic_spam_prevention_handlers[address] = new /datum/world_topic_spam_prevention_handler
var/datum/world_topic_spam_prevention_handler/sph = GLOB.world_topic_spam_prevention_handlers[address]
var/result = sph.check_lockout()
to_chat(usr, "Strikes: [sph.strikes]")
to_chat(usr, "Last request: [sph.last_request]")
to_chat(usr, "Locked out: [sph.locked_out]")
to_chat(usr, "Unlock time: [sph.unlock_time]")
to_chat(usr, "SPH Result: [result]")
to_chat(usr, "World.time: [world.time]")
to_chat(usr, "LR DIFF: [(sph.last_request - world.time)/10]s")
to_chat(usr, "LO DIFF: [(sph.unlock_time - world.time)/10]s")
to_chat(usr, "<hr>")
*/

View File

@@ -0,0 +1,36 @@
/datum/world_topic_handler
/// Key which invokes this topic
var/topic_key
/// Set this to TRUE if the topic handler needs an authorised comms key
var/requires_commskey = FALSE
/**
* Invokes the world/Topic handler
*
* This includes sanity checking for if the key is required, as well as other sanity checks
* DO NOT OVERRIDE
* Arguments:
* * input - The list of topic data, sent from [world/Topic]
*/
/datum/world_topic_handler/proc/invoke(list/input)
SHOULD_NOT_OVERRIDE(TRUE)
var/authorised = (config.comms_password && input["key"] == config.comms_password) // No password means no comms, not any password
if(requires_commskey && !authorised)
// Try keep all returns in JSON unless absolutely necessary (?ping for example)
return(json_encode(list("error" = "Invalid Key")))
return execute(input, authorised)
/**
* Actually executes the user's topic
*
* Override this to do your work in subtypes of this topic
*
* Arguments:
* * input - The list of topic data, sent from [world/Topic]
* * key_valid - Has the user entered the correct auth key
*/
/datum/world_topic_handler/proc/execute(list/input, key_valid = FALSE)
PRIVATE_PROC(TRUE) // Ensures no one can call this arbitrarily without checking key auth
CRASH("execute() not implemented/overridden for [type]")

View File

@@ -0,0 +1,38 @@
/datum/world_topic_handler/adminmsg
topic_key = "adminmsg"
requires_commskey = TRUE
/datum/world_topic_handler/adminmsg/execute(list/input, key_valid)
/*
We got an adminmsg from the Discord bot, so lets split the input then validate the input. Expected output:
1. adminmsg = ckey of person the message is to
2. msg = contents of message, params2list requires
3. sender = the discord name that send the message.
*/
var/client/C
for(var/client/K in GLOB.clients)
if(K.ckey == input["adminmsg"])
C = K
break
if(!C)
return json_encode(list("error" = "No client with that name on server"))
var/message = "<font color='red'>Discord PM from <b><a href='?discord_msg=1'>[input["sender"]]</a></b>: [input["msg"]]</font>"
var/amessage = "<font color='blue'>Discord PM from <a href='?discord_msg=1'>[input["sender"]]</a> to <b>[key_name_admin(C)]</b>: [input["msg"]]</font>"
// THESE TWO VARS DO VERY DIFFERENT THINGS. DO NOT ATTEMPT TO COMBINE THEM
C.received_discord_pm = world.time
C.last_discord_pm_time = 0
SEND_SOUND(C, 'sound/effects/adminhelp.ogg')
to_chat(C, message)
for(var/client/A in GLOB.admins)
// GLOB.admins includes anyone with a holder datum (mentors too). This makes sure only admins see ahelps
if(check_rights(R_ADMIN, FALSE, A.mob))
if(A != C)
to_chat(A, amessage)
return json_encode(list("success" = "Message Successful"))

View File

@@ -0,0 +1,11 @@
/datum/world_topic_handler/announce
topic_key = "announce"
requires_commskey = TRUE
/datum/world_topic_handler/announce/execute(list/input, key_valid)
var/prtext = input["announce"]
var/pr_substring = copytext(prtext, 1, 23)
if(pr_substring == "Pull Request merged by")
GLOB.pending_server_update = TRUE
for(var/client/C in GLOB.clients)
to_chat(C, "<span class='announce'>PR: [prtext]</span>")

View File

@@ -0,0 +1,7 @@
/datum/world_topic_handler/hostannounce
topic_key = "hostannounce"
requires_commskey = TRUE
/datum/world_topic_handler/hostannounce/execute(list/input, key_valid)
GLOB.pending_server_update = TRUE
to_chat(world, "<hr><span style='color: #12A5F4'><b>Server Announcement:</b> [input["message"]]</span><hr>")

View File

@@ -0,0 +1,35 @@
/datum/world_topic_handler/manifest
topic_key = "manifest"
/datum/world_topic_handler/manifest/execute(list/input, key_valid)
var/list/positions = list()
var/list/set_names = list(
"heads" = GLOB.command_positions,
"sec" = GLOB.security_positions,
"eng" = GLOB.engineering_positions,
"med" = GLOB.medical_positions,
"sci" = GLOB.science_positions,
"car" = GLOB.supply_positions,
"srv" = GLOB.service_positions,
"civ" = GLOB.civilian_positions,
"bot" = GLOB.nonhuman_positions
)
for(var/datum/data/record/t in GLOB.data_core.general)
var/name = t.fields["name"]
var/rank = t.fields["rank"]
var/real_rank = t.fields["real_rank"]
var/department = FALSE
for(var/k in set_names)
if(real_rank in set_names[k])
if(!positions[k])
positions[k] = list()
positions[k][name] = rank
department = TRUE
if(!department)
if(!positions["misc"])
positions["misc"] = list()
positions["misc"][name] = rank
return json_encode(positions)

View File

@@ -0,0 +1,14 @@
/datum/world_topic_handler/ping
topic_key = "ping"
/datum/world_topic_handler/ping/execute(list/input, key_valid)
/*
Basically a more efficient version of
if("ping" in input)
var/x = 1
for(var/client/C)
x++
return x
*/
return length(GLOB.clients) + 1

View File

@@ -0,0 +1,10 @@
/datum/world_topic_handler/playerlist
topic_key = "playerlist"
/datum/world_topic_handler/playerlist/execute(list/input, key_valid)
var/list/keys = list()
for(var/I in GLOB.clients)
var/client/C = I
keys += C.key
return json_encode(keys)

View File

@@ -0,0 +1,53 @@
/datum/world_topic_handler/status
topic_key = "status"
/datum/world_topic_handler/status/execute(list/input, key_valid)
var/list/status_info = list()
var/list/admins = list()
status_info["version"] = GLOB.revision_info.commit_hash
status_info["mode"] = GLOB.master_mode
status_info["respawn"] = GLOB.abandon_allowed
status_info["enter"] = GLOB.enter_allowed
status_info["vote"] = config.allow_vote_mode
status_info["ai"] = config.allow_ai
status_info["host"] = world.host ? world.host : null
status_info["players"] = list()
status_info["roundtime"] = worldtime2text()
status_info["stationtime"] = station_time_timestamp()
status_info["oldstationtime"] = classic_worldtime2text() // more "consistent" indication of the round's running time
status_info["listed"] = "Public"
if(!world.hub_password)
status_info["listed"] = "Invisible"
var/player_count = 0
var/admin_count = 0
for(var/client/C in GLOB.clients)
if(C.holder)
if(C.holder.fakekey)
continue //so stealthmins aren't revealed by the hub
admin_count++
admins += list(list(C.key, C.holder.rank))
player_count++
status_info["players"] = player_count
status_info["admins"] = admin_count
status_info["map_name"] = GLOB.map_name ? GLOB.map_name : "Unknown"
// Add more info if we are authed
if(key_valid)
if(SSticker && SSticker.mode)
status_info["real_mode"] = SSticker.mode.name
status_info["security_level"] = get_security_level()
status_info["ticker_state"] = SSticker.current_state
if(SSshuttle && SSshuttle.emergency)
// Shuttle status, see /__DEFINES/stat.dm
status_info["shuttle_mode"] = SSshuttle.emergency.mode
// Shuttle timer, in seconds
status_info["shuttle_timer"] = SSshuttle.emergency.timeLeft()
for(var/i in 1 to admins.len)
var/list/A = admins[i]
status_info["admin[i - 1]"] = A[1]
status_info["adminrank[i - 1]"] = A[2]
return json_encode(status_info)

View File

@@ -2470,6 +2470,15 @@
#include "code\modules\vehicle\speedbike.dm"
#include "code\modules\vehicle\sportscar.dm"
#include "code\modules\vehicle\vehicle.dm"
#include "code\modules\world_topic\_spam_prevention_handler.dm"
#include "code\modules\world_topic\_topic_base.dm"
#include "code\modules\world_topic\adminmsg.dm"
#include "code\modules\world_topic\announce.dm"
#include "code\modules\world_topic\hostannounce.dm"
#include "code\modules\world_topic\manifest.dm"
#include "code\modules\world_topic\ping.dm"
#include "code\modules\world_topic\players.dm"
#include "code\modules\world_topic\status.dm"
#include "goon\code\datums\browserOutput.dm"
#include "interface\interface.dm"
#include "interface\skin.dmf"