mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 18:53:06 +00:00
The for loop that used to do it loaded the wrong messages. New method calculates correctly and also doesn't spend time loading from database if we're going to throw it away.
421 lines
13 KiB
Plaintext
421 lines
13 KiB
Plaintext
//The 'V' is for 'VORE' but you can pretend it's for Vue.js if you really want.
|
|
|
|
//These are sent to the client via browse_rsc() in advance so the HTML can access them.
|
|
GLOBAL_LIST_INIT(vchatFiles, list(
|
|
"code/modules/vchat/css/vchat-font-embedded.css",
|
|
"code/modules/vchat/css/semantic.min.css",
|
|
"code/modules/vchat/css/ss13styles.css",
|
|
"code/modules/vchat/js/polyfills.js",
|
|
"code/modules/vchat/js/vue.min.js",
|
|
"code/modules/vchat/js/vchat.js"
|
|
))
|
|
|
|
// The to_chat() macro calls this proc
|
|
/proc/__to_chat(var/target, var/message)
|
|
// First do logging in database
|
|
if(isclient(target))
|
|
var/client/C = target
|
|
vchat_add_message(C.ckey, message)
|
|
else if(ismob(target))
|
|
var/mob/M = target
|
|
if(M.ckey)
|
|
vchat_add_message(M.ckey, message)
|
|
else if(target == world)
|
|
for(var/client/C in GLOB.clients)
|
|
if(!QDESTROYING(C)) // Might be necessary?
|
|
vchat_add_message(C.ckey, message)
|
|
|
|
// Now lets either queue it for sending, or send it right now
|
|
if(Master.current_runlevel == RUNLEVEL_INIT || !SSchat?.subsystem_initialized)
|
|
to_chat_immediate(target, world.time, message)
|
|
else
|
|
SSchat.queue(target, world.time, message)
|
|
|
|
//This is used to convert icons to base64 <image> strings, because byond stores icons in base64 in savefiles.
|
|
GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of icons for the browser output
|
|
|
|
//The main object attached to clients, created when they connect, and has start() called on it in client/New()
|
|
/datum/chatOutput
|
|
var/client/owner = null
|
|
var/loaded = FALSE
|
|
var/list/message_queue = list()
|
|
var/broken = FALSE
|
|
var/resources_sent = FALSE
|
|
var/message_buffer = 200 // Number of messages being actively shown to the user, used to play back that many messages on reconnect
|
|
|
|
var/last_topic_time = 0
|
|
var/too_many_topics = 0
|
|
var/topic_spam_limit = 10 //Just enough to get over the startup and such
|
|
|
|
/datum/chatOutput/New(client/C)
|
|
. = ..()
|
|
|
|
owner = C
|
|
|
|
/datum/chatOutput/Destroy()
|
|
owner = null
|
|
. = ..()
|
|
|
|
/datum/chatOutput/proc/update_vis()
|
|
if(!loaded && !broken)
|
|
winset(owner, null, "outputwindow.htmloutput.is-visible=false;outputwindow.oldoutput.is-visible=false;outputwindow.chatloadlabel.is-visible=true")
|
|
else if(broken)
|
|
winset(owner, null, "outputwindow.htmloutput.is-visible=false;outputwindow.oldoutput.is-visible=true;outputwindow.chatloadlabel.is-visible=false")
|
|
else if(loaded)
|
|
return //It can do it's own winsets from inside the JS if it's working.
|
|
|
|
//Shove all the assets at them
|
|
/datum/chatOutput/proc/send_resources()
|
|
for(var/filename in GLOB.vchatFiles)
|
|
owner << browse_rsc(file(filename))
|
|
resources_sent = TRUE
|
|
|
|
//Called from client/New() in a spawn()
|
|
/datum/chatOutput/proc/start()
|
|
if(!owner)
|
|
qdel(src)
|
|
return FALSE
|
|
|
|
if(!winexists(owner, "htmloutput"))
|
|
spawn()
|
|
alert(owner, "Updated chat window does not exist. If you are using a custom skin file please allow the game to update.")
|
|
become_broken()
|
|
return FALSE
|
|
|
|
if(!owner.is_preference_enabled(/datum/client_preference/vchat_enable))
|
|
become_broken()
|
|
return FALSE
|
|
|
|
//Could be loaded from a previous round, are you still there?
|
|
if(winget(owner,"outputwindow.htmloutput","is-visible") == "true") //Winget returns strings
|
|
send_event(event = list("evttype" = "availability"))
|
|
sleep(3 SECONDS)
|
|
|
|
if(!owner) // In case the client vanishes before winexists returns
|
|
qdel(src)
|
|
return FALSE
|
|
|
|
if(!loaded)
|
|
update_vis()
|
|
if(!resources_sent)
|
|
send_resources()
|
|
load()
|
|
|
|
return TRUE
|
|
|
|
//Attempts to actually load the HTML page into the client's UI
|
|
/datum/chatOutput/proc/load()
|
|
if(!owner)
|
|
qdel(src)
|
|
return
|
|
|
|
owner << browse(file2text("code/modules/vchat/html/vchat.html"), "window=htmloutput")
|
|
|
|
//Check back later
|
|
spawn(15 SECONDS)
|
|
if(!src)
|
|
return
|
|
if(!src.loaded)
|
|
src.become_broken()
|
|
|
|
//var/list/joins = list() //Just for testing with the below
|
|
//Called by Topic, when the JS in the HTML page finishes loading
|
|
/datum/chatOutput/proc/done_loading()
|
|
if(loaded)
|
|
return
|
|
|
|
loaded = TRUE
|
|
broken = FALSE
|
|
owner.chatOutputLoadedAt = world.time
|
|
|
|
//update_vis() //It does it's own winsets
|
|
ping_cycle()
|
|
send_playerinfo()
|
|
load_database()
|
|
|
|
owner.verbs += /client/proc/vchat_export_log
|
|
|
|
//Perform DB shenanigans
|
|
/datum/chatOutput/proc/load_database()
|
|
set waitfor = FALSE
|
|
// Only send them the number of buffered messages, instead of the ENTIRE log
|
|
var/list/results = vchat_get_messages(owner.ckey, message_buffer) //If there's bad performance on reconnects, look no further
|
|
for(var/i in results.len to 1 step -1)
|
|
var/list/message = results[i]
|
|
var/count = 10
|
|
to_chat_immediate(owner, message["time"], message["message"])
|
|
count++
|
|
if(count >= 10)
|
|
count = 0
|
|
CHECK_TICK
|
|
|
|
//It din work
|
|
/datum/chatOutput/proc/become_broken()
|
|
broken = TRUE
|
|
loaded = FALSE
|
|
|
|
if(!owner)
|
|
qdel(src)
|
|
return
|
|
|
|
update_vis()
|
|
|
|
spawn()
|
|
alert(owner,"VChat didn't load after some time. Switching to use oldchat as a fallback. Try using 'Reload VChat' verb in OOC verbs, or reconnecting to try again.")
|
|
|
|
//Provide the JS with who we are
|
|
/datum/chatOutput/proc/send_playerinfo()
|
|
if(!owner)
|
|
qdel(src)
|
|
return
|
|
|
|
var/list/playerinfo = list("evttype" = "byond_player", "cid" = owner.computer_id, "ckey" = owner.ckey, "address" = owner.address, "admin" = owner.holder ? "true" : "false")
|
|
send_event(playerinfo)
|
|
|
|
//Ugh byond doesn't handle UTF-8 well so we have to do this.
|
|
/proc/jsEncode(var/list/message) {
|
|
if(!islist(message))
|
|
CRASH("Passed a non-list to encode.")
|
|
return; //Necessary?
|
|
|
|
return url_encode(url_encode(json_encode(message)))
|
|
}
|
|
|
|
//Send a side-channel event to the chat window
|
|
/datum/chatOutput/proc/send_event(var/event, var/client/C = owner)
|
|
C << output(jsEncode(event), "htmloutput:get_event")
|
|
|
|
//Looping sleeping proc that just pings the client and dies when we die
|
|
/datum/chatOutput/proc/ping_cycle()
|
|
set waitfor = FALSE
|
|
while(!QDELING(src))
|
|
if(!owner)
|
|
qdel(src)
|
|
return
|
|
send_event(event = keep_alive())
|
|
sleep(20 SECONDS) //Make sure this makes sense with what the js client is expecting
|
|
|
|
//Just produces a message for using in keepalives from the server to the client
|
|
/datum/chatOutput/proc/keep_alive()
|
|
return list("evttype" = "keepalive")
|
|
|
|
//A response to a latency check from the client
|
|
/datum/chatOutput/proc/latency_check()
|
|
return list("evttype" = "pong")
|
|
|
|
//Redirected from client/Topic when the user clicks a link that pertains directly to the chat (when src == "chat")
|
|
/datum/chatOutput/Topic(var/href, var/list/href_list)
|
|
if(usr.client != owner)
|
|
return 1
|
|
|
|
if(last_topic_time > (world.time - 3 SECONDS))
|
|
too_many_topics++
|
|
if(too_many_topics >= topic_spam_limit)
|
|
log_and_message_admins("Kicking [key_name(owner)] - VChat Topic() spam")
|
|
to_chat(owner,"<span class='danger'>You have been kicked due to VChat sending too many messages to the server. Try reconnecting.</span>")
|
|
qdel(owner)
|
|
qdel(src)
|
|
return
|
|
else
|
|
too_many_topics = 0
|
|
last_topic_time = world.time
|
|
|
|
var/list/params = list()
|
|
for(var/key in href_list)
|
|
if(length(key) > 7 && findtext(key, "param"))
|
|
var/param_name = copytext(key, 7, -1)
|
|
var/item = href_list[key]
|
|
params[param_name] = item
|
|
|
|
var/data
|
|
switch(href_list["proc"])
|
|
if("not_ready")
|
|
CRASH("Tried to send a message to [owner.ckey] chatOutput before it was ready!")
|
|
if("done_loading")
|
|
data = done_loading(arglist(params))
|
|
if("ping")
|
|
data = latency_check(arglist(params))
|
|
if("ident")
|
|
data = bancheck(arglist(params))
|
|
if("unloading")
|
|
loaded = FALSE
|
|
if("debug")
|
|
data = debugmsg(arglist(params))
|
|
|
|
if(href_list["showingnum"])
|
|
message_buffer = CLAMP(text2num(href_list["showingnum"]), 50, 2000)
|
|
|
|
if(data)
|
|
send_event(event = data)
|
|
|
|
//Print a message that was an error from a client
|
|
/datum/chatOutput/proc/debugmsg(var/message = "No String Provided")
|
|
log_debug("VChat: [owner] got: [message]")
|
|
|
|
//Check relevant client info reported from JS
|
|
/datum/chatOutput/proc/bancheck(var/clientdata)
|
|
var/list/info = json_decode(clientdata)
|
|
var/ckey = info["ckey"]
|
|
var/ip = info["ip"]
|
|
var/cid = info["cid"]
|
|
|
|
//Never connected? How sad!
|
|
if(!cid && !ip && !ckey)
|
|
return
|
|
|
|
var/list/ban = world.IsBanned(key = ckey, address = ip, computer_id = cid)
|
|
if(ban)
|
|
log_and_message_admins("[key_name(owner)] has a cookie from a banned account! (Cookie: [ckey], [ip], [cid])")
|
|
|
|
//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 FALSE
|
|
|
|
GLOB.iconCache[iconKey] << icon
|
|
var/iconData = GLOB.iconCache.ExportText(iconKey)
|
|
var/list/partial = splittext(iconData, "{")
|
|
return replacetext(copytext(partial[2], 3, -5), "\n", "")
|
|
|
|
/proc/expire_bicon_cache(key)
|
|
if(GLOB.bicon_cache[key])
|
|
GLOB.bicon_cache -= key
|
|
return TRUE
|
|
return FALSE
|
|
|
|
GLOBAL_LIST_EMPTY(bicon_cache) // Cache of the <img> tag results, not the icons
|
|
/proc/bicon(var/obj, var/use_class = 1, var/custom_classes = "")
|
|
var/class = use_class ? "class='icon misc [custom_classes]'" : null
|
|
if(!obj)
|
|
return
|
|
|
|
// Try to avoid passing bicon an /icon directly. It is better to pass it an atom so it can cache.
|
|
if(isicon(obj)) // Passed an icon directly, nothing to cache-key on, as icon refs get reused *often*
|
|
return "<img [class] src='data:image/png;base64,[icon2base64(obj)]'>"
|
|
|
|
// Either an atom or somebody fucked up and is gonna get a runtime, which I'm fine with.
|
|
var/atom/A = obj
|
|
var/key
|
|
var/changes_often = ishuman(A) || isobserver(A) // If this ends up with more, move it into a proc or var on atom.
|
|
|
|
if(changes_often)
|
|
key = "\ref[A]"
|
|
else
|
|
key = "[istype(A.icon, /icon) ? "\ref[A.icon]" : A.icon]:[A.icon_state]"
|
|
|
|
var/base64 = GLOB.bicon_cache[key]
|
|
// Non-human atom, no cache
|
|
if(!base64) // Doesn't exist, make it.
|
|
base64 = icon2base64(A.examine_icon(), key)
|
|
GLOB.bicon_cache[key] = base64
|
|
if(changes_often)
|
|
addtimer(CALLBACK(GLOBAL_PROC, .proc/expire_bicon_cache, key), 50 SECONDS, TIMER_UNIQUE)
|
|
|
|
// May add a class to the img tag created by bicon
|
|
if(use_class)
|
|
class = "class='icon [A.icon_state] [custom_classes]'"
|
|
|
|
return "<img [class] src='data:image/png;base64,[base64]'>"
|
|
|
|
//Checks if the message content is a valid to_chat message
|
|
/proc/is_valid_tochat_message(message)
|
|
return istext(message)
|
|
|
|
//Checks if the target of to_chat is something we can send to
|
|
/proc/is_valid_tochat_target(target)
|
|
return !istype(target, /savefile) && (ismob(target) || islist(target) || isclient(target) || target == world)
|
|
|
|
var/to_chat_filename
|
|
var/to_chat_line
|
|
var/to_chat_src
|
|
|
|
//This proc is only really used if the SSchat subsystem is unavailable (not started yet)
|
|
/proc/to_chat_immediate(target, time, message)
|
|
if(!is_valid_tochat_message(message) || !is_valid_tochat_target(target))
|
|
target << message
|
|
|
|
// Info about the "message"
|
|
if(isnull(message))
|
|
message = "(null)"
|
|
else if(istype(message, /datum))
|
|
var/datum/D = message
|
|
message = "([D.type]): '[D]'"
|
|
else if(!is_valid_tochat_message(message))
|
|
message = "(bad message) : '[message]'"
|
|
|
|
// Info about the target
|
|
var/targetstring = "'[target]'"
|
|
if(istype(target, /datum))
|
|
var/datum/D = target
|
|
targetstring += ", [D.type]"
|
|
|
|
// The final output
|
|
log_debug("to_chat called with invalid message/target: [to_chat_filename], [to_chat_line], [to_chat_src], Message: '[message]', Target: [targetstring]")
|
|
return
|
|
|
|
else if(is_valid_tochat_message(message))
|
|
if(istext(target))
|
|
log_debug("Somehow, to_chat got a text as a target")
|
|
return
|
|
|
|
var/original_message = message
|
|
message = replacetext(message, "\n", "<br>")
|
|
message = replacetext(message, "\improper", "")
|
|
message = replacetext(message, "\proper", "")
|
|
|
|
if(isnull(time))
|
|
time = world.time
|
|
|
|
var/client/C = CLIENT_FROM_VAR(target)
|
|
if(!C)
|
|
return // No client? No care.
|
|
else if(C.chatOutput.broken)
|
|
DIRECT_OUTPUT(C, original_message)
|
|
return
|
|
else if(!C.chatOutput.loaded)
|
|
return // If not loaded yet, do nothing and history-sending on load will get it.
|
|
|
|
var/list/tojson = list("time" = time, "message" = message);
|
|
target << output(jsEncode(tojson), "htmloutput:putmessage")
|
|
|
|
/client/proc/vchat_export_log()
|
|
set name = "Export chatlog"
|
|
set category = "OOC"
|
|
|
|
if(chatOutput.broken)
|
|
to_chat(src, "<span class='warning'>Error: VChat isn't processing your messages!</span>")
|
|
return
|
|
|
|
var/list/results = vchat_get_messages(ckey)
|
|
if(!LAZYLEN(results))
|
|
to_chat(src, "<span class='warning'>Error: No messages found! Please inform a dev if you do have messages!</span>")
|
|
return
|
|
|
|
var/o_file = "data/chatlog_tmp/[ckey]_chat_log"
|
|
if(fexists(o_file) && !fdel(o_file))
|
|
to_chat(src, "<span class='warning'>Error: Your chat log is already being prepared. Please wait until it's been downloaded before trying to export it again.</span>")
|
|
return
|
|
|
|
o_file = file(o_file)
|
|
|
|
// Write the CSS file to the log
|
|
o_file << "<html><head><style>"
|
|
o_file << file2text(file("code/modules/vchat/css/ss13styles.css"))
|
|
o_file << "</style></head><body>"
|
|
|
|
// Write the messages to the log
|
|
for(var/list/result in results)
|
|
o_file << "[result["message"]]<br>"
|
|
|
|
o_file << "</body></html>"
|
|
|
|
// Send the log to the client
|
|
src << ftp(o_file, "log_[time2text(world.timeofday, "YYYY_MM_DD_(hh_mm)")].html")
|
|
|
|
// clean up the file on our end
|
|
spawn(10 SECONDS)
|
|
if(!fdel(o_file))
|
|
spawn(1 MINUTE)
|
|
if(!fdel(o_file))
|
|
log_debug("Warning: [ckey]'s chatlog could not be deleted one minute after file transfer was initiated. It is located at 'data/chatlog_tmp/[ckey]_chat_log' and will need to be manually removed.") |