diff --git a/aurorastation.dme b/aurorastation.dme
index ef5c1a8ceb8..6db3b6aa0c9 100644
--- a/aurorastation.dme
+++ b/aurorastation.dme
@@ -221,7 +221,6 @@
#include "code\controllers\subsystems\processing\vueui.dm"
#include "code\datums\ai_law_sets.dm"
#include "code\datums\ai_laws.dm"
-#include "code\datums\api.dm"
#include "code\datums\beam.dm"
#include "code\datums\browser.dm"
#include "code\datums\callback.dm"
@@ -2425,6 +2424,15 @@
#include "code\modules\vueui\ui.dm"
#include "code\modules\vueui\var_monitor.dm"
#include "code\modules\web_interface\webint_procs.dm"
+#include "code\modules\world_api\api_command.dm"
+#include "code\modules\world_api\helpers.dm"
+#include "code\modules\world_api\commands\admin.dm"
+#include "code\modules\world_api\commands\api_helpers.dm"
+#include "code\modules\world_api\commands\cciaa.dm"
+#include "code\modules\world_api\commands\misc.dm"
+#include "code\modules\world_api\commands\server_management.dm"
+#include "code\modules\world_api\commands\server_query.dm"
+#include "code\modules\world_api\commands\tickets.dm"
#include "code\modules\xgm\xgm_gas_data.dm"
#include "code\modules\xgm\xgm_gas_mixture.dm"
#include "code\unit_tests\chemistry_tests.dm"
diff --git a/code/datums/api.dm b/code/datums/api.dm
deleted file mode 100644
index 6ba42b538e2..00000000000
--- a/code/datums/api.dm
+++ /dev/null
@@ -1,1226 +0,0 @@
-//
-// This file contains the API commands for the serverside API
-//
-// IMPORTANT:
-// When changing api commands always update the version number of the API
-// The version number is defined in /datum/topic_command/api_get_version
-
-//Init the API at startup
-/hook/startup/proc/setup_api()
- for (var/path in typesof(/datum/topic_command) - /datum/topic_command)
- var/datum/topic_command/A = new path()
- if(A != null)
- topic_commands[A.name] = A
- topic_commands_names.Add(A.name)
- listclearnulls(topic_commands)
- listclearnulls(topic_commands_names)
-
- if (config.api_rate_limit_whitelist.len)
- // To make the api_rate_limit_whitelist[addr] grabs actually work.
- for (var/addr in config.api_rate_limit_whitelist)
- config.api_rate_limit_whitelist[addr] = 1
-
- return 1
-
-/world/proc/api_do_auth_check(var/addr, var/auth, var/datum/topic_command/command)
- //Check if command is on nothrottle list
- if(command.no_throttle == 1)
- log_debug("API: Throttling bypassed - Command [command.name] set to no_throttle")
- else
- if(world_api_rate_limit[addr] != null && config.api_rate_limit_whitelist[addr] == null) //Check if the ip is in the rate limiting list and not in the whitelist
- if(abs(world_api_rate_limit[addr] - world.time) < config.api_rate_limit) //Check the last request time of the ip
- world_api_rate_limit[addr] = world.time // Set the time of the last request
- return 2 //Throttled
- world_api_rate_limit[addr] = world.time // Set the time of the last request
-
-
- //Check if the command is on the auth whitelist
- if(command.no_auth == 1)
- log_debug("API: Auth bypassed - Command [command.name] set to no_auth")
- return 0 // Authed (bypassed)
-
- var/DBQuery/authquery = dbcon.NewQuery({"SELECT api_f.command
- FROM ss13_api_token_command as api_t_f, ss13_api_tokens as api_t, ss13_api_commands as api_f
- WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.command_id
- AND api_t.deleted_at IS NULL
- AND (
- (token = :token: AND ip = :ip: AND command = :command:)
- OR
- (token = :token: AND ip IS NULL AND command = :command:)
- OR
- (token = :token: AND ip = :ip: AND command = \"_ANY\")
- OR
- (token = :token: AND ip IS NULL AND command = \"_ANY\")
- OR
- (token IS NULL AND ip IS NULL AND command = :command:)
- )"})
- //Check if the token is not deleted
- //Check if one of the following is true:
- // Full Match - Token IP and Command Matches
- // Any IP - Token and Command Matches, IP is set to NULL (not required)
- // Any Command - Token and IP Matches, Command is set to _ANY
- // Any Command, Any IP - Token Matches, IP is set to NULL (not required), Command is set to _ANY
- // Public - Token is set to NULL, IP is set to NULL and command matches
-
- authquery.Execute(list("token" = auth, "ip" = addr, "command" = command.name))
- log_debug("API: Auth Check - Query Executed - Returned Rows: [authquery.RowCount()]")
-
- if (authquery.RowCount())
- return 0 // Authed
- return 1 // Bad Key
-
-
-proc/api_update_command_database()
- log_debug("API: DB Command Update Called")
- //Check if DB Connection is established
- if (!establish_db_connection(dbcon))
- return 0 //Error
-
- var/DBQuery/commandinsertquery = dbcon.NewQuery({"INSERT INTO ss13_api_commands (command,description)
- VALUES (:command_name:,:command_description:)
- ON DUPLICATE KEY UPDATE description = :command_description:;"})
-
- for(var/com in topic_commands)
- var/datum/topic_command/command = topic_commands[com]
- commandinsertquery.Execute(list("command_name" = command.name, "command_description" = command.description))
- log_debug("API: DB Command Update Executed")
- return 1 //OK
-
-//API Boilerplate
-/datum/topic_command
- var/name = null //Name for the command
- var/no_auth = 0 //If the user does NOT need to be authed to use the command
- var/no_throttle = 0 //If this command should NOT be limited by the throtteling
- var/description = null //Description for the command
- var/list/params = list() //Required Parameters for the command
- //Explanation of the parameter options:
- //Required - name -> Name of the parameter - should be the same as the index in the list
- //Required - desc -> Description of the parameter
- //Required - req -> Is this a required parameter: 1 -> Yes, 0 -> No
- //Required - type -> What type is this:
- // str->String,
- // int->Integer,
- // lst->List/array,
- // senderkey->unique identifier of the person sending the request
- // slct -> Select one of multiple specified options
- //Required* - options -> The possible options that can be selected (slct)
- var/statuscode = null
- var/response = null
- var/data = null
-/datum/topic_command/proc/run_command(queryparams)
- // Always returns 1 --> Details status in statuscode, response and data
- return 1
-/datum/topic_command/proc/check_params_missing(queryparams)
- //Check if some of the required params are missing
- // 0 -> if all params are supplied
- // >=1 -> if a param is missing
- var/list/missing_params = list()
- var/errorcount = 0
-
- for(var/key in params)
- var/list/param = params[key]
- if(queryparams[key] == null)
- if(param["req"] == 0)
- log_debug("API: The following parameter is OPTIONAL and missing: [param["name"]] - [param["desc"]]")
- else
- log_debug("API: The following parameter is REQUIRED but missing: [param["name"]] - [param["desc"]]")
- errorcount ++
- missing_params += param["name"]
- if(errorcount)
- log_debug("API: Request aborted. Required parameters missing")
- statuscode = 400
- response = "Required params missing"
- data = missing_params
- return errorcount
- return 0
-
-//
-// API for the API
-//
-/datum/topic_command/api_get_version
- name = "api_get_version"
- description = "Gets the version of the API"
- no_auth = 1
- no_throttle = 1
-/datum/topic_command/api_get_version/run_command(queryparams)
- var/list/version = list()
- var/versionstring = null
- //The Version Number follows SemVer http://semver.org/
- version["major"] = 2 //Major Version Number --> Increment when implementing breaking changes
- version["minor"] = 4 //Minor Version Number --> Increment when adding features
- version["patch"] = 0 //Patchlevel --> Increment when fixing bugs
-
- versionstring = "[version["major"]].[version["minor"]].[version["patch"]]"
-
- statuscode = 200
- response = versionstring
- data = version
- return 1
-
-
-//Get all the commands a specific token / ip combo is authorized to use
-/datum/topic_command/api_get_authed_commands
- name = "api_get_authed_commands"
- description = "Returns the commands that can be accessed by the requesting ip and token"
-/datum/topic_command/api_get_authed_commands/run_command(queryparams)
- var/list/commands = list()
-
-
- //Check if DB Connection is established
- if (!establish_db_connection(dbcon))
- statuscode = 500
- response = "DB Connection Unavailable"
- return 1
-
- var/DBQuery/commandsquery = dbcon.NewQuery({"SELECT api_f.command
- FROM ss13_api_token_command as api_t_f, ss13_api_tokens as api_t, ss13_api_commands as api_f
- WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.command_id
- AND (
- (token = :token: AND ip = :ip:)
- OR
- (token = :token: AND ip IS NULL)
- OR
- (token IS NULL AND ip = :ip:)
- )
- ORDER BY command DESC"})
-
-
- commandsquery.Execute(list("token" = queryparams["auth"], "ip" = queryparams["addr"]))
- while (commandsquery.NextRow())
- commands[commandsquery.item[1]] = commandsquery.item[1]
- if(commandsquery.item[1] == "_ANY")
- statuscode = 200
- response = "Authorized commands retrieved - ALL"
- data = topic_commands_names
- return 1
-
-
- statuscode = 200
- response = "Authorized commands retrieved"
- data = commands
- return 1
-
-//Get details for a specific api command
-/datum/topic_command/api_explain_command
- name = "api_explain_command"
- description = "Explains a specific API command"
- no_throttle = 1
- params = list(
- "command" = list("name"="command","desc"="The name of the API command that should be explained","req"=1,"type"="str")
- )
-/datum/topic_command/api_explain_command/run_command(queryparams)
- var/datum/topic_command/apicommand = topic_commands[queryparams["command"]]
- var/list/commanddata = list()
-
- if (isnull(apicommand))
- statuscode = 501
- response = "Not Implemented - The requested command does not exist"
- return 1
-
- //Then query for auth
- if (!establish_db_connection(dbcon))
- statuscode = 500
- response = "DB Connection Unavailable"
- return 1
-
- var/DBQuery/permquery = dbcon.NewQuery({"SELECT api_f.command
- FROM ss13_api_token_command as api_t_f, ss13_api_tokens as api_t, ss13_api_commands as api_f
- WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.command_id
- AND api_t.deleted_at IS NULL
- AND (
- (token = :token: AND ip = :ip: AND command = :command:)
- OR
- (token = :token: AND ip IS NULL AND command = :command:)
- OR
- (token = :token: AND ip = :ip: AND command = \"_ANY\")
- OR
- (token = :token: AND ip IS NULL AND command = \"_ANY\")
- OR
- (token IS NULL AND ip IS NULL AND command = :command:)
- )"})
- //Get the tokens and the associated commands
- //Check if the token, the ip and the command matches OR
- // the token + command matches and the ip is NULL (commands that can be used by any ip, but require a token)
- // the token + ip matches and the command is NULL (Allow a specific ip with a specific token to use all commands)
- // the token + ip is NULL and the command matches (Allow a specific command to be used without auth)
-
- permquery.Execute(list("token" = queryparams["auth"], "ip" = queryparams["addr"], "command" = queryparams["command"]))
-
- if (!permquery.RowCount())
- statuscode = 401
- response = "Unauthorized - To access this command"
- return 1
-
- commanddata["name"] = apicommand.name
- commanddata["description"] = apicommand.description
- commanddata["params"] = apicommand.params
-
- statuscode = 200
- response = "Command data retrieved"
- data = commanddata
- return 1
-
-
-/datum/topic_command/update_command_database
- name = "update_command_database"
- description = "Updates the available topic commands in the database"
-/datum/topic_command/update_command_database/run_command(queryparams)
- api_update_command_database()
-
- statuscode = 200
- response = "Database Updated"
- return 1
-
-//
-// API for the other stuff
-//
-
-//Char Names
-/datum/topic_command/get_char_list
- name = "get_char_list"
- description = "Provides a list of all characters ingame"
-/datum/topic_command/get_char_list/run_command(queryparams)
- var/list/chars = list()
-
- var/list/mobs = sortmobs()
- for(var/mob/M in mobs)
- if(!M.ckey) continue
- chars[M.name] += M.key ? (M.client ? M.key : "[M.key] (DC)") : "No key"
-
- statuscode = 200
- response = "Char list fetched"
- data = chars
- return 1
-
-//Admin Count
-/datum/topic_command/get_count_admin
- name = "get_count_admin"
- description = "Gets the number of admins connected"
-/datum/topic_command/get_count_admin/run_command(queryparams)
- var/n = 0
- for (var/client/client in clients)
- if (client.holder && client.holder.rights & (R_ADMIN))
- n++
-
- statuscode = 200
- response = "Admin count fetched"
- data = n
- return 1
-
-//CCIA Count
-/datum/topic_command/get_count_cciaa
- name = "get_count_cciaa"
- description = "Gets the number of ccia connected"
-/datum/topic_command/get_count_ccia/run_command(queryparams)
- var/n = 0
- for (var/client/client in clients)
- if (client.holder && (client.holder.rights & R_CCIAA) && !(client.holder.rights & R_ADMIN))
- n++
-
- statuscode = 200
- response = "CCIA count fetched"
- data = n
- return 1
-
-//Mod Count
-/datum/topic_command/get_count_mod
- name = "get_count_mod"
- description = "Gets the number of mods connected"
-/datum/topic_command/get_count_mod/run_command(queryparams)
- var/n = 0
- for (var/client/client in clients)
- if (client.holder && (client.holder.rights & R_MOD) && !(client.holder.rights & R_ADMIN))
- n++
-
- statuscode = 200
- response = "Mod count fetched"
- data = n
- return 1
-
-//Player Count
-/datum/topic_command/get_count_player
- name = "get_count_player"
- description = "Gets the number of players connected"
-/datum/topic_command/get_count_player/run_command(queryparams)
- var/n = 0
- for(var/mob/M in player_list)
- if(M.client)
- n++
-
- statuscode = 200
- response = "Player count fetched"
- data = n
- return 1
-
-//Get available Fax Machines
-/datum/topic_command/get_faxmachines
- name = "get_faxmachines"
- description = "Gets all available fax machines"
-/datum/topic_command/get_faxmachines/run_command(queryparams)
- var/list/faxlocations = list()
-
- for (var/obj/machinery/photocopier/faxmachine/F in allfaxes)
- faxlocations.Add(F.department)
-
- statuscode = 200
- response = "Fax machines fetched"
- data = faxlocations
- return 1
-
-//Get Fax List
-/datum/topic_command/get_faxlist
- name = "get_faxlist"
- description = "Gets the list of faxes sent / received"
- params = list(
- "faxtype" = list("name"="faxtype","desc"="Type of the faxes that should be retrieved","req"=1,"type"="slct","options"=list("sent","received"))
- )
-/datum/topic_command/get_faxlist/run_command(queryparams)
- var/list/faxes = list()
- switch (queryparams["faxtype"])
- if ("received")
- faxes = arrived_faxes
- if ("sent")
- faxes = sent_faxes
-
- if (!faxes || !faxes.len)
- statuscode = 404
- response = "No faxes found"
- data = null
- return 1
-
- var/list/output = list()
- for (var/i = 1, i <= faxes.len, i++)
- var/obj/item/a = faxes[i]
- output += "[i]"
- output[i] = a.name ? a.name : "Untitled Fax"
-
- statuscode = 200
- response = "Fetched Fax List"
- data = output
- return 1
-
-//Get Specific Fax
-/datum/topic_command/get_fax
- name = "get_fax"
- description = "Gets a specific fax that has been sent or received"
- params = list(
- "faxtype" = list("name"="faxtype","desc"="Type of the faxes that should be retrieved","req"=1,"type"="slct","options"=list("sent","received")),
- "faxid" = list("name"="faxid","desc"="ID of the fax that should be retrieved","req"=1,"type"="int")
- )
-/datum/topic_command/get_fax/run_command(queryparams)
- var/list/faxes = list()
- switch (queryparams["faxtype"])
- if ("received")
- faxes = arrived_faxes
- if ("sent")
- faxes = sent_faxes
-
- if (!faxes || !faxes.len)
- statuscode = 500
- response = "No faxes found!"
- data = null
- return 1
-
- var/fax_id = text2num(queryparams["faxid"])
- if (fax_id > faxes.len || fax_id < 1)
- statuscode = 404
- response = "Invalid Fax ID"
- data = null
- return 1
-
- var/output = list()
- if (istype(faxes[fax_id], /obj/item/weapon/paper))
- var/obj/item/weapon/paper/a = faxes[fax_id]
- output["title"] = a.name ? a.name : "Untitled Fax"
-
- var/content = replacetext(a.info, "
", "\n")
- content = strip_html_properly(content, 0)
- output["content"] = content
-
- statuscode = 200
- response = "Fax (Paper) with id [fax_id] retrieved"
- data = output
- return 1
- else if (istype(faxes[fax_id], /obj/item/weapon/photo))
- statuscode = 501
- response = "Fax is a Photo - Unable to send"
- data = null
- return 1
- else if (istype(faxes[fax_id], /obj/item/weapon/paper_bundle))
- var/obj/item/weapon/paper_bundle/b = faxes[fax_id]
- output["title"] = b.name ? b.name : "Untitled Paper Bundle"
-
- if (!b.pages || !b.pages.len)
- statuscode = 500
- response = "Fax Paper Bundle is empty - This should not happen"
- data = null
- return 1
-
- var/i = 0
- for (var/obj/item/weapon/paper/c in b.pages)
- i++
- var/content = replacetext(c.info, "
", "\n")
- content = strip_html_properly(content, 0)
- output["content"] += "Page [i]:\n[content]\n\n"
-
- statuscode = 200
- response = "Fax (PaperBundle) retrieved"
- data = output
- return 1
-
- statuscode = 500
- response = "Unable to recognize the fax type. Cannot send contents!"
- data = null
- return 1
-
-//Get Ghosts
-/datum/topic_command/get_ghosts
- name = "get_ghosts"
- description = "Gets the ghosts"
-/datum/topic_command/get_ghosts/run_command(queryparams)
- var/list/ghosts[] = list()
- ghosts = get_ghosts(1,1)
-
- statuscode = 200
- response = "Fetched Ghost list"
- data = ghosts
- return 1
-
-// Crew Manifest
-/datum/topic_command/get_manifest
- name = "get_manifest"
- description = "Gets the crew manifest"
-/datum/topic_command/get_manifest/run_command(queryparams)
- var/list/positions = list()
- var/list/set_names = list(
- "heads" = command_positions,
- "sec" = security_positions,
- "eng" = engineering_positions,
- "med" = medical_positions,
- "sci" = science_positions,
- "civ" = civilian_positions,
- "bot" = nonhuman_positions
- )
-
- for(var/datum/data/record/t in data_core.general)
- var/name = t.fields["name"]
- var/rank = t.fields["rank"]
- var/real_rank = make_list_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
-
- // for(var/k in positions)
- // positions[k] = list2params(positions[k]) // converts positions["heads"] = list("Bob"="Captain", "Bill"="CMO") into positions["heads"] = "Bob=Captain&Bill=CMO"
-
- statuscode = 200
- response = "Manifest fetched"
- data = positions
- return 1
-
-//Player Ckeys
-/datum/topic_command/get_player_list
- name = "get_player_list"
- description = "Gets a list of connected players"
- params = list(
- "showadmins" = list("name"="show admins","desc"="A boolean to toggle whether or not hidden admins should be shown with proper or improper ckeys.","req"=0,"type"="int")
- )
-/datum/topic_command/get_player_list/run_command(queryparams)
- var/show_hidden_admins = 0
-
- if (!isnull(queryparams["showadmins"]))
- show_hidden_admins = text2num(queryparams["showadmins"])
-
- var/list/players = list()
- for (var/client/C in clients)
- if (show_hidden_admins && C.holder && C.holder.fakekey)
- players += ckey(C.holder.fakekey)
- else
- players += C.ckey
-
- statuscode = 200
- response = "Player list fetched"
- data = players
- return 1
-
-//Get info about a specific player
-/datum/topic_command/get_player_info
- name = "get_player_info"
- description = "Gets information about a specific player"
- params = list(
- "search" = list("name"="search","desc"="List with strings that should be searched for","req"=1,"type"="lst")
- )
-/datum/topic_command/get_player_info/run_command(queryparams)
- var/list/search = queryparams["search"]
-
- var/list/ckeysearch = list()
- for(var/text in search)
- ckeysearch += ckey(text)
-
- var/list/match = list()
-
- for(var/mob/M in mob_list)
- var/strings = list(M.name, M.ckey)
- if(M.mind)
- strings += M.mind.assigned_role
- strings += M.mind.special_role
- for(var/text in strings)
- if(ckey(text) in ckeysearch)
- match[M] += 10 // an exact match is far better than a partial one
- else
- for(var/searchstr in search)
- if(findtext(text, searchstr))
- match[M] += 1
-
- var/maxstrength = 0
- for(var/mob/M in match)
- maxstrength = max(match[M], maxstrength)
- for(var/mob/M in match)
- if(match[M] < maxstrength)
- match -= M
-
- if(!match.len)
- statuscode = 449
- response = "No match found"
- data = null
- return 1
- else if(match.len == 1)
- var/mob/M = match[1]
- var/info = list()
- info["key"] = M.key
- if (M.client)
- var/client/C = M.client
- info["discordmuted"] = C.mute_discord ? "Yes" : "No"
- info["name"] = M.name == M.real_name ? M.name : "[M.name] ([M.real_name])"
- info["role"] = M.mind ? (M.mind.assigned_role ? M.mind.assigned_role : "No role") : "No mind"
- var/turf/MT = get_turf(M)
- info["loc"] = M.loc ? "[M.loc]" : "null"
- info["turf"] = MT ? "[MT] @ [MT.x], [MT.y], [MT.z]" : "null"
- info["area"] = MT ? "[MT.loc]" : "null"
- info["antag"] = M.mind ? (M.mind.special_role ? M.mind.special_role : "Not antag") : "No mind"
- info["hasbeenrev"] = M.mind ? M.mind.has_been_rev : "No mind"
- info["stat"] = M.stat
- info["type"] = M.type
- if(isliving(M))
- var/mob/living/L = M
- info["damage"] = list2params(list(
- oxy = L.getOxyLoss(),
- tox = L.getToxLoss(),
- fire = L.getFireLoss(),
- brute = L.getBruteLoss(),
- clone = L.getCloneLoss(),
- brain = L.getBrainLoss()
- ))
- else
- info["damage"] = "non-living"
- info["gender"] = M.gender
- statuscode = 200
- response = "Client data fetched"
- data = info
- return 1
- else
- statuscode = 449
- response = "Multiple Matches found"
- data = null
- return 1
-
-//Get Server Status
-/datum/topic_command/get_serverstatus
- name = "get_serverstatus"
- description = "Gets the serverstatus"
-/datum/topic_command/get_serverstatus/run_command(queryparams)
- var/list/s[] = list()
- s["version"] = game_version
- s["mode"] = master_mode
- s["respawn"] = config.abandon_allowed
- s["enter"] = config.enter_allowed
- s["vote"] = config.allow_vote_mode
- s["ai"] = config.allow_ai
- s["host"] = host ? host : null
- s["players"] = 0
- s["stationtime"] = worldtime2text()
- s["roundduration"] = get_round_duration_formatted()
- s["gameid"] = game_id
-
- if(queryparams["status"] == "2")
- var/list/players = list()
- var/list/admins = list()
-
- for(var/client/C in clients)
- if(C.holder)
- if(C.holder.fakekey)
- continue
- admins[C.key] = C.holder.rank
- players += C.key
-
- s["players"] = players.len
- s["playerlist"] = players
- s["admins"] = admins.len
- s["adminlist"] = admins
- else
- var/n = 0
- var/admins = 0
-
- for(var/client/C in clients)
- if(C.holder)
- if(C.holder.fakekey)
- continue //so stealthmins aren't revealed by the hub
- admins++
- s["player[n]"] = C.key
- n++
-
- s["players"] = n
- s["admins"] = admins
-
- statuscode = 200
- response = "Server Status fetched"
- data = s
- return 1
-
-//Get a Staff List
-/datum/topic_command/get_stafflist
- name = "get_stafflist"
- description = "Gets a list of connected staffmembers"
-/datum/topic_command/get_stafflist/run_command(queryparams)
- var/list/staff = list()
- for (var/client/C in admins)
- staff[C] = C.holder.rank
-
- statuscode = 200
- response = "Staff list fetched"
- data = staff
- return 1
-
-//Grant Respawn
-/datum/topic_command/grant_respawn
- name = "grant_respawn"
- description = "Grants a respawn to a specific target"
- params = list(
- "senderkey" = list("name"="senderkey","desc"="Unique id of the person that authorized the respawn","req"=1,"type"="senderkey"),
- "target" = list("name"="target","desc"="Ckey of the target that should be granted a respawn","req"=1,"type"="str")
- )
-/datum/topic_command/grant_respawn/run_command(queryparams)
- var/list/ghosts = get_ghosts(1,1)
- var/target = queryparams["target"]
- var/allow_antaghud = queryparams["allow_antaghud"]
- var/senderkey = queryparams["senderkey"] //Identifier of the sender (Ckey / Userid / ...)
-
- var/mob/abstract/observer/G = ghosts[target]
-
- if(!G in ghosts)
- statuscode = 404
- response = "Target not in ghosts list"
- data = null
- return 1
-
- if(G.has_enabled_antagHUD && config.antag_hud_restricted && allow_antaghud == 0)
- statuscode = 409
- response = "Ghost has used Antag Hud - Respawn Aborted"
- data = null
- return 1
- G.timeofdeath=-19999 /* time of death is checked in /mob/verb/abandon_mob() which is the Respawn verb.
- timeofdeath is used for bodies on autopsy but since we're messing with a ghost I'm pretty sure
- there won't be an autopsy.
- */
- var/datum/preferences/P
-
- if (G.client)
- P = G.client.prefs
- else if (G.ckey)
- P = preferences_datums[G.ckey]
- else
- statuscode = 500
- response = "Something went wrong, couldn't find the target's preferences datum"
- data = null
- return 1
-
- for (var/entry in P.time_of_death)//Set all the prefs' times of death to a huge negative value so any respawn timers will be fine
- P.time_of_death[entry] = -99999
-
- G.has_enabled_antagHUD = 2
- G.can_reenter_corpse = 1
-
- G:show_message(text("You may now respawn. You should roleplay as if you learned nothing about the round during your time with the dead."), 1)
- log_admin("[senderkey] allowed [key_name(G)] to bypass the 30 minute respawn limit via the API",ckey=key_name(G),admin_key=senderkey)
- message_admins("Admin [senderkey] allowed [key_name_admin(G)] to bypass the 30 minute respawn limit via the API", 1)
-
-
- statuscode = 200
- response = "Respawn Granted"
- data = null
- return 1
-
-//Ping Test
-/datum/topic_command/ping
- name = "ping"
- description = "API test command"
-/datum/topic_command/ping/run_command(queryparams)
- var/x = 1
- for (var/client/C)
- x++
- statuscode = 200
- response = "Pong"
- data = x
- return 1
-
-//Restart Round
-/datum/topic_command/restart_round
- name = "restart_round"
- description = "Restarts the round"
- params = list(
- "senderkey" = list("name"="senderkey","desc"="Unique id of the person that authorized the restart","req"=1,"type"="senderkey")
- )
-/datum/topic_command/restart_round/run_command(queryparams)
- var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
-
- to_world("Server restarting by remote command.")
- log_and_message_admins("World restart initiated remotely by [senderkey].")
- feedback_set_details("end_error","remote restart")
-
- spawn(50)
- log_game("Rebooting due to remote command.")
- world.Reboot(10)
-
- statuscode = 200
- response = "Restart Command accepted"
- data = null
- return 1
-
-//Get available Fax Machines
-/datum/topic_command/send_adminmsg
- name = "send_adminmsg"
- description = "Sends a adminmessage to a player"
- params = list(
- "ckey" = list("name"="ckey","desc"="The target of the adminmessage","req"=1,"type"="str"),
- "msg" = list("name"="msg","desc"="The message that should be sent","req"=1,"type"="str"),
- "senderkey" = list("name"="senderkey","desc"="Unique id of the person that sent the adminmessage","req"=1,"type"="senderkey"),
- "rank" = list("name"="rank","desc"="The rank that should be displayed - Defaults to admin if none specified","req"=0,"type"="str")
- )
-
-/datum/topic_command/send_adminmsg/run_command(queryparams)
- /*
- We got an adminmsg from IRC bot lets split the API
- expected output:
- 1. ckey = ckey of person the message is to
- 2. msg = contents of message, parems2list requires
- 3. rank = Rank that should be displayed
- 4. senderkey = the ircnick that send the message.
- */
-
- var/client/C
- var/req_ckey = ckey(queryparams["ckey"])
-
- for(var/client/K in clients)
- if(K.ckey == req_ckey)
- C = K
- break
- if(!C)
- statuscode = 404
- response = "No client with that name on server"
- data = null
- return 1
-
- var/rank = queryparams["rank"]
- if(!rank)
- rank = "Admin"
-
- var/message = "[rank] PM from [queryparams["senderkey"]]: [queryparams["msg"]]"
- var/amessage = "[rank] PM from [queryparams["senderkey"]] to [key_name(C, highlight_special = 1)] : [queryparams["msg"]]"
-
- C.received_discord_pm = world.time
- C.discord_admin = queryparams["senderkey"]
-
- sound_to(C, 'sound/effects/adminhelp.ogg')
- to_chat(C, message)
-
- for(var/client/A in admins)
- if(A != C)
- to_chat(A, amessage)
-
-
- statuscode = 200
- response = "Admin Message sent"
- data = null
- return 1
-
-//Send a Command Report
-/datum/topic_command/send_commandreport
- name = "send_commandreport"
- description = "Sends a command report"
- params = list(
- "senderkey" = list("name"="senderkey","desc"="Unique id of the person that sent the commandreport","req"=1,"type"="senderkey"),
- "title" = list("name"="title","desc"="The message title that should be sent, Defaults to NanoTrasen Update if not specified","req"=0,"type"="str"),
- "body" = list("name"="body","desc"="The message body that should be sent","req"=1,"type"="str"),
- "type" = list("name"="type","desc"="The type of the message that should be sent, Defaults to freeform","req"=0,"type"="slct","options"=list("freeform","ccia")),
- "sendername" = list("name"="sendername","desc"="IC Name of the sender for the CCIA Report, Defaults to CCIAAMS, \[Command-StationName\]","req"=0,"type"="str"),
- "announce" = list("name"="announce","desc"="If the report should be announce 1 -> Yes, 0 -> No, Defaults to 1","req"=0,"type"="int")
- )
-/datum/topic_command/send_commandreport/run_command(queryparams)
- var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
- var/reporttitle = sanitizeSafe(queryparams["title"]) //Title of the report
- var/reportbody = nl2br(sanitize(queryparams["body"],encode=0,extra=0,max_length=0)) //Body of the report
- var/reporttype = queryparams["type"] //Type of the report: freeform / ccia / admin
- var/reportsender = sanitizeSafe(queryparams["sendername"]) //Name of the sender
- var/reportannounce = text2num(queryparams["announce"]) //Announce the contents report to the public: 1 / 0
-
- if(!reporttitle)
- reporttitle = "NanoTrasen Update"
- if(!reporttype)
- reporttype = "freeform"
- if(!reportannounce)
- reportannounce = 1
-
- //Set the report footer for CCIA Announcements
- if (reporttype == "ccia")
- if (reportsender)
- reportbody += "
- [reportsender], Central Command Internal Affairs Agent, [commstation_name()]"
- else
- reportbody += "
- CCIAAMS, [commstation_name()]"
-
- //Send the message to the communications consoles
- post_comm_message(reporttitle, reportbody)
-
- if(reportannounce == 1)
- command_announcement.Announce(reportbody, reporttitle, new_sound = 'sound/AI/commandreport.ogg', do_newscast = 1, msg_sanitized = 1);
- if(reportannounce == 0)
- to_world("New NanoTrasen Update available at all communication consoles.")
- to_world(sound('sound/AI/commandreport.ogg'))
-
-
- log_admin("[senderkey] has created a command report via the api: [reportbody]",admin_key=senderkey)
- message_admins("[senderkey] has created a command report via the api", 1)
-
- statuscode = 200
- response = "Command Report sent"
- data = null
- return 1
-
-//Send Fax
-/datum/topic_command/send_fax
- name = "send_fax"
- description = "Sends a fax"
- params = list(
- "senderkey" = list("name"="senderkey","desc"="Unique id of the person that sent the fax","req"=1,"type"="senderkey"),
- "title" = list("name"="title","desc"="The message title that should be sent","req"=1,"type"="str"),
- "body" = list("name"="body","desc"="The message body that should be sent","req"=1,"type"="str"),
- "target" = list("name"="target","desc"="The target faxmachines the fax should be sent to","req"=1,"type"="lst")
- )
-/datum/topic_command/send_fax/run_command(queryparams)
- var/list/responselist = list()
- var/list/sendsuccess = list()
- var/list/targetlist = queryparams["target"] //Target locations where the fax should be sent to
- var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
- var/faxtitle = sanitizeSafe(queryparams["title"]) //Title of the report
- var/faxbody = sanitize(queryparams["body"],max_length=0) //Body of the report
- var/faxannounce = text2num(queryparams["announce"]) //Announce the contents report to the public: 0 - Dont announce, 1 - Announce, 2 - Only if pda not linked
-
- if(!targetlist || targetlist.len < 1)
- statuscode = 400
- response = "Parameter target not set"
- data = null
- return 1
-
- var/sendresult = 0
- var/notifyresult = 1
-
- //Send the fax
- for (var/obj/machinery/photocopier/faxmachine/F in allfaxes)
- if (F.department in targetlist)
- sendresult = send_fax(F, faxtitle, faxbody, senderkey)
- if (sendresult == 1)
- sendsuccess.Add(F.department)
- if(!LAZYLEN(F.alert_pdas))
- notifyresult = 0
- responselist[F.department] = "notlinked"
- else
- responselist[F.department] = "success"
- else
- responselist[F.department] = "failed"
-
- //Announce that the fax has been sent
- if(faxannounce == 1 || (faxannounce==2 && notifyresult==0))
- if(sendsuccess.len < 1)
- command_announcement.Announce("A fax message from Central Command could not be delivered because all of the following fax machines are inoperational:
"+jointext(targetlist, ", "), "Fax Delivery Failure", new_sound = 'sound/AI/commandreport.ogg', msg_sanitized = 1);
- else
- command_announcement.Announce("A fax message from Central Command has been sent to the following fax machines:
"+jointext(sendsuccess, ", "), "Fax Received", new_sound = 'sound/AI/commandreport.ogg', msg_sanitized = 1);
-
- log_admin("[senderkey] sent a fax via the API: : [faxbody]",admin_key=senderkey)
- message_admins("[senderkey] sent a fax via the API", 1)
-
- statuscode = 200
- response = "Fax sent"
- data = responselist
- return 1
-
-/datum/topic_command/send_fax/proc/send_fax(var/obj/machinery/photocopier/faxmachine/F, title, body, senderkey)
- // Create the reply message
- var/obj/item/weapon/paper/P = new /obj/item/weapon/paper( null ) //hopefully the null loc won't cause trouble for us
- P.name = "[current_map.boss_name] - [title]"
- P.info = body
- P.update_icon()
-
- // Stamps
- var/image/stampoverlay = image('icons/obj/bureaucracy.dmi')
- stampoverlay.icon_state = "paper_stamp-cent"
- if(!P.stamped)
- P.stamped = new
- P.stamped += /obj/item/weapon/stamp
- P.add_overlay(stampoverlay)
- P.stamps += "
This paper has been stamped by the Central Command Quantum Relay."
-
- if(F.receivefax(P))
- log_and_message_admins("[senderkey] sent a fax message to the [F.department] fax machine via the api. (JMP)")
- sent_faxes += P
- return 1
- else
- qdel(P)
- return 0
-
-// Update discord_bot's channels.
-/datum/topic_command/update_bot_channels
- name = "update_bot_channels"
- description = "Tells the ingame instance of the Discord bot to update its cached channels list."
-
-/datum/topic_command/update_bot_channels/run_command()
- data = null
-
- if (!discord_bot)
- statuscode = 404
- response = "Ingame Discord bot not initialized."
- return 1
-
- switch (discord_bot.update_channels())
- if (1)
- statuscode = 404
- response = "Ingame Discord bot is not active."
- if (2)
- statuscode = 500
- response = "Ingame Discord bot encountered error attempting to access database."
- else
- statuscode = 200
- response = "Ingame Discord bot's channels were successfully updated."
-
- return 1
-
-// Gets the currently configured access levels
-/datum/topic_command/get_access_levels
- name = "get_access_levels"
- description = "Gets the currently configured access levels."
-
-/datum/topic_command/get_access_levels/run_command()
- var/list/access_levels = list()
- for(var/datum/access/acc in get_all_access_datums())
- access_levels.Add(list(acc.get_info_list()))
-
- data = access_levels
- statuscode = 200
- response = "Levels Sent"
- return 1
-
-// Reloads the current cargo configuration
-/datum/topic_command/cargo_reload
- name = "cargo_reload"
- description = "Reloads the current cargo configuration."
- params = list(
- "force" = list("name"="force","desc"="Force the reload even if orders have already been placed","type"="int","req"=0)
- )
-
-/datum/topic_command/cargo_reload/run_command(queryparams)
- var/force = text2num(queryparams["force"])
- if(!SScargo.get_order_count())
- SScargo.load_from_sql()
- message_admins("Cargo has been reloaded via the API.")
- statuscode = 200
- response = "Cargo Reloaded from SQL."
- else
- if(force)
- SScargo.load_from_sql()
- message_admins("Cargo has been force-reloaded via the API. All current orders have been purged.")
- statuscode = 200
- response = "Cargo Force-Reloaded from SQL."
- else
- statuscode = 500
- response = "Orders have been placed. Use force parameter to overwrite."
- return 1
-
-//Gets a overview of all polls (title, id, type)
-/datum/topic_command/get_polls
- name = "get_polls"
- description = "Gets a overview of all polls."
- params = list(
- "current_only" = list("name"="current_only","desc"="Only get information about the current polls","type"="int","req"=0),
- "admin_only" = list("name"="admin_only","desc"="Only get information about the admin_only polls","type"="int","req"=0)
- )
-
-/datum/topic_command/get_polls/run_command(queryparams)
- var/current_only = text2num(queryparams["current_only"])
- var/admin_only = text2num(queryparams["admin_only"])
-
- if(!establish_db_connection(dbcon))
- statuscode = 500
- response = "DB-Connection unavailable"
- return 1
-
- var/list/polldata = list()
-
- var/DBQuery/select_query = dbcon.NewQuery("SELECT id, polltype, starttime, endtime, question, multiplechoiceoptions, adminonly FROM ss13_poll_question [(current_only || admin_only) ? "WHERE" : ""] [(admin_only ? "adminonly = true " : "")][(current_only && admin_only ? "AND " : "")][(current_only ? "Now() BETWEEN starttime AND endtime" : "")]")
- select_query.Execute()
- while(select_query.NextRow())
- polldata["[select_query.item[1]]"] = list(
- "id"=select_query.item[1],
- "polltype"=select_query.item[2],
- "starttime"=select_query.item[3],
- "endtime"=select_query.item[4],
- "question"=select_query.item[5],
- "multiplechoiceoptions"=select_query.item[6],
- "adminonly"=select_query.item[7]
- )
-
- statuscode = 200
- response = "Polldata sent"
- data = polldata
- return 1
-
-
-// Gets infos about a poll
-/datum/topic_command/get_poll_info
- name = "get_poll_info"
- description = "Gets Information about a poll."
- params = list(
- "poll_id" = list("name"="poll_id","desc"="The poll id that should be queried","type"="int","req"=1)
- )
-
-/datum/topic_command/get_poll_info/run_command(queryparams)
- var/poll_id = text2num(queryparams["poll_id"])
-
- if(!establish_db_connection(dbcon))
- statuscode = 500
- response = "DB-Connection unavailable"
- return 1
-
- //Get general data about the poll
- var/DBQuery/select_query = dbcon.NewQuery("SELECT id, polltype, starttime, endtime, question, multiplechoiceoptions, adminonly, publicresult, viewtoken FROM ss13_poll_question WHERE id = :poll_id:")
- select_query.Execute(list("poll_id"=poll_id))
-
- //Check if the poll exists
- if(!select_query.NextRow())
- statuscode = 404
- response = "The requested poll does not exist"
- data = null
- return 1
- var/list/poll_data = list(
- "id"=select_query.item[1],
- "polltype"=select_query.item[2],
- "starttime"=select_query.item[3],
- "endtime"=select_query.item[4],
- "question"=select_query.item[5],
- "multiplechoiceoptions"=select_query.item[6],
- "adminonly"=select_query.item[7],
- "publicresult"=select_query.item[8]
- )
-
- //Lets add a WI link to the poll, if we have the WI configured
- if(config.webint_url)
- poll_data["link"]="[config.webint_url]server/poll/[select_query.item[1]]/[select_query.item[9]]"
-
- var/list/result_data = list()
-
- /** Return different data based on the poll type: */
- //If we have a option or a multiple choice poll, return the number of options
- if(poll_data["polltype"] == "OPTION" || poll_data["polltype"] == "MULTICHOICE")
- var/DBQuery/result_query = dbcon.NewQuery({"SELECT ss13_poll_vote.optionid, ss13_poll_option.text, COUNT(*) as option_count
- FROM ss13_poll_vote
- LEFT JOIN ss13_poll_option ON ss13_poll_vote.optionid = ss13_poll_option.id
- WHERE ss13_poll_vote.pollid = :poll_id:
- GROUP BY ss13_poll_vote.optionid"})
- result_query.Execute(list("poll_id"=poll_id))
-
- while(result_query.NextRow())
- result_data["[result_query.item[1]]"] = list(
- "option_id"=result_query.item[1],
- "option_question"=result_query.item[2],
- "option_count"=result_query.item[3]
- )
- if(!length(result_data))
- statuscode = 500
- response = "No data returned by result query."
- data = null
- return 1
-
- //If we have a numval poll, return the options with the min, max, and average
- else if(poll_data["polltype"] == "NUMVAL")
- var/DBQuery/result_query = dbcon.NewQuery({"SELECT ss13_poll_vote.optionid, ss13_poll_option.text, ss13_poll_option.minval, ss13_poll_option.maxval, ss13_poll_option.descmin, ss13_poll_option.descmid, ss13_poll_option.descmax, AVG(rating) as option_rating_avg, MIN(rating) as option_rating_min, MAX(rating) as option_rating_max
- FROM ss13_poll_vote
- LEFT JOIN ss13_poll_option ON ss13_poll_vote.optionid = ss13_poll_option.id
- WHERE ss13_poll_vote.pollid = :poll_id:
- GROUP BY ss13_poll_vote.optionid"})
- result_query.Execute(list("poll_id"=poll_id))
- while(result_query.NextRow())
- result_data["[result_query.item[1]]"] = list(
- "option_id"=result_query.item[1],
- "option_question"=result_query.item[2],
- "option_minval"=result_query.item[3],
- "option_maxval"=result_query.item[4],
- "option_descmin"=result_query.item[5],
- "option_descmid"=result_query.item[6],
- "option_descmax"=result_query.item[7],
- "option_rating_min"=result_query.item[8],
- "option_rating_max"=result_query.item[9],
- "option_rating_avg"=result_query.item[10] //TODO: Expand that with MEDIAN once we upgrade mariadb
- )
- if(!length(result_data))
- statuscode = 500
- response = "No data returned by result query."
- data = null
- return 1
-
- //If we have a textpoll, return the number of answers
- else if(poll_data["polltype"] == "TEXT")
- var/DBQuery/result_query = dbcon.NewQuery({"SELECT COUNT(*) as count FROM ss13_poll_textreply WHERE pollid = :poll_id:"})
- result_query.Execute(list("poll_id"=poll_id))
- if(result_query.NextRow())
- result_data = list(
- "response_count"=result_query.item[1]
- )
- else
- statuscode = 500
- response = "No data returned by result query."
- data = null
- return 1
- else
- statuscode = 500
- response = "Unknown Poll Type"
- data = poll_data
- return 1
-
-
- poll_data["results"] = result_data
-
- statuscode = 200
- response = "Poll data fetched"
- data = poll_data
- return 1
-
-//Sends a text to everyone on the server
-/datum/topic_command/broadcast_text
- name = "broadcast_text"
- description = "Sends a text to everyone on the server."
- params = list(
- "text" = list("name"="text","desc"="The text that should be sent","req"=1,"type"="str")
- )
-
-/datum/topic_command/broadcast_text/run_command(queryparams)
- to_world(queryparams["text"])
-
- statuscode = 200
- response = "Text sent"
- return 1
diff --git a/code/modules/admin/ticket.dm b/code/modules/admin/ticket.dm
index 4a57da7bb2c..bd44083bd37 100644
--- a/code/modules/admin/ticket.dm
+++ b/code/modules/admin/ticket.dm
@@ -27,31 +27,17 @@ var/global/list/ticket_panels = list()
if (config.ticket_reminder_period)
reminder_timer = addtimer(CALLBACK(src, .proc/remind), config.ticket_reminder_period SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
-/datum/ticket/proc/close(var/client/closed_by)
- if(!closed_by)
- return
-
- if(status == TICKET_CLOSED)
- return
-
- if(status == TICKET_ASSIGNED && !((closed_by.ckey in assigned_admins) || owner == closed_by.ckey) && alert(closed_by, "You are not assigned to this ticket. Are you sure you want to close it?", "Close ticket?" , "Yes" , "No") != "Yes")
- return
-
- if(status == TICKET_ASSIGNED && !closed_by.holder) // non-admins can only close a ticket if no admin has taken it
- return
-
+/datum/ticket/proc/broadcast_closure(closing_user)
var/client/owner_client = client_by_ckey(owner)
if(owner_client && owner_client.adminhelped == ADMINHELPED_DISCORD)
- discord_bot.send_to_admins("[key_name(owner_client)]'s request for help has been closed/deemed unnecessary by [key_name(closed_by)].")
+ discord_bot.send_to_admins("[key_name(owner_client)]'s request for help has been closed/deemed unnecessary by [closing_user].")
owner_client.adminhelped = ADMINHELPED
+/datum/ticket/proc/set_to_closed(closing_key)
src.status = TICKET_CLOSED
- src.closed_by = closed_by.ckey
+ src.closed_by = closing_key
src.closed_rt = world.realtime
- to_chat(client_by_ckey(src.owner), "Your ticket has been closed by [closed_by].")
- message_admins("[src.owner]'s ticket has been closed by [key_name(closed_by)].")
-
update_ticket_panels()
if (reminder_timer)
@@ -59,7 +45,43 @@ var/global/list/ticket_panels = list()
log_to_db()
- return 1
+/datum/ticket/proc/close(var/client/closed_by)
+ if(!closed_by)
+ return FALSE
+
+ if(status == TICKET_CLOSED)
+ return FALSE
+
+ if(status == TICKET_ASSIGNED && !((closed_by.ckey in assigned_admins) || owner == closed_by.ckey) && alert(closed_by, "You are not assigned to this ticket. Are you sure you want to close it?", "Close ticket?" , "Yes" , "No") != "Yes")
+ return FALSE
+
+ if(status == TICKET_ASSIGNED && !closed_by.holder) // non-admins can only close a ticket if no admin has taken it
+ return FALSE
+
+ broadcast_closure(key_name(closed_by))
+
+ to_chat(client_by_ckey(src.owner), "Your ticket has been closed by [closed_by].")
+ message_admins("[src.owner]'s ticket has been closed by [key_name(closed_by)].")
+
+ set_to_closed(closed_by.ckey)
+
+ return TRUE
+
+/datum/ticket/proc/close_remotely(closing_user)
+ if (!closing_user)
+ return FALSE
+
+ if (status != TICKET_OPEN)
+ return FALSE
+
+ broadcast_closure("[closing_user] (Remotely)")
+
+ to_chat(client_by_ckey(src.owner), "Your ticket has been closed by [closing_user] (remotely).")
+ message_admins("[src.owner]'s ticket has been closed by [closing_user] (remotely).")
+
+ set_to_closed(closing_user)
+
+ return TRUE
/datum/ticket/proc/take(var/client/assigned_admin)
if(!assigned_admin)
@@ -117,6 +139,13 @@ var/global/list/ticket_panels = list()
if(ticket.owner == owner && (ticket.status == TICKET_OPEN || ticket.status == TICKET_ASSIGNED))
return ticket // there should only be one open ticket by a client at a time, so no need to keep looking
+/proc/get_ticket_by_id(id)
+ for (var/datum/ticket/ticket in tickets)
+ if (ticket.id == id)
+ return ticket
+
+ return null
+
/datum/ticket/proc/is_active()
if(status != TICKET_ASSIGNED)
return 0
diff --git a/code/modules/world_api/api_command.dm b/code/modules/world_api/api_command.dm
new file mode 100644
index 00000000000..716fac27b8d
--- /dev/null
+++ b/code/modules/world_api/api_command.dm
@@ -0,0 +1,114 @@
+//API Boilerplate
+/datum/topic_command
+ var/name = null //Name for the command
+ var/no_auth = FALSE //If the user does NOT need to be authed to use the command
+ var/no_throttle = FALSE //If this command should NOT be limited by the throtteling
+ var/description = null //Description for the command
+ var/list/params = list() //Required Parameters for the command
+ //Explanation of the parameter options:
+ //Required - name -> Name of the parameter - should be the same as the index in the list
+ //Required - desc -> Description of the parameter
+ //Required - req -> Is this a required parameter: 1 -> Yes, 0 -> No
+ //Required - type -> What type is this:
+ // str->String,
+ // int->Integer,
+ // lst->List/array,
+ // senderkey->unique identifier of the person sending the request
+ // slct -> Select one of multiple specified options
+ //Required* - options -> The possible options that can be selected (slct)
+ var/statuscode = null
+ var/response = null
+ var/data = null
+
+/datum/topic_command/proc/run_command(queryparams)
+ // Always returns 1 --> Details status in statuscode, response and data
+ return TRUE
+
+/datum/topic_command/proc/check_params_missing(queryparams)
+ //Check if some of the required params are missing
+ // 0 -> if all params are supplied
+ // >=1 -> if a param is missing
+ var/list/missing_params = list()
+ var/errorcount = 0
+
+ for(var/key in params)
+ var/list/param = params[key]
+ if(queryparams[key] == null)
+ if(param["req"] == 0)
+ log_debug("API: The following parameter is OPTIONAL and missing: [param["name"]] - [param["desc"]]")
+ else
+ log_debug("API: The following parameter is REQUIRED but missing: [param["name"]] - [param["desc"]]")
+ errorcount ++
+ missing_params += param["name"]
+ if(errorcount)
+ log_debug("API: Request aborted. Required parameters missing")
+ statuscode = 400
+ response = "Required params missing"
+ data = missing_params
+ return errorcount
+ return 0
+
+/datum/topic_command/proc/check_auth(addr, auth_key, bypass_throttle_check = FALSE)
+ if (!bypass_throttle_check && _is_throttled(addr))
+ return 2
+
+ if (no_auth)
+ log_debug("API: Auth bypassed - Command [name] set to no_auth")
+ return 0
+
+ if (_is_authorized_via_token(addr, auth_key))
+ return 0
+ else
+ return 1
+
+/datum/topic_command/proc/_is_throttled(addr)
+ if (no_throttle)
+ log_debug("API: Throttling bypassed - Command [name] set to no_throttle")
+ return FALSE
+
+ if (config.api_rate_limit_whitelist[addr] == null)
+ log_debug("API: Throttling bypassed - IP [addr] is whitelisted.")
+ return FALSE
+
+ var/last_time = world_api_rate_limit[addr]
+ world_api_rate_limit[addr] = REALTIMEOFDAY
+
+ if (last_time != null && abs(last_time - REALTIMEOFDAY) < config.api_rate_limit)
+ return TRUE
+
+ return FALSE
+
+/datum/topic_command/proc/_is_authorized_via_token(addr, auth_key)
+ if (!establish_db_connection(dbcon))
+ return FALSE
+
+ var/DBQuery/authquery = dbcon.NewQuery({"SELECT api_f.command
+ FROM ss13_api_token_command as api_t_f, ss13_api_tokens as api_t, ss13_api_commands as api_f
+ WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.command_id
+ AND api_t.deleted_at IS NULL
+ AND (
+ (token = :token: AND ip = :ip: AND command = :command:)
+ OR
+ (token = :token: AND ip IS NULL AND command = :command:)
+ OR
+ (token = :token: AND ip = :ip: AND command = \"_ANY\")
+ OR
+ (token = :token: AND ip IS NULL AND command = \"_ANY\")
+ OR
+ (token IS NULL AND ip IS NULL AND command = :command:)
+ )"})
+ //Check if the token is not deleted
+ //Check if one of the following is true:
+ // Full Match - Token IP and Command Matches
+ // Any IP - Token and Command Matches, IP is set to NULL (not required)
+ // Any Command - Token and IP Matches, Command is set to _ANY
+ // Any Command, Any IP - Token Matches, IP is set to NULL (not required), Command is set to _ANY
+ // Public - Token is set to NULL, IP is set to NULL and command matches
+
+ authquery.Execute(list("token" = auth_key, "ip" = addr, "command" = name))
+ log_debug("API: Auth Check - Query Executed - Returned Rows: [authquery.RowCount()]")
+
+ if (authquery.RowCount())
+ return TRUE
+
+ return FALSE
diff --git a/code/modules/world_api/commands/admin.dm b/code/modules/world_api/commands/admin.dm
new file mode 100644
index 00000000000..58d5d7b36b0
--- /dev/null
+++ b/code/modules/world_api/commands/admin.dm
@@ -0,0 +1,116 @@
+//Grant Respawn
+/datum/topic_command/grant_respawn
+ name = "grant_respawn"
+ description = "Grants a respawn to a specific target"
+ params = list(
+ "senderkey" = list("name"="senderkey","desc"="Unique id of the person that authorized the respawn","req"=1,"type"="senderkey"),
+ "target" = list("name"="target","desc"="Ckey of the target that should be granted a respawn","req"=1,"type"="str")
+ )
+
+/datum/topic_command/grant_respawn/run_command(queryparams)
+ var/list/ghosts = get_ghosts(1,1)
+ var/target = queryparams["target"]
+ var/allow_antaghud = queryparams["allow_antaghud"]
+ var/senderkey = queryparams["senderkey"] //Identifier of the sender (Ckey / Userid / ...)
+
+ var/mob/abstract/observer/G = ghosts[target]
+
+ if(!G in ghosts)
+ statuscode = 404
+ response = "Target not in ghosts list"
+ data = null
+ return TRUE
+
+ if(G.has_enabled_antagHUD && config.antag_hud_restricted && allow_antaghud == 0)
+ statuscode = 409
+ response = "Ghost has used Antag Hud - Respawn Aborted"
+ data = null
+ return TRUE
+ G.timeofdeath=-19999 /* time of death is checked in /mob/verb/abandon_mob() which is the Respawn verb.
+ timeofdeath is used for bodies on autopsy but since we're messing with a ghost I'm pretty sure
+ there won't be an autopsy.
+ */
+ var/datum/preferences/P
+
+ if (G.client)
+ P = G.client.prefs
+ else if (G.ckey)
+ P = preferences_datums[G.ckey]
+ else
+ statuscode = 500
+ response = "Something went wrong, couldn't find the target's preferences datum"
+ data = null
+ return TRUE
+
+ for (var/entry in P.time_of_death)//Set all the prefs' times of death to a huge negative value so any respawn timers will be fine
+ P.time_of_death[entry] = -99999
+
+ G.has_enabled_antagHUD = 2
+ G.can_reenter_corpse = 1
+
+ G:show_message(text("You may now respawn. You should roleplay as if you learned nothing about the round during your time with the dead."), 1)
+ log_admin("[senderkey] allowed [key_name(G)] to bypass the 30 minute respawn limit via the API",ckey=key_name(G),admin_key=senderkey)
+ message_admins("Admin [senderkey] allowed [key_name_admin(G)] to bypass the 30 minute respawn limit via the API", 1)
+
+
+ statuscode = 200
+ response = "Respawn Granted"
+ data = null
+ return TRUE
+
+//Get available Fax Machines
+/datum/topic_command/send_adminmsg
+ name = "send_adminmsg"
+ description = "Sends a adminmessage to a player"
+ params = list(
+ "ckey" = list("name"="ckey","desc"="The target of the adminmessage","req"=1,"type"="str"),
+ "msg" = list("name"="msg","desc"="The message that should be sent","req"=1,"type"="str"),
+ "senderkey" = list("name"="senderkey","desc"="Unique id of the person that sent the adminmessage","req"=1,"type"="senderkey"),
+ "rank" = list("name"="rank","desc"="The rank that should be displayed - Defaults to admin if none specified","req"=0,"type"="str")
+ )
+
+/datum/topic_command/send_adminmsg/run_command(queryparams)
+ /*
+ We got an adminmsg from IRC bot lets split the API
+ expected output:
+ 1. ckey = ckey of person the message is to
+ 2. msg = contents of message, parems2list requires
+ 3. rank = Rank that should be displayed
+ 4. senderkey = the ircnick that send the message.
+ */
+
+ var/client/C
+ var/req_ckey = ckey(queryparams["ckey"])
+
+ for(var/client/K in clients)
+ if(K.ckey == req_ckey)
+ C = K
+ break
+ if(!C)
+ statuscode = 404
+ response = "No client with that name on server"
+ data = null
+ return TRUE
+
+ var/rank = queryparams["rank"]
+ if(!rank)
+ rank = "Admin"
+
+ var/message = "[rank] PM from [queryparams["senderkey"]]: [queryparams["msg"]]"
+ var/amessage = "[rank] PM from [queryparams["senderkey"]] to [key_name(C, highlight_special = 1)] : [queryparams["msg"]]"
+
+ C.received_discord_pm = world.time
+ C.discord_admin = queryparams["senderkey"]
+
+ sound_to(C, 'sound/effects/adminhelp.ogg')
+ to_chat(C, message)
+
+ for(var/client/A in admins)
+ if(A != C)
+ to_chat(A, amessage)
+
+
+ statuscode = 200
+ response = "Admin Message sent"
+ data = null
+ return TRUE
diff --git a/code/modules/world_api/commands/api_helpers.dm b/code/modules/world_api/commands/api_helpers.dm
new file mode 100644
index 00000000000..23f34dc571e
--- /dev/null
+++ b/code/modules/world_api/commands/api_helpers.dm
@@ -0,0 +1,148 @@
+//
+// API for the API
+//
+/datum/topic_command/api_get_version
+ name = "api_get_version"
+ description = "Gets the version of the API"
+ no_auth = TRUE
+ no_throttle = TRUE
+
+/datum/topic_command/api_get_version/run_command(queryparams)
+ var/list/version = list()
+ var/versionstring = null
+ //The Version Number follows SemVer http://semver.org/
+ version["major"] = 2 //Major Version Number --> Increment when implementing breaking changes
+ version["minor"] = 5 //Minor Version Number --> Increment when adding features
+ version["patch"] = 0 //Patchlevel --> Increment when fixing bugs
+
+ versionstring = "[version["major"]].[version["minor"]].[version["patch"]]"
+
+ statuscode = 200
+ response = versionstring
+ data = version
+ return TRUE
+
+
+//Get all the commands a specific token / ip combo is authorized to use
+/datum/topic_command/api_get_authed_commands
+ name = "api_get_authed_commands"
+ description = "Returns the commands that can be accessed by the requesting ip and token"
+
+/datum/topic_command/api_get_authed_commands/run_command(queryparams)
+ var/list/commands = list()
+ //Check if DB Connection is established
+ if (!establish_db_connection(dbcon))
+ statuscode = 500
+ response = "DB Connection Unavailable"
+ return TRUE
+
+ var/DBQuery/commandsquery = dbcon.NewQuery({"SELECT api_f.command
+ FROM ss13_api_token_command as api_t_f, ss13_api_tokens as api_t, ss13_api_commands as api_f
+ WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.command_id
+ AND (
+ (token = :token: AND ip = :ip:)
+ OR
+ (token = :token: AND ip IS NULL)
+ OR
+ (token IS NULL AND ip = :ip:)
+ )
+ ORDER BY command DESC"})
+
+
+ commandsquery.Execute(list("token" = queryparams["auth"], "ip" = queryparams["addr"]))
+ while (commandsquery.NextRow())
+ commands[commandsquery.item[1]] = commandsquery.item[1]
+ if(commandsquery.item[1] == "_ANY")
+ statuscode = 200
+ response = "Authorized commands retrieved - ALL"
+ data = topic_commands_names
+ return TRUE
+
+
+ statuscode = 200
+ response = "Authorized commands retrieved"
+ data = commands
+ return TRUE
+
+//Get details for a specific api command
+/datum/topic_command/api_explain_command
+ name = "api_explain_command"
+ description = "Explains a specific API command"
+ no_throttle = TRUE
+ params = list(
+ "command" = list("name"="command","desc"="The name of the API command that should be explained","req"=1,"type"="str")
+ )
+
+/datum/topic_command/api_explain_command/run_command(queryparams)
+ var/datum/topic_command/apicommand = topic_commands[queryparams["command"]]
+ var/list/commanddata = list()
+
+ if (isnull(apicommand))
+ statuscode = 501
+ response = "Not Implemented - The requested command does not exist"
+ return TRUE
+
+ //Then query for auth
+ if (!establish_db_connection(dbcon))
+ statuscode = 500
+ response = "DB Connection Unavailable"
+ return TRUE
+
+ if (apicommand.check_auth(queryparams["addr"], queryparams["auth"], TRUE))
+ statuscode = 401
+ response = "Not Authorized - You are not authorized to use the requested command."
+ return TRUE
+
+ commanddata["name"] = apicommand.name
+ commanddata["description"] = apicommand.description
+ commanddata["params"] = apicommand.params
+
+ statuscode = 200
+ response = "Command data retrieved"
+ data = commanddata
+ return TRUE
+
+
+/datum/topic_command/update_command_database
+ name = "update_command_database"
+ description = "Updates the available topic commands in the database"
+
+/datum/topic_command/update_command_database/run_command(queryparams)
+ if (!api_update_command_database())
+ statuscode = 500
+ return FALSE
+ else
+ statuscode = 200
+ return TRUE
+
+/datum/topic_command/update_command_database/proc/api_update_command_database()
+ log_debug("API: DB Command Update Called")
+ //Check if DB Connection is established
+ if (!establish_db_connection(dbcon))
+ response = "Database connection lost, cannot update commands."
+ return FALSE //Error
+
+ var/DBQuery/commandinsertquery = dbcon.NewQuery({"INSERT INTO ss13_api_commands (command,description)
+ VALUES (:command_name:,:command_description:)
+ ON DUPLICATE KEY UPDATE description = :command_description:;"})
+
+ for(var/com in topic_commands)
+ var/datum/topic_command/command = topic_commands[com]
+ commandinsertquery.Execute(list("command_name" = command.name, "command_description" = command.description))
+
+ log_debug("API: DB Command Update Executed")
+
+ response = "Commands successfully updated."
+ return TRUE //OK
+
+//Ping Test
+/datum/topic_command/ping
+ name = "ping"
+ description = "API test command"
+ no_auth = TRUE
+
+/datum/topic_command/ping/run_command(queryparams)
+ statuscode = 200
+ response = "Pong"
+ data = "Pong"
+ return TRUE
diff --git a/code/modules/world_api/commands/cciaa.dm b/code/modules/world_api/commands/cciaa.dm
new file mode 100644
index 00000000000..7b6b3422d67
--- /dev/null
+++ b/code/modules/world_api/commands/cciaa.dm
@@ -0,0 +1,258 @@
+//Get available Fax Machines
+/datum/topic_command/get_faxmachines
+ name = "get_faxmachines"
+ description = "Gets all available fax machines"
+
+/datum/topic_command/get_faxmachines/run_command(queryparams)
+ var/list/faxlocations = list()
+
+ for (var/obj/machinery/photocopier/faxmachine/F in allfaxes)
+ faxlocations.Add(F.department)
+
+ statuscode = 200
+ response = "Fax machines fetched"
+ data = faxlocations
+ return TRUE
+
+//Get Fax List
+/datum/topic_command/get_faxlist
+ name = "get_faxlist"
+ description = "Gets the list of faxes sent / received"
+ params = list(
+ "faxtype" = list("name"="faxtype","desc"="Type of the faxes that should be retrieved","req"=1,"type"="slct","options"=list("sent","received"))
+ )
+
+/datum/topic_command/get_faxlist/run_command(queryparams)
+ var/list/faxes = list()
+ switch (queryparams["faxtype"])
+ if ("received")
+ faxes = arrived_faxes
+ if ("sent")
+ faxes = sent_faxes
+
+ if (!faxes || !faxes.len)
+ statuscode = 404
+ response = "No faxes found"
+ data = null
+ return TRUE
+
+ var/list/output = list()
+ for (var/i = 1, i <= faxes.len, i++)
+ var/obj/item/a = faxes[i]
+ output += "[i]"
+ output[i] = a.name ? a.name : "Untitled Fax"
+
+ statuscode = 200
+ response = "Fetched Fax List"
+ data = output
+ return TRUE
+
+//Get Specific Fax
+/datum/topic_command/get_fax
+ name = "get_fax"
+ description = "Gets a specific fax that has been sent or received"
+ params = list(
+ "faxtype" = list("name"="faxtype","desc"="Type of the faxes that should be retrieved","req"=1,"type"="slct","options"=list("sent","received")),
+ "faxid" = list("name"="faxid","desc"="ID of the fax that should be retrieved","req"=1,"type"="int")
+ )
+
+/datum/topic_command/get_fax/run_command(queryparams)
+ var/list/faxes = list()
+ switch (queryparams["faxtype"])
+ if ("received")
+ faxes = arrived_faxes
+ if ("sent")
+ faxes = sent_faxes
+
+ if (!faxes || !faxes.len)
+ statuscode = 500
+ response = "No faxes found!"
+ data = null
+ return TRUE
+
+ var/fax_id = text2num(queryparams["faxid"])
+ if (fax_id > faxes.len || fax_id < 1)
+ statuscode = 404
+ response = "Invalid Fax ID"
+ data = null
+ return TRUE
+
+ var/output = list()
+ if (istype(faxes[fax_id], /obj/item/weapon/paper))
+ var/obj/item/weapon/paper/a = faxes[fax_id]
+ output["title"] = a.name ? a.name : "Untitled Fax"
+
+ var/content = replacetext(a.info, "
", "\n")
+ content = strip_html_properly(content, 0)
+ output["content"] = content
+
+ statuscode = 200
+ response = "Fax (Paper) with id [fax_id] retrieved"
+ data = output
+ return TRUE
+ else if (istype(faxes[fax_id], /obj/item/weapon/photo))
+ statuscode = 501
+ response = "Fax is a Photo - Unable to send"
+ data = null
+ return TRUE
+ else if (istype(faxes[fax_id], /obj/item/weapon/paper_bundle))
+ var/obj/item/weapon/paper_bundle/b = faxes[fax_id]
+ output["title"] = b.name ? b.name : "Untitled Paper Bundle"
+
+ if (!b.pages || !b.pages.len)
+ statuscode = 500
+ response = "Fax Paper Bundle is empty - This should not happen"
+ data = null
+ return TRUE
+
+ var/i = 0
+ for (var/obj/item/weapon/paper/c in b.pages)
+ i++
+ var/content = replacetext(c.info, "
", "\n")
+ content = strip_html_properly(content, 0)
+ output["content"] += "Page [i]:\n[content]\n\n"
+
+ statuscode = 200
+ response = "Fax (PaperBundle) retrieved"
+ data = output
+ return TRUE
+
+ statuscode = 500
+ response = "Unable to recognize the fax type. Cannot send contents!"
+ data = null
+ return TRUE
+
+//Send a Command Report
+/datum/topic_command/send_commandreport
+ name = "send_commandreport"
+ description = "Sends a command report"
+ params = list(
+ "senderkey" = list("name"="senderkey","desc"="Unique id of the person that sent the commandreport","req"=1,"type"="senderkey"),
+ "title" = list("name"="title","desc"="The message title that should be sent, Defaults to NanoTrasen Update if not specified","req"=0,"type"="str"),
+ "body" = list("name"="body","desc"="The message body that should be sent","req"=1,"type"="str"),
+ "type" = list("name"="type","desc"="The type of the message that should be sent, Defaults to freeform","req"=0,"type"="slct","options"=list("freeform","ccia")),
+ "sendername" = list("name"="sendername","desc"="IC Name of the sender for the CCIA Report, Defaults to CCIAAMS, \[Command-StationName\]","req"=0,"type"="str"),
+ "announce" = list("name"="announce","desc"="If the report should be announce 1 -> Yes, 0 -> No, Defaults to 1","req"=0,"type"="int")
+ )
+
+/datum/topic_command/send_commandreport/run_command(queryparams)
+ var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
+ var/reporttitle = sanitizeSafe(queryparams["title"]) //Title of the report
+ var/reportbody = nl2br(sanitize(queryparams["body"],encode=0,extra=0,max_length=0)) //Body of the report
+ var/reporttype = queryparams["type"] //Type of the report: freeform / ccia / admin
+ var/reportsender = sanitizeSafe(queryparams["sendername"]) //Name of the sender
+ var/reportannounce = text2num(queryparams["announce"]) //Announce the contents report to the public: 1 / 0
+
+ if(!reporttitle)
+ reporttitle = "NanoTrasen Update"
+ if(!reporttype)
+ reporttype = "freeform"
+ if(!reportannounce)
+ reportannounce = 1
+
+ //Set the report footer for CCIA Announcements
+ if (reporttype == "ccia")
+ if (reportsender)
+ reportbody += "
- [reportsender], Central Command Internal Affairs Agent, [commstation_name()]"
+ else
+ reportbody += "
- CCIAAMS, [commstation_name()]"
+
+ //Send the message to the communications consoles
+ post_comm_message(reporttitle, reportbody)
+
+ if(reportannounce == 1)
+ command_announcement.Announce(reportbody, reporttitle, new_sound = 'sound/AI/commandreport.ogg', do_newscast = 1, msg_sanitized = 1);
+ if(reportannounce == 0)
+ to_world("New NanoTrasen Update available at all communication consoles.")
+ to_world(sound('sound/AI/commandreport.ogg'))
+
+
+ log_admin("[senderkey] has created a command report via the api: [reportbody]",admin_key=senderkey)
+ message_admins("[senderkey] has created a command report via the api", 1)
+
+ statuscode = 200
+ response = "Command Report sent"
+ data = null
+ return TRUE
+
+//Send Fax
+/datum/topic_command/send_fax
+ name = "send_fax"
+ description = "Sends a fax"
+ params = list(
+ "senderkey" = list("name"="senderkey","desc"="Unique id of the person that sent the fax","req"=1,"type"="senderkey"),
+ "title" = list("name"="title","desc"="The message title that should be sent","req"=1,"type"="str"),
+ "body" = list("name"="body","desc"="The message body that should be sent","req"=1,"type"="str"),
+ "target" = list("name"="target","desc"="The target faxmachines the fax should be sent to","req"=1,"type"="lst")
+ )
+
+/datum/topic_command/send_fax/run_command(queryparams)
+ var/list/responselist = list()
+ var/list/sendsuccess = list()
+ var/list/targetlist = queryparams["target"] //Target locations where the fax should be sent to
+ var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
+ var/faxtitle = sanitizeSafe(queryparams["title"]) //Title of the report
+ var/faxbody = sanitize(queryparams["body"],max_length=0) //Body of the report
+ var/faxannounce = text2num(queryparams["announce"]) //Announce the contents report to the public: 0 - Dont announce, 1 - Announce, 2 - Only if pda not linked
+
+ if(!targetlist || targetlist.len < 1)
+ statuscode = 400
+ response = "Parameter target not set"
+ data = null
+ return TRUE
+
+ var/sendresult = 0
+ var/notifyresult = 1
+
+ //Send the fax
+ for (var/obj/machinery/photocopier/faxmachine/F in allfaxes)
+ if (F.department in targetlist)
+ sendresult = send_fax(F, faxtitle, faxbody, senderkey)
+ if (sendresult == 1)
+ sendsuccess.Add(F.department)
+ if(!LAZYLEN(F.alert_pdas))
+ notifyresult = 0
+ responselist[F.department] = "notlinked"
+ else
+ responselist[F.department] = "success"
+ else
+ responselist[F.department] = "failed"
+
+ //Announce that the fax has been sent
+ if(faxannounce == 1 || (faxannounce==2 && notifyresult==0))
+ if(sendsuccess.len < 1)
+ command_announcement.Announce("A fax message from Central Command could not be delivered because all of the following fax machines are inoperational:
"+jointext(targetlist, ", "), "Fax Delivery Failure", new_sound = 'sound/AI/commandreport.ogg', msg_sanitized = 1);
+ else
+ command_announcement.Announce("A fax message from Central Command has been sent to the following fax machines:
"+jointext(sendsuccess, ", "), "Fax Received", new_sound = 'sound/AI/commandreport.ogg', msg_sanitized = 1);
+
+ log_admin("[senderkey] sent a fax via the API: : [faxbody]",admin_key=senderkey)
+ message_admins("[senderkey] sent a fax via the API", 1)
+
+ statuscode = 200
+ response = "Fax sent"
+ data = responselist
+ return TRUE
+
+/datum/topic_command/send_fax/proc/send_fax(var/obj/machinery/photocopier/faxmachine/F, title, body, senderkey)
+ // Create the reply message
+ var/obj/item/weapon/paper/P = new /obj/item/weapon/paper( null ) //hopefully the null loc won't cause trouble for us
+ P.name = "[current_map.boss_name] - [title]"
+ P.info = body
+ P.update_icon()
+
+ // Stamps
+ var/image/stampoverlay = image('icons/obj/bureaucracy.dmi')
+ stampoverlay.icon_state = "paper_stamp-cent"
+ if(!P.stamped)
+ P.stamped = new
+ P.stamped += /obj/item/weapon/stamp
+ P.add_overlay(stampoverlay)
+ P.stamps += "
This paper has been stamped by the Central Command Quantum Relay."
+
+ if(F.receivefax(P))
+ log_and_message_admins("[senderkey] sent a fax message to the [F.department] fax machine via the api. (JMP)")
+ sent_faxes += P
+ return TRUE
+ else
+ qdel(P)
+ return FALSE
diff --git a/code/modules/world_api/commands/misc.dm b/code/modules/world_api/commands/misc.dm
new file mode 100644
index 00000000000..b45f5d9976a
--- /dev/null
+++ b/code/modules/world_api/commands/misc.dm
@@ -0,0 +1,172 @@
+// Gets the currently configured access levels
+/datum/topic_command/get_access_levels
+ name = "get_access_levels"
+ description = "Gets the currently configured access levels."
+
+/datum/topic_command/get_access_levels/run_command()
+ var/list/access_levels = list()
+ for(var/datum/access/acc in get_all_access_datums())
+ access_levels.Add(list(acc.get_info_list()))
+
+ data = access_levels
+ statuscode = 200
+ response = "Levels Sent"
+ return TRUE
+
+//Gets a overview of all polls (title, id, type)
+/datum/topic_command/get_polls
+ name = "get_polls"
+ description = "Gets a overview of all polls."
+ params = list(
+ "current_only" = list("name"="current_only","desc"="Only get information about the current polls","type"="int","req"=0),
+ "admin_only" = list("name"="admin_only","desc"="Only get information about the admin_only polls","type"="int","req"=0)
+ )
+
+/datum/topic_command/get_polls/run_command(queryparams)
+ var/current_only = text2num(queryparams["current_only"])
+ var/admin_only = text2num(queryparams["admin_only"])
+
+ if(!establish_db_connection(dbcon))
+ statuscode = 500
+ response = "DB-Connection unavailable"
+ return TRUE
+
+ var/list/polldata = list()
+
+ var/DBQuery/select_query = dbcon.NewQuery("SELECT id, polltype, starttime, endtime, question, multiplechoiceoptions, adminonly FROM ss13_poll_question [(current_only || admin_only) ? "WHERE" : ""] [(admin_only ? "adminonly = true " : "")][(current_only && admin_only ? "AND " : "")][(current_only ? "Now() BETWEEN starttime AND endtime" : "")]")
+ select_query.Execute()
+ while(select_query.NextRow())
+ polldata["[select_query.item[1]]"] = list(
+ "id"=select_query.item[1],
+ "polltype"=select_query.item[2],
+ "starttime"=select_query.item[3],
+ "endtime"=select_query.item[4],
+ "question"=select_query.item[5],
+ "multiplechoiceoptions"=select_query.item[6],
+ "adminonly"=select_query.item[7]
+ )
+
+ statuscode = 200
+ response = "Polldata sent"
+ data = polldata
+ return TRUE
+
+
+// Gets infos about a poll
+/datum/topic_command/get_poll_info
+ name = "get_poll_info"
+ description = "Gets Information about a poll."
+ params = list(
+ "poll_id" = list("name"="poll_id","desc"="The poll id that should be queried","type"="int","req"=1)
+ )
+
+/datum/topic_command/get_poll_info/run_command(queryparams)
+ var/poll_id = text2num(queryparams["poll_id"])
+
+ if(!establish_db_connection(dbcon))
+ statuscode = 500
+ response = "DB-Connection unavailable"
+ return TRUE
+
+ //Get general data about the poll
+ var/DBQuery/select_query = dbcon.NewQuery("SELECT id, polltype, starttime, endtime, question, multiplechoiceoptions, adminonly, publicresult, viewtoken FROM ss13_poll_question WHERE id = :poll_id:")
+ select_query.Execute(list("poll_id"=poll_id))
+
+ //Check if the poll exists
+ if(!select_query.NextRow())
+ statuscode = 404
+ response = "The requested poll does not exist"
+ data = null
+ return TRUE
+ var/list/poll_data = list(
+ "id"=select_query.item[1],
+ "polltype"=select_query.item[2],
+ "starttime"=select_query.item[3],
+ "endtime"=select_query.item[4],
+ "question"=select_query.item[5],
+ "multiplechoiceoptions"=select_query.item[6],
+ "adminonly"=select_query.item[7],
+ "publicresult"=select_query.item[8]
+ )
+
+ //Lets add a WI link to the poll, if we have the WI configured
+ if(config.webint_url)
+ poll_data["link"]="[config.webint_url]server/poll/[select_query.item[1]]/[select_query.item[9]]"
+
+ var/list/result_data = list()
+
+ /** Return different data based on the poll type: */
+ //If we have a option or a multiple choice poll, return the number of options
+ if(poll_data["polltype"] == "OPTION" || poll_data["polltype"] == "MULTICHOICE")
+ var/DBQuery/result_query = dbcon.NewQuery({"SELECT ss13_poll_vote.optionid, ss13_poll_option.text, COUNT(*) as option_count
+ FROM ss13_poll_vote
+ LEFT JOIN ss13_poll_option ON ss13_poll_vote.optionid = ss13_poll_option.id
+ WHERE ss13_poll_vote.pollid = :poll_id:
+ GROUP BY ss13_poll_vote.optionid"})
+ result_query.Execute(list("poll_id"=poll_id))
+
+ while(result_query.NextRow())
+ result_data["[result_query.item[1]]"] = list(
+ "option_id"=result_query.item[1],
+ "option_question"=result_query.item[2],
+ "option_count"=result_query.item[3]
+ )
+ if(!length(result_data))
+ statuscode = 500
+ response = "No data returned by result query."
+ data = null
+ return TRUE
+
+ //If we have a numval poll, return the options with the min, max, and average
+ else if(poll_data["polltype"] == "NUMVAL")
+ var/DBQuery/result_query = dbcon.NewQuery({"SELECT ss13_poll_vote.optionid, ss13_poll_option.text, ss13_poll_option.minval, ss13_poll_option.maxval, ss13_poll_option.descmin, ss13_poll_option.descmid, ss13_poll_option.descmax, AVG(rating) as option_rating_avg, MIN(rating) as option_rating_min, MAX(rating) as option_rating_max
+ FROM ss13_poll_vote
+ LEFT JOIN ss13_poll_option ON ss13_poll_vote.optionid = ss13_poll_option.id
+ WHERE ss13_poll_vote.pollid = :poll_id:
+ GROUP BY ss13_poll_vote.optionid"})
+ result_query.Execute(list("poll_id"=poll_id))
+ while(result_query.NextRow())
+ result_data["[result_query.item[1]]"] = list(
+ "option_id"=result_query.item[1],
+ "option_question"=result_query.item[2],
+ "option_minval"=result_query.item[3],
+ "option_maxval"=result_query.item[4],
+ "option_descmin"=result_query.item[5],
+ "option_descmid"=result_query.item[6],
+ "option_descmax"=result_query.item[7],
+ "option_rating_min"=result_query.item[8],
+ "option_rating_max"=result_query.item[9],
+ "option_rating_avg"=result_query.item[10] //TODO: Expand that with MEDIAN once we upgrade mariadb
+ )
+ if(!length(result_data))
+ statuscode = 500
+ response = "No data returned by result query."
+ data = null
+ return TRUE
+
+ //If we have a textpoll, return the number of answers
+ else if(poll_data["polltype"] == "TEXT")
+ var/DBQuery/result_query = dbcon.NewQuery({"SELECT COUNT(*) as count FROM ss13_poll_textreply WHERE pollid = :poll_id:"})
+ result_query.Execute(list("poll_id"=poll_id))
+ if(result_query.NextRow())
+ result_data = list(
+ "response_count"=result_query.item[1]
+ )
+ else
+ statuscode = 500
+ response = "No data returned by result query."
+ data = null
+ return TRUE
+ else
+ statuscode = 500
+ response = "Unknown Poll Type"
+ data = poll_data
+ return TRUE
+
+
+ poll_data["results"] = result_data
+
+ statuscode = 200
+ response = "Poll data fetched"
+ data = poll_data
+ return TRUE
diff --git a/code/modules/world_api/commands/server_management.dm b/code/modules/world_api/commands/server_management.dm
new file mode 100644
index 00000000000..cc12d4d6b8e
--- /dev/null
+++ b/code/modules/world_api/commands/server_management.dm
@@ -0,0 +1,90 @@
+// Reloads the current cargo configuration
+/datum/topic_command/cargo_reload
+ name = "cargo_reload"
+ description = "Reloads the current cargo configuration."
+ params = list(
+ "force" = list("name"="force","desc"="Force the reload even if orders have already been placed","type"="int","req"=0)
+ )
+
+/datum/topic_command/cargo_reload/run_command(queryparams)
+ var/force = text2num(queryparams["force"])
+ if(!SScargo.get_order_count())
+ SScargo.load_from_sql()
+ message_admins("Cargo has been reloaded via the API.")
+ statuscode = 200
+ response = "Cargo Reloaded from SQL."
+ else
+ if(force)
+ SScargo.load_from_sql()
+ message_admins("Cargo has been force-reloaded via the API. All current orders have been purged.")
+ statuscode = 200
+ response = "Cargo Force-Reloaded from SQL."
+ else
+ statuscode = 500
+ response = "Orders have been placed. Use force parameter to overwrite."
+ return TRUE
+
+// Update discord_bot's channels.
+/datum/topic_command/update_bot_channels
+ name = "update_bot_channels"
+ description = "Tells the ingame instance of the Discord bot to update its cached channels list."
+
+/datum/topic_command/update_bot_channels/run_command()
+ data = null
+
+ if (!discord_bot)
+ statuscode = 404
+ response = "Ingame Discord bot not initialized."
+ return 1
+
+ switch (discord_bot.update_channels())
+ if (1)
+ statuscode = 404
+ response = "Ingame Discord bot is not active."
+ if (2)
+ statuscode = 500
+ response = "Ingame Discord bot encountered error attempting to access database."
+ else
+ statuscode = 200
+ response = "Ingame Discord bot's channels were successfully updated."
+
+ return TRUE
+
+//Restart Round
+/datum/topic_command/restart_round
+ name = "restart_round"
+ description = "Restarts the round"
+ params = list(
+ "senderkey" = list("name"="senderkey","desc"="Unique id of the person that authorized the restart","req"=1,"type"="str")
+ )
+
+/datum/topic_command/restart_round/run_command(queryparams)
+ var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
+
+ to_world("Server restarting by remote command.")
+ log_and_message_admins("World restart initiated remotely by [senderkey].")
+ feedback_set_details("end_error","remote restart")
+
+ spawn(50)
+ log_game("Rebooting due to remote command.")
+ world.Reboot("Rebooting due to remote command.")
+
+ statuscode = 200
+ response = "Restart Command accepted"
+ data = null
+ return TRUE
+
+//Sends a text to everyone on the server
+/datum/topic_command/broadcast_text
+ name = "broadcast_text"
+ description = "Sends a text to everyone on the server."
+ params = list(
+ "text" = list("name"="text","desc"="The text that should be sent","req"=1,"type"="str")
+ )
+
+/datum/topic_command/broadcast_text/run_command(queryparams)
+ to_world(queryparams["text"])
+
+ statuscode = 200
+ response = "Text sent"
+ return TRUE
diff --git a/code/modules/world_api/commands/server_query.dm b/code/modules/world_api/commands/server_query.dm
new file mode 100644
index 00000000000..92d5a6c7e6e
--- /dev/null
+++ b/code/modules/world_api/commands/server_query.dm
@@ -0,0 +1,295 @@
+//Get Server Status
+/datum/topic_command/get_serverstatus
+ name = "get_serverstatus"
+ description = "Gets the server status."
+ no_auth = TRUE
+
+/datum/topic_command/get_serverstatus/run_command(queryparams)
+ var/list/s[] = list()
+ s["version"] = game_version
+ s["mode"] = master_mode
+ s["respawn"] = config.abandon_allowed
+ s["enter"] = config.enter_allowed
+ s["vote"] = config.allow_vote_mode
+ s["ai"] = config.allow_ai
+ s["host"] = host ? host : null
+ s["stationtime"] = worldtime2text()
+ s["roundduration"] = get_round_duration_formatted()
+ s["gameid"] = game_id
+ s["game_state"] = SSticker ? 0 : SSticker.current_state
+ s["transferring"] = emergency_shuttle ? !emergency_shuttle.online() : FALSE
+
+ s["players"] = clients.len
+ s["admins"] = 0
+
+ for(var/client/C in clients)
+ if(C.holder)
+ if(C.holder.fakekey)
+ continue
+
+ s["admins"]++
+
+ statuscode = 200
+ response = "Server status fetched."
+ data = s
+ return TRUE
+
+
+//Get a Staff List
+/datum/topic_command/get_stafflist
+ name = "get_stafflist"
+ description = "Gets a list of connected staffmembers"
+
+/datum/topic_command/get_stafflist/run_command(queryparams)
+ var/list/staff = list()
+ for (var/client/C in admins)
+ staff[C] = C.holder.rank
+
+ statuscode = 200
+ response = "Staff list fetched"
+ data = staff
+ return TRUE
+
+//Char Names
+/datum/topic_command/get_char_list
+ name = "get_char_list"
+ description = "Provides a list of all characters ingame"
+
+/datum/topic_command/get_char_list/run_command(queryparams)
+ var/list/chars = list()
+
+ var/list/mobs = sortmobs()
+ for(var/mob/M in mobs)
+ if(!M.ckey) continue
+ chars[M.name] += M.key ? (M.client ? M.key : "[M.key] (DC)") : "No key"
+
+ statuscode = 200
+ response = "Char list fetched"
+ data = chars
+ return TRUE
+
+//Admin Count
+/datum/topic_command/get_count_admin
+ name = "get_count_admin"
+ description = "Gets the number of admins connected"
+
+/datum/topic_command/get_count_admin/run_command(queryparams)
+ var/n = 0
+ for (var/client/client in clients)
+ if (client.holder && client.holder.rights & (R_ADMIN))
+ n++
+
+ statuscode = 200
+ response = "Admin count fetched"
+ data = n
+ return TRUE
+
+//CCIA Count
+/datum/topic_command/get_count_cciaa
+ name = "get_count_cciaa"
+ description = "Gets the number of ccia connected"
+
+/datum/topic_command/get_count_ccia/run_command(queryparams)
+ var/n = 0
+ for (var/client/client in clients)
+ if (client.holder && (client.holder.rights & R_CCIAA) && !(client.holder.rights & R_ADMIN))
+ n++
+
+ statuscode = 200
+ response = "CCIA count fetched"
+ data = n
+ return TRUE
+
+//Mod Count
+/datum/topic_command/get_count_mod
+ name = "get_count_mod"
+ description = "Gets the number of mods connected"
+
+/datum/topic_command/get_count_mod/run_command(queryparams)
+ var/n = 0
+ for (var/client/client in clients)
+ if (client.holder && (client.holder.rights & R_MOD) && !(client.holder.rights & R_ADMIN))
+ n++
+
+ statuscode = 200
+ response = "Mod count fetched"
+ data = n
+ return TRUE
+
+//Player Count
+/datum/topic_command/get_count_player
+ name = "get_count_player"
+ description = "Gets the number of players connected"
+ no_auth = TRUE
+
+/datum/topic_command/get_count_player/run_command(queryparams)
+ var/n = 0
+ for(var/mob/M in player_list)
+ if(M.client)
+ n++
+
+ statuscode = 200
+ response = "Player count fetched"
+ data = n
+ return TRUE
+
+//Get Ghosts
+/datum/topic_command/get_ghosts
+ name = "get_ghosts"
+ description = "Gets the ghosts"
+
+/datum/topic_command/get_ghosts/run_command(queryparams)
+ var/list/ghosts[] = list()
+ ghosts = get_ghosts(1,1)
+
+ statuscode = 200
+ response = "Fetched Ghost list"
+ data = ghosts
+ return TRUE
+
+// Crew Manifest
+/datum/topic_command/get_manifest
+ name = "get_manifest"
+ description = "Gets the crew manifest"
+
+/datum/topic_command/get_manifest/run_command(queryparams)
+ var/list/positions = list()
+ var/list/set_names = list(
+ "heads" = command_positions,
+ "sec" = security_positions,
+ "eng" = engineering_positions,
+ "med" = medical_positions,
+ "sci" = science_positions,
+ "civ" = civilian_positions,
+ "bot" = nonhuman_positions
+ )
+
+ for(var/datum/data/record/t in data_core.general)
+ var/name = t.fields["name"]
+ var/rank = t.fields["rank"]
+ var/real_rank = make_list_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
+
+ statuscode = 200
+ response = "Manifest fetched"
+ data = positions
+ return TRUE
+
+//Player Ckeys
+/datum/topic_command/get_player_list
+ name = "get_player_list"
+ description = "Gets a list of connected players"
+ params = list(
+ "showadmins" = list("name"="show admins","desc"="A boolean to toggle whether or not hidden admins should be shown with proper or improper ckeys.","req"=0,"type"="int")
+ )
+
+/datum/topic_command/get_player_list/run_command(queryparams)
+ var/show_hidden_admins = 0
+
+ if (!isnull(queryparams["showadmins"]))
+ show_hidden_admins = text2num(queryparams["showadmins"])
+
+ var/list/players = list()
+ for (var/client/C in clients)
+ if (show_hidden_admins && C.holder && C.holder.fakekey)
+ players += ckey(C.holder.fakekey)
+ else
+ players += C.ckey
+
+ statuscode = 200
+ response = "Player list fetched"
+ data = players
+ return TRUE
+
+//Get info about a specific player
+/datum/topic_command/get_player_info
+ name = "get_player_info"
+ description = "Gets information about a specific player"
+ params = list(
+ "search" = list("name"="search","desc"="List with strings that should be searched for","req"=1,"type"="lst")
+ )
+
+/datum/topic_command/get_player_info/run_command(queryparams)
+ var/list/search = queryparams["search"]
+
+ var/list/ckeysearch = list()
+ for(var/text in search)
+ ckeysearch += ckey(text)
+
+ var/list/match = list()
+
+ for(var/mob/M in mob_list)
+ var/strings = list(M.name, M.ckey)
+ if(M.mind)
+ strings += M.mind.assigned_role
+ strings += M.mind.special_role
+ for(var/text in strings)
+ if(ckey(text) in ckeysearch)
+ match[M] += 10 // an exact match is far better than a partial one
+ else
+ for(var/searchstr in search)
+ if(findtext(text, searchstr))
+ match[M] += 1
+
+ var/maxstrength = 0
+ for(var/mob/M in match)
+ maxstrength = max(match[M], maxstrength)
+ for(var/mob/M in match)
+ if(match[M] < maxstrength)
+ match -= M
+
+ if(!match.len)
+ statuscode = 449
+ response = "No match found"
+ data = null
+ return TRUE
+ else if(match.len == 1)
+ var/mob/M = match[1]
+ var/info = list()
+ info["key"] = M.key
+ if (M.client)
+ var/client/C = M.client
+ info["discordmuted"] = C.mute_discord ? "Yes" : "No"
+ info["name"] = M.name == M.real_name ? M.name : "[M.name] ([M.real_name])"
+ info["role"] = M.mind ? (M.mind.assigned_role ? M.mind.assigned_role : "No role") : "No mind"
+ var/turf/MT = get_turf(M)
+ info["loc"] = M.loc ? "[M.loc]" : "null"
+ info["turf"] = MT ? "[MT] @ [MT.x], [MT.y], [MT.z]" : "null"
+ info["area"] = MT ? "[MT.loc]" : "null"
+ info["antag"] = M.mind ? (M.mind.special_role ? M.mind.special_role : "Not antag") : "No mind"
+ info["hasbeenrev"] = M.mind ? M.mind.has_been_rev : "No mind"
+ info["stat"] = M.stat
+ info["type"] = M.type
+ if(isliving(M))
+ var/mob/living/L = M
+ info["damage"] = list2params(list(
+ oxy = L.getOxyLoss(),
+ tox = L.getToxLoss(),
+ fire = L.getFireLoss(),
+ brute = L.getBruteLoss(),
+ clone = L.getCloneLoss(),
+ brain = L.getBrainLoss()
+ ))
+ else
+ info["damage"] = "non-living"
+ info["gender"] = M.gender
+ statuscode = 200
+ response = "Client data fetched"
+ data = info
+ return TRUE
+ else
+ statuscode = 449
+ response = "Multiple Matches found"
+ data = null
+ return TRUE
diff --git a/code/modules/world_api/commands/tickets.dm b/code/modules/world_api/commands/tickets.dm
new file mode 100644
index 00000000000..f89354631ae
--- /dev/null
+++ b/code/modules/world_api/commands/tickets.dm
@@ -0,0 +1,105 @@
+/datum/topic_command/tickets_info
+ name = "get_ticketsinfo"
+ description = "Lists a general overview of tickets in the current round."
+
+/datum/topic_command/tickets_info/run_command(queryparams)
+ statuscode = 200
+ response = "General tickets overview."
+
+ var/list/ticket_data = list(
+ "total" = tickets.len,
+ "assigned" = 0,
+ "unassigned" = 0,
+ "closed" = 0
+ )
+
+ for (var/id in tickets)
+ var/datum/ticket/ticket = tickets[id]
+ switch (ticket.status)
+ if (TICKET_OPEN)
+ ticket_data["unassigned"]++
+ if (TICKET_ASSIGNED)
+ ticket_data["assigned"]++
+ if (TICKET_CLOSED)
+ ticket_data["closed"]++
+
+ data = ticket_data
+ return TRUE
+
+
+/datum/topic_command/tickets_list
+ name = "get_ticketslist"
+ description = "Lists tickets in the current round."
+ params = list(
+ "only_open" = list("name"="only_open","desc"="If present, only opened tickets are listed.","req"=0,"type"="int")
+ )
+
+/datum/topic_command/tickets_list/run_command(queryparams)
+ statuscode = 200
+ response = "Tickets list."
+
+ var/only_open = !!queryparams["only_open"]
+
+ var/list/ticket_data = list()
+
+ for (var/datum/ticket/ticket in tickets)
+ if (!only_open || ticket.status == TICKET_CLOSED)
+ continue
+
+ ticket_data["[ticket.id]"] = serialize_ticket(ticket)
+
+ data = ticket_data
+ return TRUE
+
+/datum/topic_command/tickets_list/proc/serialize_ticket(datum/ticket/ticket)
+ return list(
+ "id" = ticket.id,
+ "owner" = ticket.owner,
+ "status" = ticket.status,
+ "closed_by" = ticket.closed_by,
+ "opened_time" = ticket.opened_time,
+ "assigned_admins" = ticket.assigned_admins,
+ "message_count" = ticket.msgs.len
+ )
+
+/datum/topic_command/tickets_close
+ name = "tickets_close"
+ description = "Closes the listed ticket."
+ params = list(
+ "id" = list("name"="id","desc"="The ID of the ticket to be closed.","req"=1,"type"="int"),
+ "admin" = list("name"="admin","desc"="Ckey of the admin who is closing the ticket.","req"=1,"type"="str")
+ )
+
+/datum/topic_command/tickets_close/run_command(queryparams)
+ if (!queryparams["id"] || !isnum(queryparams["id"]))
+ response = "No or invalid ID provided."
+ statuscode = 400
+ data = null
+ return TRUE
+
+ var/id = text2num(queryparams["id"])
+
+ if (!queryparams["admin"] || !ckey(queryparams["admin"]))
+ response = "No administrator ckey provided."
+ statuscode = 400
+ data = null
+ return TRUE
+
+ var/ckey = ckey(queryparams["admin"])
+
+ var/datum/ticket/ticket = get_ticket_by_id(id)
+
+ if (!ticket)
+ response = "Ticket not found with the given ID."
+ statuscode = 404
+ data = null
+ return TRUE
+
+ if (ticket.close_remotely(ckey))
+ response = "Ticket successfully closed."
+ statuscode = 200
+ return TRUE
+ else
+ response = "Unable to close the ticket."
+ statuscode = 500
+ return TRUE
diff --git a/code/modules/world_api/helpers.dm b/code/modules/world_api/helpers.dm
new file mode 100644
index 00000000000..dca8d205dbf
--- /dev/null
+++ b/code/modules/world_api/helpers.dm
@@ -0,0 +1,16 @@
+//Init the API at startup
+/hook/startup/proc/setup_api()
+ for (var/path in typesof(/datum/topic_command) - /datum/topic_command)
+ var/datum/topic_command/A = new path()
+ if(A != null)
+ topic_commands[A.name] = A
+ topic_commands_names.Add(A.name)
+ listclearnulls(topic_commands)
+ listclearnulls(topic_commands_names)
+
+ if (config.api_rate_limit_whitelist.len)
+ // To make the api_rate_limit_whitelist[addr] grabs actually work.
+ for (var/addr in config.api_rate_limit_whitelist)
+ config.api_rate_limit_whitelist[addr] = 1
+
+ return 1
diff --git a/code/world.dm b/code/world.dm
index 6c83e08c3cc..87305e636fd 100644
--- a/code/world.dm
+++ b/code/world.dm
@@ -153,7 +153,7 @@ var/list/world_api_rate_limit = list()
response["response"] = "Not Implemented"
return json_encode(response)
- var/unauthed = api_do_auth_check(addr,auth,command)
+ var/unauthed = command.check_auth(addr, auth)
if (unauthed)
if (unauthed == 3)
log_debug("API: Request denied - Auth Service Unavailable")