/********************************* For the main html chat area *********************************/ /var/list/chatResources = list( "code/modules/html_interface/jquery.min.js", "goon/browserassets/js/json2.min.js", "goon/browserassets/js/browserOutput.js", "goon/browserassets/css/fonts/fontawesome-webfont.eot", "goon/browserassets/css/fonts/fontawesome-webfont.svg", "goon/browserassets/css/fonts/fontawesome-webfont.ttf", "goon/browserassets/css/fonts/fontawesome-webfont.woff", "goon/browserassets/css/font-awesome.css", "goon/browserassets/css/browserOutput.css", "goon/browserassets/css/browserOutput_dark.css", "goon/browserassets/css/browserOutput_colorblindv1.css" ) //Precaching a bunch of shit /var/savefile/iconCache = new /savefile("data/iconCache.sav") //Cache of icons for the browser output //On client, created on login /datum/chatOutput var/client/owner = null //client ref var/loaded = 0 // Has the client loaded the browser output area? var/list/messageQueue = list() //If they haven't loaded chat, this is where messages will go until they do var/cookieSent = 0 // Has the client sent a cookie for analysis var/list/connectionHistory = list() //Contains the connection history passed from chat cookie var/broken = FALSE /datum/chatOutput/New(client/C) . = ..() owner = C // world.log << "chatOutput: New()" /datum/chatOutput/proc/start() //Check for existing chat if(!owner) return 0 if(!winexists(owner, "browseroutput")) // Oh goddamnit. alert(owner.mob, "Updated chat window does not exist. If you are using a custom skin file please allow the game to update.") broken = TRUE return 0 if(winget(owner, "browseroutput", "is-disabled") == "false") //Already setup doneLoading() else //Not setup load() return 1 /datum/chatOutput/proc/load() set waitfor = FALSE if(!owner) return // world.log << "chatOutput: load()" for(var/attempts = 1 to 5) for(var/asset in global.chatResources) // No asset cache, just get this fucking shit SENT. owner << browse_rsc(file(asset)) // world.log << "Sending main chat window to client [owner.ckey]" owner << browse(file("goon/browserassets/html/browserOutput.html"), "window=browseroutput") sleep(20 SECONDS) if(!owner || loaded) break // world.log << "chatOutput: load() completed" /datum/chatOutput/Topic(var/href, var/list/href_list) if(usr.client != owner) return 1 // Build arguments. // Arguments are in the form "param[paramname]=thing" var/list/params = list() for(var/key in href_list) if(length(key) > 7 && findtext(key, "param")) // 7 is the amount of characters in the basic param key template. var/param_name = copytext(key, 7, -1) var/item = href_list[key] params[param_name] = item var/data // Data to be sent back to the chat. switch(href_list["proc"]) if("doneLoading") data = doneLoading(arglist(params)) if("debug") data = debug(arglist(params)) if("ping") data = ping(arglist(params)) if("analyzeClientData") data = analyzeClientData(arglist(params)) if("encoding") var/encoding = href_list["encoding"] var/static/regex/RE = regex("windows-(874|125\[0-8])") if (RE.Find(encoding)) owner.encoding = RE.group[1] else if (encoding == "gb2312") owner.encoding = "2312" // This seems to be the result on Japanese locales, but the client still seems to accept 1252. else if (encoding == "_autodetect") owner.encoding = "1252" // This is the result of that Windows 10 beta thing where Microsoft has UTF-8 as a code page. // It doesn't work in BYOND and *seems* to fall back to 1252? else if (encoding == "utf-8") owner.encoding = "1252" else stack_trace("Unknown encoding received from client: \"[sanitize(encoding)]\". Please report this as a bug.") if("colorPresetPost") //User just swapped color presets in their goonchat preferences. Do we do anything else? switch(href_list["preset"]) if("normal","colorblindv1") owner.white_theme() if("dark") owner.dark_theme() if(data) ehjax_send(data = data) //Called on chat output done-loading by JS. /datum/chatOutput/proc/doneLoading() if(loaded) return loaded = TRUE winset(owner, "browseroutput", "is-disabled=false") for(var/message in messageQueue) to_chat(owner, message) messageQueue = null src.sendClientData() pingLoop() /datum/chatOutput/proc/pingLoop() set waitfor = FALSE usr = null //Otherwise this holds a reference to the new_player and causes it to hard del while (owner) ehjax_send(data = owner.is_afk(29 SECONDS) ? "softPang" : "pang") // SoftPang isn't handled anywhere but it'll always reset the opts.lastPang. sleep(30 SECONDS) /datum/chatOutput/proc/ehjax_send(var/client/C = owner, var/window = "browseroutput", var/data) if(islist(data)) data = json_encode(data) C << output("[data]", "[window]:ehjaxCallback") //Sends client connection details to the chat to handle and save /datum/chatOutput/proc/sendClientData() //Get dem deets var/list/deets = list("clientData" = list()) deets["clientData"]["ckey"] = owner.ckey deets["clientData"]["ip"] = owner.address deets["clientData"]["compid"] = owner.computer_id var/data = list2json(deets) ehjax_send(data = data) //Called by client, sent data to investigate (cookie history so far) /datum/chatOutput/proc/analyzeClientData(cookie = "") if(!cookie) return if(cookie != "none") var/list/connData = json2list(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 (world.IsBanned(row["ckey"], row["ip"], row["compid"], "goonchat")) found = row break //Uh oh this fucker has a history of playing on a banned account!! if (found.len > 0) if(owner.ckey == found["ckey"]) //If the banned ckey is the same as the one that successfully connected, ignore it. This happens when someone tries to join as a guest account at some point. cookieSent = 1 return //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"]])") log_admin("[key_name(src.owner)] has a cookie from a banned account! (Matched: [found["ckey"]], [found["ip"]], [found["compid"]])") var/admins_number = admins.len var/admin_number_afk = get_afk_admins() var/available_admins = admins_number - admin_number_afk //Probably not a good idea to print IP and CID in these channels send2adminirc("[key_name(src.owner)] has a cookie from a banned account! (Matched: [found["ckey"]]) [available_admins ? "" : "No non-AFK admins online"]") send2admindiscord("**[key_name(src.owner)] has a cookie from a banned account! (Matched: [found["ckey"]]) [available_admins ? "" : "No non-AFK admins online"]**", !available_admins) cookieSent = 1 //Called by js client every 60 seconds /datum/chatOutput/proc/ping() return "pong" //Called by js client on js error /datum/chatOutput/proc/debug(error) world.log <<"\[[time2text(world.realtime, "YYYY-MM-DD hh:mm:ss")]\] Client: [(src.owner.key ? src.owner.key : src.owner)] triggered JS error: [error]" /client/verb/debug_chat() set hidden = 1 chatOutput.ehjax_send(data = list("firebug" = 1)) //Global chat procs /var/list/bicon_cache = list() //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(var/icon/icon, var/iconKey = "misc") if (!isicon(icon)) return 0 iconCache[iconKey] << icon var/iconData = iconCache.ExportText(iconKey) var/list/partial = splittext(iconData, "{") return replacetext(copytext(partial[2], 3, -5), "\n", "") //same as above but only saves the South icon_state, used to display the players on last round's scoreboard. /proc/iconsouth2base64(var/icon/icon, var/iconKey = "misc") if (!isicon(icon)) return 0 iconCache[iconKey] << icon(icon, dir = SOUTH, frame = 1) var/iconData = iconCache.ExportText(iconKey) var/list/partial = splittext(iconData, "{") var/list/partial2 = splittext(partial[2], "}") return replacetext(copytext(partial2[1], 2, -1), "\n", "") /proc/bicon(var/obj) if (!obj) return if (isicon(obj)) //Icons get pooled constantly, references are no good here. /*if (!bicon_cache["\ref[obj]"]) // Doesn't exist yet, make it. bicon_cache["\ref[obj]"] = icon2base64(obj) return ""*/ return "" // Either an atom or somebody fucked up and is gonna get a runtime, which I'm fine with. var/atom/A = obj var/key = ("[A.icon]" || "\ref[A.icon]")+":[A.icon_state]" if (!bicon_cache[key]) // Doesn't exist, make it. var/icon/I = icon(A.icon, A.icon_state, SOUTH, 1, 0) if (!"[A.icon]") // Shitty workaround for a BYOND issue. var/icon/temp = I I = icon() I.Insert(temp, dir = SOUTH) bicon_cache[key] = icon2base64(I, key) return "" //Aliases for bicon /proc/bi(obj) bicon(obj) //Costlier version of bicon() that uses getFlatIcon() to account for overlays, underlays, etc. Use with extreme moderation, ESPECIALLY on mobs. /proc/costly_bicon(var/obj) if (!obj) return if (isicon(obj)) return bicon(obj) var/icon/I = getFlatIconDeluxe(sort_image_datas(get_content_image_datas(obj))) return bicon(I) /proc/to_chat(target, message) //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(!target) return if (istype(target, /datum/zLevel)) //Passing it to an entire z level for(var/mob/M in player_list) //List of all mobs with clients, excluding new_player if(!istype(get_z_level(M),target)) continue to_chat(M, message) return if (istype(message, /image) || istype(message, /sound) || istype(target, /savefile) || !(ismob(target) || islist(target) || isclient(target) || istype(target, /datum/log) || target == world)) target << message if (!isatom(target)) // Really easy to mix these up, and not having to make sure things are mobs makes the code cleaner. 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 if (findtext(message, "\improper")) message = replacetext(message, "\improper", "") if (findtext(message, "\proper")) message = replacetext(message, "\proper", "") //Grab us a client if possible var/client/C if (istype(target, /client)) C = target else if (istype(target, /mob)) C = target:client else if (istype(target, /datum/mind) && target:current) C = target:current:client if (C && C.chatOutput) if(C.chatOutput.broken) // Either a secret repo fuck up or a player who hasn't updated his skin file. C << message return if(!C.chatOutput.loaded && C.chatOutput.messageQueue && islist(C.chatOutput.messageQueue)) //Client sucks at loading things, put their messages in a queue C.chatOutput.messageQueue.Add(message) return if(istype(target, /datum/log)) var/datum/log/L = target L.log += (message + "\n") return message = replacetext(message, "\n", "
") target << output(url_encode(message), "browseroutput:output") /proc/get_deadchat_hearers() . = list() for(var/mob/M in player_list) if(!M.client) continue if(istype(M, /mob/new_player)) continue else if(M.client.prefs.get_pref(/datum/preference_setting/binary_flag/toggles) & CHAT_DEAD) if(M.client.holder && M.client.holder.rights & R_ADMIN) //admins can toggle deadchat on and off. This is a proc in admin.dm and is only give to Administrators and above . += M else if(M.stat == DEAD && !istype(M, /mob/dead/observer/deafmute)) . += M else if(istype(M,/mob/living/carbon/brain)) var/mob/living/carbon/brain/B = M if(B.brain_dead_chat()) . += M /proc/formatFollow(var/mob/target,var/custom_text="(Follow)") return "[custom_text]" /proc/formatGhostJump(var/mob/target,var/custom_text="Teleport") return "[custom_text]" /* This proc only handles sending the message to everyone who can hear deadchat. Formatting that message is up to you! Consider using on your message! */ /* Kinda useless if your message needs to include an href, though... */ /proc/to_deadchat(message) var/list/hearers = get_deadchat_hearers() if(!hearers || !message) return for(var/mob/M in hearers) to_chat(M, message) log_game("DEADCHAT: [message]") return 1 /datum/log //exists purely to capture to_chat() output var/log = ""