Rewrite API (#825)

he /world/Topic() API has been rewritten.

General function:

The API is initialized upon roundstart and generates a list of possible api requests from /code/datums/api.dm
Once a request is made the following checks are performed:
If a query parameter is set (the function that should be called)
If the ip is ratelimited (or rate-limit-whitelisted)
The query is authenticated against the db using ip, function and key
If these checks pass, it is validated that the api command exists
Then the api command is called, all supplied params are passed through
Each API function returns 1 no matter if it failed or succeded
Additional info is provided in the statuscode, response and data vars
The statuscode, response and data are send back to the caller (through list2params)
The data var is json encoded before sending it back
This is not backward compatible to the current API
Current API Clients need to be updated
This commit is contained in:
Werner
2016-09-04 21:41:44 +02:00
committed by skull132
parent 2118078422
commit fb25382405
10 changed files with 911 additions and 437 deletions

View File

@@ -19,6 +19,41 @@ CREATE TABLE `ss13_admin_log` (
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `ss13_api_functions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`function` VARCHAR(50) NULL DEFAULT '' COLLATE 'utf8_bin',
`description` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_bin',
PRIMARY KEY (`id`)
)
COLLATE='utf8_bin'
ENGINE=InnoDB;
CREATE TABLE `ss13_api_tokens` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`token` VARCHAR(100) NOT NULL COLLATE 'utf8_bin',
`ip` VARCHAR(16) NULL DEFAULT NULL COLLATE 'utf8_bin',
`creator` VARCHAR(50) NOT NULL COLLATE 'utf8_bin',
`description` VARCHAR(100) NOT NULL COLLATE 'utf8_bin',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` DATETIME NULL DEFAULT NULL,
PRIMARY KEY (`id`)
)
COLLATE='utf8_bin'
ENGINE=InnoDB;
CREATE TABLE `ss13_api_token_function` (
`function_id` INT(11) NOT NULL,
`token_id` INT(11) NOT NULL,
PRIMARY KEY (`function_id`, `token_id`),
INDEX `token_id` (`token_id`),
CONSTRAINT `function_id` FOREIGN KEY (`function_id`) REFERENCES `ss13_api_functions` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `token_id` FOREIGN KEY (`token_id`) REFERENCES `ss13_api_tokens` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
)
COLLATE='utf8_bin'
ENGINE=InnoDB;
CREATE TABLE `ss13_ban` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`bantime` datetime NOT NULL,

View File

@@ -124,6 +124,7 @@
#include "code\controllers\ProcessScheduler\core\processScheduler.dm"
#include "code\datums\ai_law_sets.dm"
#include "code\datums\ai_laws.dm"
#include "code\datums\api.dm"
#include "code\datums\browser.dm"
#include "code\datums\computerfiles.dm"
#include "code\datums\crew.dm"

View File

@@ -11,6 +11,7 @@ var/global/list/human_mob_list = list() //List of all human mobs and sub-type
var/global/list/silicon_mob_list = list() //List of all silicon mobs, including clientless
var/global/list/living_mob_list = list() //List of all alive mobs, including clientless. Excludes /mob/new_player
var/global/list/dead_mob_list = list() //List of all dead mobs, including clientless. Excludes /mob/new_player
var/global/list/topic_commands = list() //List of all API commands available
var/global/list/cable_list = list() //Index for all cables, so that powernets don't have to look through the entire world all the time
var/global/list/chemical_reactions_list //list of all /datum/chemical_reaction datums. Used during chemical reactions

View File

@@ -230,6 +230,10 @@ var/list/gamemode_cache = list()
//AUG2016
var/antag_contest_enabled = 0
//API Rate limiting
var/api_rate_limit = 50
var/list/api_rate_limit_whitelist = list()
/datum/configuration/New()
var/list/L = typesof(/datum/game_mode) - /datum/game_mode
for (var/T in L)
@@ -729,6 +733,12 @@ var/list/gamemode_cache = list()
if("antag_contest_enabled")
config.antag_contest_enabled = 1
if("api_rate_limit")
config.api_rate_limit = text2num(value)
if("api_rate_limit_whitelist")
config.api_rate_limit_whitelist = text2list(value, ";")
else
log_misc("Unknown setting in configuration: '[name]'")

712
code/datums/api.dm Normal file
View File

@@ -0,0 +1,712 @@
//
// This file contains the API functions for the serverside API
//
//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()
topic_commands[A.name] = A
return 1
//API Boilerplate
/datum/topic_command
var/name = null
var/list/required_params = list() //Required Parameters for the command
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/param in required_params)
if(queryparams[param] == null)
errorcount ++
missing_params += param
if(errorcount)
statuscode = 400
response = "Required params missing"
data = missing_params
return errorcount
//Char Names
/datum/topic_command/get_char_list
name = "get_char_list"
/datum/topic_command/get_char_list/run_command(queryparams)
if (!ticker)
statuscode = 500
response = "Game not started yet!"
return 1
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"
/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_ccia
name = "get_count_ccia"
/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"
/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"
/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"
/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"
required_params = list("faxtype") //Type of the faxes to be retrieved (sent / received)
/datum/topic_command/get_faxlist/run_command(queryparams)
if (!ticker)
statuscode = 500
response = "Round hasn't started yet! No faxes to display!"
data = null
return 1
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"
required_params = list("faxtype","faxid")
/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, "<br>", "\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, "<br>", "\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"
/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"
/datum/topic_command/get_manifest/run_command(queryparams)
if (!ticker)
statuscode = 500
response = "Game not started yet!"
return 1
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"
/datum/topic_command/get_player_list/run_command(queryparams)
var/list/players = list()
for (var/client/C in clients)
players += C.key
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"
required_params = list("search") //search --> list with data to search for
/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"
/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"] = round_duration()
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"] = list2params(players)
s["admins"] = admins.len
s["adminlist"] = list2params(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"
/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"
required_params = list("senderkey","target")
/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/dead/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("\blue <B>You may now respawn. You should roleplay as if you learned nothing about the round during your time with the dead.</B>"), 1)
log_admin("[senderkey] allowed [key_name(G)] to bypass the 30 minute respawn limit via the API")
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"
/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"
required_params = list("senderkey")
/datum/topic_command/restart_round/run_command(queryparams)
var/senderkey = sanitize(queryparams["senderkey"]) //Identifier of the sender (Ckey / Userid / ...)
world << "<font size=4 color='#ff2222'>Server restarting by remote command.</font>"
log_and_message_admins("World restart initiated remotely by [senderkey].")
feedback_set_details("end_error","remote restart")
if (blackbox)
blackbox.save_all_data_to_sql()
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"
required_params = list("ckey","msg","senderkey")
/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 = "<font color='red'>[rank] PM from <b><a href='?discord_msg=[queryparams["senderkey"]]'>[queryparams["senderkey"]]</a></b>: [queryparams["msg"]]</font>"
var/amessage = "<font color='blue'>[rank] PM from <a href='?discord_msg=[queryparams["senderkey"]]'>[queryparams["senderkey"]]</a> to <b>[key_name(C)]</b> : [queryparams["msg"]]</font>"
C.received_discord_pm = world.time
C.discord_admin = queryparams["senderkey"]
C << 'sound/effects/adminhelp.ogg'
C << message
for(var/client/A in admins)
if(A != C)
A << amessage
statuscode = 200
response = "Admin Message sent"
data = null
return 1
//Send a Command Report
/datum/topic_command/send_commandreport
name = "send_commandreport"
required_params = list("senderkey","body")
/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 = sanitize(queryparams["body"]) //Body of the report
var/reporttype = queryparams["type"] //Type of the report: freeform / ccia / admin
var/reportsender = sanitize(queryparams["sendername"]) //Name of the sender
var/reportannounce = queryparams["announce"] //Announce the contents report to the public: 1 / 0
if(!reporttitle)
reporttitle = "NanoTrasen Update"
if(!reporttype)
reporttype = "freeform"
if(!reportannounce)
reportannounce = 1
//Send the message to the communications consoles
for (var/obj/machinery/computer/communications/C in machines)
if(! (C.stat & (BROKEN|NOPOWER) ) )
var/obj/item/weapon/paper/P = new /obj/item/weapon/paper( C.loc )
P.name = "[command_name()] Update"
P.info = replacetext(reportbody, "\n", "<br/>")
P.update_space(P.info)
P.update_icon()
C.messagetitle.Add("[command_name()] Update")
C.messagetext.Add(P.info)
//Set the report footer for CCIA Announcements
if (reporttype == "ccia")
if (reportsender)
reportbody += "<br/><br/>- [reportsender], Central Command Internal Affairs Agent, [commstation_name()]"
else
reportbody += "<br/><br/>- CCIAAMS, [commstation_name()]"
if(reportannounce == 1)
command_announcement.Announce(reportbody, reporttitle, new_sound = 'sound/AI/commandreport.ogg', msg_sanitized = 1);
if(reportannounce == 0)
world << "\red New NanoTrasen Update available at all communication consoles."
world << sound('sound/AI/commandreport.ogg')
log_admin("[senderkey] has created a command report via the api: [reportbody]")
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"
required_params = list("target","senderkey","title","body")
/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"]) //Body of the report
var/faxsender = sanitize(queryparams["sendername"]) //Name of the sender
var/faxannounce = queryparams["announce"] //Announce the contents report to the public: 1 / 0
if(!targetlist || targetlist.len < 1)
statuscode = 400
response = "Parameter target not set"
data = null
return 1
if(!faxannounce)
faxannounce = 1
var/sendresult = 0
//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)
responselist[F.department] = "success"
else
responselist[F.department] = "failed"
//Announce that the fax has been sent
if(faxannounce == 1)
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: <br>"+list2text(targetlist, ", "), "Fax Received", 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: <br>"+list2text(sendsuccess, ", "), "Fax Received", new_sound = 'sound/AI/commandreport.ogg', msg_sanitized = 1);
log_admin("[faxsender] sent a fax via the API: : [faxbody]")
message_admins("[faxsender] 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 = "[command_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.overlays += stampoverlay
P.stamps += "<HR><i>This paper has been stamped by the Central Command Quantum Relay.</i>"
if(F.recievefax(P))
log_and_message_admins("[senderkey] sent a fax message to the [F.department] fax machine via the api. (<A HREF='?_src_=holder;adminplayerobservecoodjump=1;X=[F.x];Y=[F.y];Z=[F.z]'>JMP</a>)")
sent_faxes += P
return 1
else
qdel(P)
return 2

View File

@@ -229,20 +229,20 @@ Allow admins to set players to be able to respawn/bypass 30 min wait, without th
Ccomp's first proc.
*/
/client/proc/get_ghosts(var/notify = 0,var/what = 2)
proc/get_ghosts(var/notify = 0,var/what = 2, var/client/C = null)
// what = 1, return ghosts ass list.
// what = 2, return mob list
var/list/mobs = list()
var/list/ghosts = list()
var/list/sortmob = sortAtom(mob_list) // get the mob list.
/var/any=0
var/any=0
for(var/mob/dead/observer/M in sortmob)
mobs.Add(M) //filter it where it's only ghosts
any = 1 //if no ghosts show up, any will just be 0
if(!any)
if(notify)
src << "There doesn't appear to be any ghosts for you to select."
if(notify && C)
C << "There doesn't appear to be any ghosts for you to select."
return
for(var/mob/M in mobs)
@@ -260,7 +260,7 @@ Ccomp's first proc.
set desc = "Let's the player bypass the 30 minute wait to respawn or allow them to re-enter their corpse."
if(!holder)
src << "Only administrators may use this command."
var/list/ghosts= get_ghosts(1,1)
var/list/ghosts = get_ghosts(1,1,src)
var/target = input("Please, select a ghost!", "COME BACK TO LIFE!", null, null) as null|anything in ghosts
if(!target)

View File

@@ -193,24 +193,23 @@ var/list/sent_faxes = list() //cache for faxes that have been sent by the admins
if(department == "Unknown")
return 0 //You can't send faxes to "Unknown"
if (!istype(incoming, /obj/item/weapon/paper) && !istype(incoming, /obj/item/weapon/photo) && !istype(incoming, /obj/item/weapon/paper_bundle))
return 0
flick("faxreceive", src)
playsound(loc, "sound/items/polaroid1.ogg", 50, 1)
// give the sprite some time to flick
sleep(20)
spawn(20)
if (istype(incoming, /obj/item/weapon/paper))
copy(incoming)
else if (istype(incoming, /obj/item/weapon/photo))
photocopy(incoming)
else if (istype(incoming, /obj/item/weapon/paper_bundle))
bundlecopy(incoming)
do_pda_alerts()
use_power(active_power_usage)
if (istype(incoming, /obj/item/weapon/paper))
copy(incoming)
else if (istype(incoming, /obj/item/weapon/photo))
photocopy(incoming)
else if (istype(incoming, /obj/item/weapon/paper_bundle))
bundlecopy(incoming)
else
return 0
do_pda_alerts()
use_power(active_power_usage)
return 1
/obj/machinery/photocopier/faxmachine/proc/send_admin_fax(var/mob/sender, var/destination)

View File

@@ -108,414 +108,65 @@ var/global/datum/global_init/init = new ()
return
var/world_topic_spam_protect_ip = "0.0.0.0"
var/world_topic_spam_protect_time = world.timeofday
var/list/world_api_rate_limit = list()
/world/Topic(T, addr, master, key)
diary << "TOPIC: \"[T]\", from:[addr], master:[master], key:[key][log_end]"
var/list/response[] = list()
var/list/queryparams[] = json_decode(T)
var/query = queryparams["query"]
var/auth = queryparams["auth"]
log_debug("API: Request Received - from:[addr], master:[master], key:[key]")
diary << "TOPIC: \"[T]\", from:[addr], master:[master], key:[key], auth:[auth] [log_end]"
if (T == "ping")
var/x = 1
for (var/client/C)
x++
return x
if (isnull(query))
log_debug("API - Bad Request - No query specified")
response["statuscode"] = 400
response["response"] = "Bad Request - No query specified"
return json_encode(response)
else if(T == "players")
var/n = 0
for(var/mob/M in player_list)
if(M.client)
n++
return n
else if (T == "admins")
var/n = 0
for (var/client/client in clients)
if (client.holder && client.holder.rights & (R_MOD|R_ADMIN))
n++
return n
else if (T == "cciaa")
var/n = 0
for (var/client/client in clients)
if (client.holder && (client.holder.rights & R_CCIAA) && !(client.holder.rights & R_ADMIN))
n++
return n
else if (T == "gamemode")
return master_mode
else if (T == "who")
var/list/players = list()
for (var/client/C in clients)
players += C.key
return list2params(players)
else if (copytext(T,1,7) == "status")
var/input[] = params2list(T)
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
// This is dumb, but spacestation13.com's banners break if player count isn't the 8th field of the reply, so... this has to go here.
s["players"] = 0
s["stationtime"] = worldtime2text()
s["roundduration"] = round_duration()
if(input["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"] = list2params(players)
s["admins"] = admins.len
s["adminlist"] = list2params(admins)
var/unauthed = do_auth_check(addr,auth,query)
if (unauthed)
if (unauthed == 3)
log_debug("API: Request denied - Auth Service Unavailable")
response["statuscode"] = 503
response["response"] = "Auth Service Unavailable"
return json_encode(response)
else if (unauthed == 2)
log_debug("API: Request denied - Throttled")
response["statuscode"] = 429
response["response"] = "Throttled"
return json_encode(response)
else
var/n = 0
var/admins = 0
log_debug("API: Request denied - Bad Auth")
response["statuscode"] = 401
response["response"] = "Bad Auth"
return json_encode(response)
log_debug("API: Auth valid")
var/datum/topic_command/command = topic_commands[query]
if (isnull(command))
log_debug("API: Unknown command called: [query]")
response["statuscode"] = 501
response["response"] = "Not Implemented"
return json_encode(response)
if(command.check_params_missing(queryparams))
log_debug("API: Mising Params - Status: [command.statuscode] - Response: [command.response]")
response["statuscode"] = command.statuscode
response["response"] = command.response
response["data"] = command.data
return json_encode(response)
else
command.run_command(queryparams)
log_debug("API: Function called: [query] - Status: [command.statuscode] - Response: [command.response]")
response["statuscode"] = command.statuscode
response["response"] = command.response
response["data"] = command.data
return json_encode(response)
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
return list2params(s)
else if(T == "manifest")
if (!ticker)
return "Game not started yet!"
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"
return list2params(positions)
else if(copytext(T,1,5) == "mute")
var/input[] = params2list(T)
var/bad_key = do_topic_spam_protection(addr, input["key"])
if (bad_key)
return bad_key
for (var/client/C in clients)
if (C.ckey == ckey(input["mute"]))
C.mute_discord = !C.mute_discord
switch (C.mute_discord)
if (1)
C << "<b><font color='red'>You have been muted from replying to Discord PMs by [input["admin"]]!</font></b>"
log_and_message_admins("[C] has been muted from Discord PMs by [input["admin"]].")
return "[C.key] is now muted from replying to Discord PMs."
if (0)
C << "<b><font color='red'>You have been unmuted from replying to Discord PMs by [input["admin"]]!</font></b>"
log_and_message_admins("[C] has been unmuted from Discord PMs by [input["admin"]].")
return "[C.key] is now unmuted from replying to Discord PMs."
return "I couldn't find that ckey!"
else if(copytext(T,1,5) == "info")
var/input[] = params2list(T)
var/bad_key = do_topic_spam_protection(addr, input["key"])
if (bad_key)
return bad_key
var/list/search = params2list(input["info"])
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)
return "No matches"
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
return list2params(info)
else
return "Multiple matches found!"
else if(copytext(T,1,9) == "adminmsg")
/*
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.
*/
var/input[] = params2list(T)
var/bad_key = do_topic_spam_protection(addr, input["key"])
if (bad_key)
return bad_key
var/client/C
var/req_ckey = ckey(input["adminmsg"])
for(var/client/K in clients)
if(K.ckey == req_ckey)
C = K
break
if(!C)
return "No client with that name on server"
var/rank = input["rank"]
if(!rank)
rank = "Admin"
var/message = "<font color='red'>Discord-[rank] PM from <b><a href='?discord_msg=[input["sender"]]'>Discord-[input["sender"]]</a></b>: [input["msg"]]</font>"
var/amessage = "<font color='blue'>Discord-[rank] PM from <a href='?discord_msg=[input["sender"]]'>Discord-[input["sender"]]</a> to <b>[key_name(C)]</b> : [input["msg"]]</font>"
C.received_discord_pm = world.time
C.discord_admin = input["sender"]
C << 'sound/effects/adminhelp.ogg'
C << message
for(var/client/A in admins)
if(A != C)
A << amessage
return "Message Successful"
else if(copytext(T,1,6) == "notes")
var/input[] = params2list(T)
var/bad_key = do_topic_spam_protection(addr, input["key"])
if (bad_key)
return bad_key
return show_player_info_discord(ckey(input["notes"]))
else if(copytext(T,1,4) == "age")
var/input[] = params2list(T)
var/bad_key = do_topic_spam_protection(addr, input["key"])
if (bad_key)
return bad_key
var/age = get_player_age(input["age"])
if(isnum(age))
if(age >= 0)
return "[age]"
else
return "Ckey not found"
else
return "Database connection failed or not set up"
else if (copytext(T, 1, 8) == "restart")
var/input[] = params2list(T)
var/bad_key = do_topic_spam_protection(addr, input["key"])
if (bad_key)
log_and_message_admins("Remote restart attempted and stopped. Dumping topic call data.")
log_and_message_admins("TOPIC: \"[T]\", from: [addr], master: [master], key: [key].")
return bad_key
world << "<font size=4 color='#ff2222'>Server restarting by remote command.</font>"
log_and_message_admins("World restart initiated remotely by [input["restart"]].")
feedback_set_details("end_error","remote restart")
if (blackbox)
blackbox.save_all_data_to_sql()
sleep(50)
log_game("Rebooting due to remote command.")
world.Reboot(2)
return "Server successfully restarted."
else if (copytext(T, 1, 9) == "announce")
var/input[] = params2list(T)
var/bad_key = do_topic_spam_protection(addr, input["key"])
if (bad_key)
return bad_key
var/message = replacetext(input["msg"], "\n", "<br>")
world << "<span class=notice><b>[input["announce"] ? input["announce"] : "Administrator"] Announces via Discord:</b><p style='text-indent: 50px'>[message]</p></span>"
log_and_message_admins("[input["announce"]] announced remotely: [input["msg"]].")
return "Announcement successfully sent."
else if (copytext(T, 1, 8) == "faxlist")
var/input[] = params2list(T)
var/bad_key = do_topic_spam_protection(addr, input["key"])
if (bad_key)
return bad_key
if (!ticker)
return "Round hasn't started yet! No faxes to display!"
var/list/faxes = list()
switch (input["faxlist"])
if ("received")
faxes = arrived_faxes
if ("sent")
faxes = sent_faxes
if (!faxes || !faxes.len)
return "No faxes found!"
var/list/output = list()
for (var/i = 1, i <= faxes.len, i++)
var/obj/item/a = faxes[i]
output += "ID [i]"
output["ID [i]"] = a.name ? a.name : "Untitled Fax"
return list2params(output)
else if (copytext(T, 1, 7) == "getfax")
var/input[] = params2list(T)
var/bad_key = do_topic_spam_protection(addr, input["key"])
if (bad_key)
return bad_key
var/list/faxes = list()
switch (input["received"])
if ("received")
faxes = arrived_faxes
if ("sent")
faxes = sent_faxes
if (!faxes || !faxes.len)
return "No faxes found!"
var/fax_id = text2num(input["getfax"])
if (fax_id > faxes.len || fax_id < 1)
return "Invalid fax ID!"
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, "<br>", "\n")
content = strip_html_properly(content, 0)
output["content"] = content
return list2params(output)
else if (istype(faxes[fax_id], /obj/item/weapon/photo))
return "The fax is a photo. I cannot send images, unfortunately..."
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)
return "The bundle was empty! How is that even possible?"
var/i = 0
for (var/obj/item/weapon/paper/c in b.pages)
i++
var/content = replacetext(c.info, "<br>", "\n")
content = strip_html_properly(content, 0)
output["content"] += "Page [i]:\n[content]\n\n"
return list2params(output)
return "Unable to recognize the fax type. Cannot send contents!"
/world/Reboot(var/reason)
/*spawn(0)
@@ -754,20 +405,41 @@ var/world_topic_spam_protect_time = world.timeofday
#undef FAILED_DB_CONNECTION_CUTOFF
/world/proc/do_topic_spam_protection(var/addr, var/key)
if (!config.comms_password || config.comms_password == "")
return "No comms password configured, aborting."
/world/proc/do_auth_check(var/addr, var/auth, var/function)
//Check if rate limited
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
if (key == config.comms_password)
return 0
world_api_rate_limit[addr] = world.time // Set the time of the last request
//Then query for auth
if (!establish_db_connection(dbcon))
return 3 //DB Unavailable
var/DBQuery/authquery = dbcon.NewQuery({"SELECT *
FROM ss13_api_token_function as api_t_f, ss13_api_tokens as api_t, ss13_api_functions as api_f
WHERE api_t.id = api_t_f.token_id AND api_f.id = api_t_f.function_id
AND (
(token = :token AND ip = :ip AND function = :function)
OR
(token = :token AND ip IS NULL AND function = :function)
OR
(token = :token AND ip = :ip AND function IS NULL)
OR
(token IS NULL AND ip = :ip AND function IS NULL)
)"})
//Get the tokens and the associated functions
//Check if the token, the ip and the function matches OR
// the token + function matches and the ip is NULL (Functions that can be used by any ip, but require a token)
// the token + ip matches and the function is NULL (Allow a specific ip with a specific token to use all functions)
// the token + ip is NULL and the function matches (Allow a specific function to be used without auth)
authquery.Execute(list(":token" = auth, ":ip" = addr, ":function" = function))
log_debug("API: Auth Check - Query Executed - Returned Rows: [authquery.RowCount()]")
if (authquery.RowCount())
return 0 // Authed
else
if (world_topic_spam_protect_ip == addr && abs(world_topic_spam_protect_time - world.time) < 50)
spawn(50)
world_topic_spam_protect_time = world.time
return "Bad Key (Throttled)"
world_topic_spam_protect_time = world.time
world_topic_spam_protect_ip = addr
return "Bad Key"
return 1 // Bad Key

View File

@@ -397,3 +397,11 @@ STARLIGHT 0
## Uncomment to enable the antag contest!
#ANTAG_CONTEST_ENABLED
## API Rate Limit in ds
API_RATE_LIMIT 50
## API Rate Limit IP Whitelist
## IPs that should not be throttled by the API RATE Limiter
## IPs are separated by ;
API_RATE_LIMIT_WHITELIST 127.0.0.1

View File

@@ -0,0 +1,36 @@
################################
# Example Changelog File
#
# Note: This file, and files beginning with ".", and files that don't end in ".yml" will not be read. If you change this file, you will look really dumb.
#
# Your changelog will be merged with a master changelog. (New stuff added only, and only on the date entry for the day it was merged.)
# When it is, any changes listed below will disappear.
#
# Valid Prefixes:
# bugfix
# wip (For works in progress)
# tweak
# soundadd
# sounddel
# rscadd (general adding of nice things)
# rscdel (general deleting of nice things)
# imageadd
# imagedel
# maptweak
# spellcheck (typo fixes)
# experiment
#################################
# Your name.
author: Arrow768
# Optional: Remove this file after generating master changelog. Useful for PR changelogs that won't get used again.
delete-after: True
# Any changes you've made. See valid prefix list above.
# INDENT WITH TWO SPACES. NOT TABS. SPACES.
# SCREW THIS UP AND IT WON'T WORK.
# Also, all entries are changed into a single [] after a master changelog generation. Just remove the brackets when you add new entries.
# Please surround your changes in double quotes ("), as certain characters otherwise screws up compiling. The quotes will not show up in the changelog.
changes:
- rscadd: "Rewrite of the API - This enables more advanced features in the webpanel"