Files
2020-02-19 19:48:29 -08:00

420 lines
13 KiB
Plaintext

/*********************************
For the main html chat area
*********************************/
#define CTX_PM 1
#define CTX_SMSG 2
#define CTX_BOOT 4
#define CTX_BAN 8
#define CTX_GIB 16
#define CTX_POPT 32
#define CTX_JUMP 64
#define CTX_GET 128
#define CTX_OBSERVE 256
#define CTX_GHOSTJUMP 512
//Precaching a bunch of shit
var/global
savefile/iconCache = new /savefile("data/iconCache.sav") //Cache of icons for the browser output
cFlagsShitguy = CTX_GIB | CTX_GET
cFlagsSa = CTX_BAN | CTX_POPT | CTX_JUMP
cFlagsMod = CTX_SMSG | CTX_BOOT | CTX_PM
cFlagsDead = CTX_OBSERVE | CTX_GHOSTJUMP
// Why is this defined this way you ask?
// It's because if you define an associative list mapping constants inside strings
// like "[LEVEL_MOD]" = FOOBAR_SHITFUCK_FUCKFACE
// The byond object tree output gets completely fucked in the ass and generates
// broken xml
list/contextFlags = list(1 = 0,2 = cFlagsMod,3 = cFlagsSa,4 = 0,5 = 0,6 = cFlagsShitguy,7 = 0,8 = 0)
/*
8 = LEVEL_HOST
7 = LEVEL_CODER
6 = LEVEL_SHITGUY
5 = LEVEL_PA
4 = LEVEL_ADMIN
3 = LEVEL_SA
2 = LEVEL_MOD
1 = LEVEL_BABBY
*/
//On client, created on login
/datum/chatOutput
var
client/owner = null //client ref
loaded = 0 //Has the client loaded the browser output area?
loadAttempts = 0 //How many times has the client tried to load the output area?
list/messageQueue = list() //If they haven't loaded chat, this is where messages will go until they do
ctxFlag = 0 //Context menu flags for the admin powers
cookieSent = 0 //Has the client sent a cookie for analysis
list/connectionHistory = list() //Contains the connection history passed from chat cookie
New(client/C)
..()
if (C)
src.owner = C
return 1
proc
start()
//Check for existing chat
if (!src.owner) return 0
if (winget(src.owner, "browseroutput", "is-disabled") == "false") //Already setup
src.doneLoading()
else //Not setup
src.load()
return 1
load()
if (src.owner)
//For local-testing fallback
if (!cdn)
var/list/chatResources = list(
"browserassets/js/jquery.min.js",
"browserassets/js/errorHandler.js",
//"browserassets/js/array.generics.min.js",
//"browserassets/js/anchorme.js",
"browserassets/js/browserOutput.js",
"browserassets/css/fonts/fontawesome-webfont.eot",
"browserassets/css/fonts/fontawesome-webfont.svg",
"browserassets/css/fonts/fontawesome-webfont.ttf",
"browserassets/css/fonts/fontawesome-webfont.woff",
"browserassets/css/font-awesome.css",
"browserassets/css/browserOutput.css"
)
src.owner.loadResourcesFromList(chatResources)
src.owner << browse(grabResource("html/browserOutput.html"), "window=browseroutput")
if (src.loadAttempts < 5) //To a max of 5 load attempts
SPAWN_DBG(20 SECONDS) //20 seconds
if (src.owner && !src.loaded)
src.loadAttempts++
src.load()
else
//Exceeded. Maybe do something extra here
return
else
//Client managed to logoff or otherwise get deleted
return
//Called on chat output done-loading by JS.
doneLoading(ua)
if (src.owner && !src.loaded)
src.loaded = 1
winset(src.owner, "browseroutput", "is-disabled=false")
//if (src.owner.holder)
src.loadAdmin()
if (src.messageQueue)
for (var/x = 0, x < src.messageQueue.len, x++)
boutput(src.owner, src.messageQueue["[x]"]["message"], src.messageQueue["[x]"]["group"])
src.messageQueue = null
if (ua)
//For persistent user tracking
apiHandler.queryAPI("versions/add", list(
"ckey" = src.owner.ckey,
"userAgent" = ua,
"byondMajor" = src.owner.byond_version,
"byondMinor" = src.owner.byond_build
))
else
src.sendClientData()
/* WIRE TODO: Fix this so the CDN dying doesn't break everyone
SPAWN_DBG(1 MINUTE) //60 seconds
if (!src.cookieSent) //Client has very likely futzed with their local html/js chat file
out(src.owner, "<div class='fatalError'>Chat file tampering detected. Closing connection.</div>")
del(src.owner)
*/
//Called in update_admins()
loadAdmin()
var/data = json_encode(list("loadAdminCode" = replacetext(replacetext(grabResource("html/adminOutput.html"), "\n", ""), "\t", "")))
ehjax.send(src.owner, "browseroutput", url_encode(data))
//Sends client connection details to the chat to handle and save
sendClientData()
//Fix for Cannot read null.ckey (how!?)
if (!src.owner) return
//Get dem deets
var/list/deets = list("clientData" = list())
deets["clientData"]["ckey"] = src.owner.ckey
deets["clientData"]["ip"] = src.owner.address
deets["clientData"]["compid"] = src.owner.computer_id
var/data = json_encode(deets)
ehjax.send(src.owner, "browseroutput", data)
//Called by client, sent data to investigate (cookie history so far)
analyzeClientData(cookie = "")
if (!cookie) return
if (cookie != "none")
var/list/connData = json_decode(cookie)
if (connData && islist(connData) && connData.len > 0 && connData["connData"])
src.connectionHistory = connData["connData"] //lol fuck
var/list/found = new()
for (var/i = src.connectionHistory.len; i >= 1; i--)
var/list/row = src.connectionHistory[i]
if (!row || row.len < 3 || (!row["ckey"] && !row["compid"] && !row["ip"])) //Passed malformed history object
return
if (checkBan(row["ckey"], row["compid"], row["ip"]))
found = row
break
//Uh oh this fucker has a history of playing on a banned account!!
if (found.len > 0)
//TODO: add a new evasion ban for the CURRENT client details, using the matched row details
message_admins("[key_name(src.owner)] has a cookie from a banned account! (Matched: [found["ckey"]], [found["ip"]], [found["compid"]])")
logTheThing("debug", src.owner, null, "has a cookie from a banned account! (Matched: [found["ckey"]], [found["ip"]], [found["compid"]])")
logTheThing("diary", src.owner, null, "has a cookie from a banned account! (Matched: [found["ckey"]], [found["ip"]], [found["compid"]])", "debug")
//Irc message too
if(owner)
var/ircmsg[] = new()
ircmsg["key"] = owner.key
ircmsg["name"] = owner.mob.name
ircmsg["msg"] = "has a cookie from banned account [found["ckey"]](IP: [found["ip"]], CompID: [found["compID"]])"
ircbot.export("admin", ircmsg)
src.cookieSent = 1
getContextFlags()
var/ret = src.ctxFlag
if(src.owner && istype( src.owner.mob, /mob/dead/observer ))
ret |= cFlagsDead
return ret
//Called in New() (/datum/admins)
getContextFlag()
if (!src.owner.holder) return
var/level = src.owner.holder.level
for (var/x = level; x >= -1 ; x--) //-1 is the lowest rank
var/rankFlags = contextFlags[x+2] // X + 2 because fuck byond. See definition of contextflags at the top of this file.
if (rankFlags)
src.ctxFlag |= rankFlags
//Called by js client on admin command via context menu
handleContextMenu(command, target)
if (!src.owner.holder && command != "observe" && command != "teleport") return // oopsy i'm so messy heehee
var/datum/mind/targetMind = locate(target)
var/mob/targetMob
if (targetMind)
targetMob = targetMind.current
else //The mind no longer exists? What? How?!
return
switch(command)
if ("pm")
src.owner.cmd_admin_pm(targetMob)
if ("smsg")
src.owner.cmd_admin_subtle_message(targetMob)
if ("jump")
if (!istype(targetMob, /mob/dead/target_observer))
src.owner.jumptomob(targetMob)
else
var/jumptarget = targetMob.eye
if (jumptarget)
src.owner.jumptoturf(get_turf(jumptarget))
if ("get")
src.owner.Getmob(targetMob)
if ("boot")
src.owner.cmd_boot(targetMob)
if ("ban")
src.owner.addBanDialog(targetMob)
if ("gib")
src.owner.cmd_admin_gib(targetMob)
logTheThing("admin", src.owner, targetMob, "gibbed %target%.")
if ("popt")
if(src.owner.holder)
src.owner.holder.playeropt(targetMob)
if ("observe")
if (istype(src.owner.mob, /mob/dead/target_observer))
src.owner.mob:set_observe_target(targetMob)
if(istype(src.owner.mob, /mob/dead/observer))
src.owner.mob:insert_observer(targetMob)
if ("teleport")
if (istype(src.owner.mob, /mob/dead/target_observer))
src.owner.mob:stop_observing()
if(istype(src.owner.mob, /mob/dead/observer))
src.owner.mob.set_loc(get_turf(targetMob))
//todo
changeChatMode(mode)
if (!mode) return
var/data = json_encode(list("modeChange" = mode))
data = url_encode(data)
for (var/client/C in clients)
ehjax.send(C, "browseroutput", data)
playMusic(url, volume)
if (!url || !volume) return
var/data = json_encode(list("playMusic" = url, "volume" = volume / 100))
data = url_encode(data)
ehjax.send(src.owner, "browseroutput", data)
playDectalk(url, trigger, volume)
if (!url || !volume) return
var/data = json_encode(list("dectalk" = url, "decTalkTrigger" = trigger, "volume" = volume / 100))
data = url_encode(data)
ehjax.send(src.owner, "browseroutput", data)
adjustVolumeRaw(volume)
var/data = json_encode(list("adjustVolume" = volume))
data = url_encode(data)
ehjax.send(src.owner, "browseroutput", data)
adjustVolume(volume)
var/data = json_encode(list("adjustVolume" = volume / 100))
data = url_encode(data)
ehjax.send(src.owner, "browseroutput", data)
//Called by js client every 60 seconds
ping()
return "pong"
//Global chat procs
//Converts an icon to base64. Operates by putting the icon in the iconCache savefile,
// exporting it as text, and then parsing the base64 from that.
// (This relies on byond automatically storing icons in savefiles as base64)
/proc/icon2base64(icon, iconKey = "misc")
if (!isicon(icon)) return 0
iconCache[iconKey] << icon
var/iconData = iconCache.ExportText(iconKey)
var/list/partial = splittext(iconData, "{")
return copytext(partial[2], 3, -5)
/proc/bicon(obj)
if (ispath(obj))
obj = new obj()
var/baseData
if (isicon(obj))
baseData = icon2base64(obj)
return "<img style=\"position: relative; left: -1px; bottom: -3px;\" class=\"icon misc\" src=\"data:image/png;base64,[baseData]\" />"
if (obj && obj:icon)
//Hash the darn dmi path and state
var/iconKey = md5("[obj:icon][obj:icon_state]")
var/iconData
//See if key already exists in savefile
iconData = iconCache.ExportText(iconKey)
if (iconData)
//It does! Ok, parse out the base64
var/list/partial = splittext(iconData, "{")
if (length(partial) < 2)
logTheThing("debug", null, null, "Got invalid savefile data for: [obj]")
return
baseData = copytext(partial[2], 3, -5)
else
//It doesn't exist! Create the icon
var/icon/icon = icon(file(obj:icon), obj:icon_state, SOUTH, 1)
if (!icon)
logTheThing("debug", null, null, "Unable to create output icon for: [obj]")
return
baseData = icon2base64(icon, iconKey)
return "<img style=\"position: relative; left: -1px; bottom: -3px;\" class=\"icon [obj:icon_state]\" src=\"data:image/png;base64,[baseData]\" />"
//Aliases for bicon
/proc/bi(obj)
bicon(obj)
/proc/boutput(target = 0, message = "", group = "")
if (target == world)
for (var/client/C in clients)
boutput(C, message)
return
//If the target is a list, attempt to send the message to each item in the list
//(it's up to the caller to ensure the list contains actual things we can send to)
if (islist(target))
for (var/T in target)
boutput(T, message)
return
//Ok so I did my best but I accept that some calls to this will be for shit like sound and images
//It stands that we PROBABLY don't want to output those to the browser output so just handle them here
if (istype(message, /image) || istype(message, /sound) || istype(target, /savefile))
target << message
CRASH("DEBUG: Boutput called with invalid message")
return
//Otherwise, we're good to throw it at the user
else if (istext(message))
if (istext(target)) return
//Some macros remain in the string even after parsing and fuck up the eventual output
message = stripTextMacros(message)
//Grab us a client if possible
var/client/C
if (isclient(target))
C = target
else if (ismob(target))
C = target:client
else if (ismind(target) && target:current)
C = target:current:client
if (C && C.chatOutput && !C.chatOutput.loaded && C.chatOutput.messageQueue && islist(C.chatOutput.messageQueue))
//Client sucks at loading things, put their messages in a queue
C.chatOutput.messageQueue["[C.chatOutput.messageQueue.len]"] = list("message" = message, "group" = group)
else
target << output(list2params(list(
message,
group
)), "browseroutput:output")
//Aliases for boutput
/proc/bout(target = 0, message = "", group = "")
boutput(target, message, group)
/proc/out(target = 0, message = "", group = "")
boutput(target, message, group)
/proc/bo(target = 0, message = "", group = "")
boutput(target, message, group)
/*
I spent so long on this regex I don't want to get rid of it :(
if (findtext(message, "<IMG CLASS=ICON"))
var/regex/R = new("/<IMG CLASS=icon SRC=(\\\[.*?\\\]) ICONSTATE='(.*?)'>/\[insertIconImg($1,$2)\]/e")
//if (R.Find(message))
var/newtxt = R.Replace(message)
while(newtxt)
message = newtxt
newtxt = R.ReplaceNext(message)
world.log << html_encode(message)
*/
/*
/client/verb/reloadChat()
set name = "Reload Chat"
del(src.chatOutput)
winset(src, "browseroutput", "is-disabled=true")
src.chatOutput = new /datum/chatOutput(src)
src.chatOutput.start()
out(src, "Reloaded chat")
*/