diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm index 7c2f550dc409..f7734502d652 100644 --- a/code/__DEFINES/misc.dm +++ b/code/__DEFINES/misc.dm @@ -441,3 +441,8 @@ GLOBAL_LIST_INIT(ghost_others_options, list(GHOST_OTHERS_SIMPLE, GHOST_OTHERS_DE #define MOUSE_OPACITY_TRANSPARENT 0 #define MOUSE_OPACITY_ICON 1 #define MOUSE_OPACITY_OPAQUE 2 + +//world/proc/shelleo +#define SHELLEO_ERRORLEVEL 1 +#define SHELLEO_STDOUT 2 +#define SHELLEO_STDERR 3 diff --git a/code/__HELPERS/shell.dm b/code/__HELPERS/shell.dm new file mode 100644 index 000000000000..8b615eac0a02 --- /dev/null +++ b/code/__HELPERS/shell.dm @@ -0,0 +1,57 @@ +//Runs the command in the system's shell, returns a list of (error code, stdout, stderr) + +#define SHELLEO_NAME "data/shelleo." +#define SHELLEO_ERR ".err" +#define SHELLEO_OUT ".out" +/world/proc/shelleo(command) + var/static/list/shelleo_ids = list() + var/stdout = "" + var/stderr = "" + var/errorcode = 1 + var/shelleo_id + var/out_file = "" + var/err_file = "" + var/static/list/interpreters = list("[MS_WINDOWS]" = "cmd /c", "[UNIX]" = "sh -c") + var/interpreter = interpreters["[world.system_type]"] + if(interpreter) + for(var/seo_id in shelleo_ids) + if(!shelleo_ids[seo_id]) + shelleo_ids[seo_id] = TRUE + shelleo_id = "[seo_id]" + break + if(!shelleo_id) + shelleo_id = "[shelleo_ids.len + 1]" + shelleo_ids += shelleo_id + shelleo_ids[shelleo_id] = TRUE + out_file = "[SHELLEO_NAME][shelleo_id][SHELLEO_OUT]" + err_file = "[SHELLEO_NAME][shelleo_id][SHELLEO_ERR]" + errorcode = shell("[interpreter] \"[command]\" > [out_file] 2> [err_file]") + if(fexists(out_file)) + stdout = file2text(out_file) + fdel(out_file) + if(fexists(err_file)) + stderr = file2text(err_file) + fdel(err_file) + shelleo_ids[shelleo_id] = FALSE + else + CRASH("Operating System: [world.system_type] not supported") // If you encounter this error, you are encouraged to update this proc with support for the new operating system + . = list(errorcode, stdout, stderr) +#undef SHELLEO_NAME +#undef SHELLEO_ERR +#undef SHELLEO_OUT + +/proc/shell_url_scrub(url) + var/static/regex/bad_chars_regex = regex("\[^#%&./:=?\\w]*", "g") + var/scrubbed_url = "" + var/bad_match = "" + var/last_good = 1 + var/bad_chars = 1 + do + bad_chars = bad_chars_regex.Find(url) + scrubbed_url += copytext(url, last_good, bad_chars) + if(bad_chars) + bad_match = url_encode(bad_chars_regex.match) + scrubbed_url += bad_match + last_good = bad_chars + length(bad_match) + while(bad_chars) + . = scrubbed_url diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index cb971b617aa5..3e0773131605 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -15,7 +15,7 @@ GLOBAL_PROTECT(config_dir) return ..() /datum/configuration/vv_edit_var(var_name, var_value) - var/static/list/banned_edits = list("cross_address", "cross_allowed", "autoadmin", "autoadmin_rank") + var/static/list/banned_edits = list("cross_address", "cross_allowed", "autoadmin", "autoadmin_rank", "invoke_youtubedl") if(var_name in banned_edits) return FALSE return ..() @@ -93,6 +93,8 @@ GLOBAL_PROTECT(config_dir) var/panic_server_name var/panic_address //Reconnect a player this linked server if this server isn't accepting new players + var/invoke_youtubedl + //IP Intel vars var/ipintel_email var/ipintel_rating_bad = 1 @@ -475,6 +477,8 @@ GLOBAL_PROTECT(config_dir) if("panic_server_address") if(value != "byond://address:port") panic_address = value + if("invoke_youtubedl") + invoke_youtubedl = value if("show_irc_name") showircname = 1 if("see_own_notes") diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 5e013525de33..30e52ebbccae 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -261,6 +261,8 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list( verbs += GLOB.admin_verbs_poll if(rights & R_SOUNDS) verbs += GLOB.admin_verbs_sounds + if(config.invoke_youtubedl) + verbs += /client/proc/play_web_sound if(rights & R_SPAWN) verbs += GLOB.admin_verbs_spawn @@ -283,6 +285,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list( /client/proc/stealth, GLOB.admin_verbs_poll, GLOB.admin_verbs_sounds, + /client/proc/play_web_sound, GLOB.admin_verbs_spawn, /*Debug verbs added by "show debug verbs"*/ /client/proc/Cell, diff --git a/code/modules/admin/verbs/playsound.dm b/code/modules/admin/verbs/playsound.dm index 84a29d9c0119..20c1d91fd22f 100644 --- a/code/modules/admin/verbs/playsound.dm +++ b/code/modules/admin/verbs/playsound.dm @@ -36,7 +36,11 @@ for(var/mob/M in GLOB.player_list) if(M.client.prefs.toggles & SOUND_MIDI) + var/user_vol = M.client.chatOutput.adminMusicVolume + if(user_vol) + admin_sound.volume = vol * (user_vol / 100) SEND_SOUND(M, admin_sound) + admin_sound.volume = vol SSblackbox.add_details("admin_verb","Play Global Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -52,6 +56,62 @@ playsound(get_turf(src.mob), S, 50, 0, 0) SSblackbox.add_details("admin_verb","Play Local Sound") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! +/client/proc/play_web_sound() + set category = "Fun" + set name = "Play Internet Sound" + if(!check_rights(R_SOUNDS)) + return + + if(!config.invoke_youtubedl) + to_chat(src, "Youtube-dl was not configured, action unavailable") //Check config.txt for the INVOKE_YOUTUBEDL value + return + + var/web_sound_input = input("Enter content URL (supported sites only, leave blank to stop playing)", "Play Internet Sound via youtube-dl") as text|null + if(istext(web_sound_input)) + var/web_sound_url = "" + var/pitch + if(length(web_sound_input)) + + web_sound_input = trim(web_sound_input) + var/static/regex/html_protocol_regex = regex("https?://") + if(findtext(web_sound_input, ":") && !findtext(web_sound_input, html_protocol_regex)) + to_chat(src, "Non-http(s) URIs are not allowed.") + to_chat(src, "For youtube-dl shortcuts like ytsearch: please use the appropriate full url from the website.") + return + var/shell_scrubbed_input = shell_url_scrub(web_sound_input) + var/list/output = world.shelleo("[config.invoke_youtubedl] --format \"bestaudio\[ext=aac]/bestaudio\[ext=mp3]/bestaudio\[ext=m4a]\" --get-url \"[shell_scrubbed_input]\"") + var/errorlevel = output[SHELLEO_ERRORLEVEL] + var/stdout = output[SHELLEO_STDOUT] + var/stderr = output[SHELLEO_STDERR] + if(!errorlevel) + var/static/regex/content_url_regex = regex("https?://\\S+") + if(content_url_regex.Find(stdout)) + web_sound_url = content_url_regex.match + + if(SSevents.holidays && SSevents.holidays[APRIL_FOOLS]) + pitch = pick(0.5, 0.7, 0.8, 0.85, 0.9, 0.95, 1.1, 1.2, 1.4, 1.6, 2.0, 2.5) + to_chat(src, "You feel the Honkmother messing with your song...") + + log_admin("[key_name(src)] played web sound: [web_sound_input]") + message_admins("[key_name(src)] played web sound: [web_sound_input]") + else + to_chat(src, "Youtube-dl URL retrieval FAILED:") + to_chat(src, "[stderr]") + + else //pressed ok with blank + log_admin("[key_name(src)] stopped web sound") + message_admins("[key_name(src)] stopped web sound") + web_sound_url = " " + + if(web_sound_url) + for(var/m in GLOB.player_list) + var/mob/M = m + var/client/C = M.client + if((C.prefs.toggles & SOUND_MIDI) && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded) + C.chatOutput.sendMusic(web_sound_url, pitch) + + SSblackbox.add_details("admin_verb","Play Internet Sound") + /client/proc/set_round_end_sound(S as sound) set category = "Fun" set name = "Set Round End Sound" diff --git a/code/modules/client/preferences_toggles.dm b/code/modules/client/preferences_toggles.dm index 1d04964a8b9b..8e8690bde325 100644 --- a/code/modules/client/preferences_toggles.dm +++ b/code/modules/client/preferences_toggles.dm @@ -144,6 +144,9 @@ TOGGLE_CHECKBOX(/datum/verbs/menu/Settings/Sound, togglemidis)() else to_chat(usr, "You will no longer hear sounds uploaded by admins") usr.stop_sound_channel(CHANNEL_ADMIN) + var/client/C = usr.client + if(C && C.chatOutput && !C.chatOutput.broken && C.chatOutput.loaded) + C.chatOutput.sendMusic(" ") SSblackbox.add_details("preferences_verb","Toggle Hearing Midis|[usr.client.prefs.toggles & SOUND_MIDI]") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /datum/verbs/menu/Settings/Sound/togglemidis/Get_checked(client/C) return C.prefs.toggles & SOUND_MIDI diff --git a/code/modules/goonchat/browserOutput.dm b/code/modules/goonchat/browserOutput.dm index cc44c47f4e53..4ed35b855b7e 100644 --- a/code/modules/goonchat/browserOutput.dm +++ b/code/modules/goonchat/browserOutput.dm @@ -13,6 +13,7 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic var/cookieSent = FALSE // Has the client sent a cookie for analysis var/broken = FALSE var/list/connectionHistory //Contains the connection history passed from chat cookie + var/adminMusicVolume = 100 //This is for the Play Global Sound verb /datum/chatOutput/New(client/C) owner = C @@ -79,6 +80,9 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic if("analyzeClientData") data = analyzeClientData(arglist(params)) + if("setMusicVolume") + data = setMusicVolume(arglist(params)) + if(data) ehjax_send(data = data) @@ -120,6 +124,16 @@ GLOBAL_DATUM_INIT(iconCache, /savefile, new("data/iconCache.sav")) //Cache of ic data = json_encode(data) C << output("[data]", "[window]:ehjaxCallback") +/datum/chatOutput/proc/sendMusic(music, pitch) + var/list/music_data = list("adminMusic" = url_encode(url_encode(music))) + if(pitch) + music_data["musicRate"] = pitch + ehjax_send(data = music_data) + +/datum/chatOutput/proc/setMusicVolume(volume = "") + if(volume) + adminMusicVolume = Clamp(text2num(volume), 0, 100) + //Sends client connection details to the chat to handle and save /datum/chatOutput/proc/sendClientData() //Get dem deets diff --git a/code/modules/goonchat/browserassets/css/browserOutput.css b/code/modules/goonchat/browserassets/css/browserOutput.css index 3f6a5ac5ca28..98e4e6af9772 100644 --- a/code/modules/goonchat/browserassets/css/browserOutput.css +++ b/code/modules/goonchat/browserassets/css/browserOutput.css @@ -101,7 +101,7 @@ a.popt {text-decoration: none;} top: 0; right: 0; } -#options a { +#options .optionsCell { background: #ddd; height: 30px; padding: 5px 0; @@ -111,7 +111,7 @@ a.popt {text-decoration: none;} line-height: 28px; border-top: 1px solid #b4b4b4; } -#options a:hover {background: #ccc;} +#options .optionsCell:hover {background: #ccc;} #options .toggle { width: 40px; background: #ccc; @@ -121,7 +121,7 @@ a.popt {text-decoration: none;} } #options .sub {clear: both; display: none; width: 160px;} #options .sub.scroll {overflow-y: scroll;} -#options .sub a {padding: 3px 0 3px 8px; line-height: 30px; font-size: 0.9em; clear: both;} +#options .sub.optionsCell {padding: 3px 0 3px 8px; line-height: 30px; font-size: 0.9em; clear: both;} #options .sub span { display: block; line-height: 30px; @@ -136,6 +136,13 @@ a.popt {text-decoration: none;} line-height: 30px; float: right; } +#options .sub input { + position: absolute; + padding: 7px 5px; + width: 121px; + line-height: 30px; + float: left; +} #options .decreaseFont {border-top: 0;} /* POPUPS */ diff --git a/code/modules/goonchat/browserassets/html/browserOutput.html b/code/modules/goonchat/browserassets/html/browserOutput.html index f55e69ecd173..a9e30b6b5138 100644 --- a/code/modules/goonchat/browserassets/html/browserOutput.html +++ b/code/modules/goonchat/browserassets/html/browserOutput.html @@ -28,17 +28,19 @@ --ms
- +
- Decrease font size - - Increase font size + - Toggle ping display - Highlight string - Save chat log - Clear all messages + Decrease font size - + Increase font size + + Toggle ping display + Highlight string + Save chat log + Clear all messages + Admin music volume
+ \ No newline at end of file diff --git a/code/modules/goonchat/browserassets/js/browserOutput.js b/code/modules/goonchat/browserassets/js/browserOutput.js index c7eff15913f8..eccb0dd2b6f3 100644 --- a/code/modules/goonchat/browserassets/js/browserOutput.js +++ b/code/modules/goonchat/browserassets/js/browserOutput.js @@ -61,8 +61,17 @@ var opts = { 'clientDataLimit': 5, 'clientData': [], + //Admin music volume update + 'volumeUpdateDelay': 5000, //Time from when the volume updates to data being sent to the server + 'volumeUpdating': false, //True if volume update function set to fire + 'updatedVolume': 0, //The volume level that is sent to the server + }; +function clamp(val, min, max) { + return Math.max(min, Math.min(val, max)) +} + function outerHTML(el) { var wrap = document.createElement('div'); wrap.appendChild(el.cloneNode(true)); @@ -95,6 +104,15 @@ function linkify(text) { }); } +function byondDecode(message) { + // Basically we url_encode twice server side so we can manually read the encoded version and actually do UTF-8. + // The replace for + is because FOR SOME REASON, BYOND replaces spaces with a + instead of %20, and a plus with %2b. + // Marvelous. + message = message.replace(/\+/g, "%20"); + message = decoder(message); + return message; +} + //Actually turns the highlight term match into appropriate html function addHighlightMarkup(match) { var extra = ''; @@ -176,11 +194,7 @@ function output(message, flag) { if (flag !== 'internal') opts.lastPang = Date.now(); - // Basically we url_encode twice server side so we can manually read the encoded version and actually do UTF-8. - // The replace for + is because FOR SOME REASON, BYOND replaces spaces with a + instead of %20, and a plus with %2b. - // Marvelous. - message = message.replace(/\+/g, "%20") - message = decoder(message) + message = byondDecode(message) //The behemoth of filter-code (for Admin message filters) //Note: This is proooobably hella inefficient @@ -423,7 +437,22 @@ function ehjaxCallback(data) { var firebugEl = document.createElement('script'); firebugEl.src = 'https://getfirebug.com/firebug-lite-debug.js'; document.body.appendChild(firebugEl); - } + } else if (data.adminMusic) { + if (typeof data.adminMusic === 'string') { + var adminMusic = byondDecode(data.adminMusic); + adminMusic = adminMusic.match(/https?:\/\/\S+/) || ''; + if (data.musicRate) { + var newRate = Number(data.musicRate); + if(newRate) { + $('#adminMusic').prop('defaultPlaybackRate', newRate); + } + } else { + $('#adminMusic').prop('defaultPlaybackRate', 1.0); + } + $('#adminMusic').prop('src', adminMusic); + $('#adminMusic').trigger("play"); + } + } } } @@ -446,6 +475,13 @@ function toggleWasd(state) { opts.wasd = (state == 'on' ? true : false); } +function sendVolumeUpdate() { + opts.volumeUpdating = false; + if(opts.updatedVolume) { + runByond('?_src_=chat&proc=setMusicVolume¶m[volume]='+opts.updatedVolume); + } +} + /***************************************** * * DOM READY @@ -486,6 +522,7 @@ $(function() { 'spingDisabled': getCookie('pingdisabled'), 'shighlightTerms': getCookie('highlightterms'), 'shighlightColor': getCookie('highlightcolor'), + 'smusicVolume': getCookie('musicVolume'), }; if (savedConfig.sfontSize) { @@ -517,6 +554,14 @@ $(function() { opts.highlightColor = savedConfig.shighlightColor; internalOutput('Loaded highlight color of: '+savedConfig.shighlightColor+'', 'internal'); } + if (savedConfig.smusicVolume) { + var newVolume = clamp(savedConfig.smusicVolume, 0, 100); + $('#adminMusic').prop('volume', newVolume / 100); + $('#musicVolume').val(newVolume); + opts.updatedVolume = newVolume; + sendVolumeUpdate(); + internalOutput('Loaded music volume of: '+savedConfig.smusicVolume+'', 'internal'); + } (function() { var dataCookie = getCookie('connData'); @@ -835,6 +880,26 @@ $(function() { opts.messageCount = 0; }); + $('#musicVolumeSpan').hover(function() { + $('#musicVolumeText').addClass('hidden'); + $('#musicVolume').removeClass('hidden'); + }, function() { + $('#musicVolume').addClass('hidden'); + $('#musicVolumeText').removeClass('hidden'); + }); + + $('#musicVolume').change(function() { + var newVolume = $('#musicVolume').val(); + newVolume = clamp(newVolume, 0, 100); + $('#adminMusic').prop('volume', newVolume / 100); + setCookie('musicVolume', newVolume, 365); + opts.updatedVolume = newVolume; + if(!opts.volumeUpdating) { + setTimeout(sendVolumeUpdate, opts.volumeUpdateDelay); + opts.volumeUpdating = true; + } + }); + $('img.icon').error(iconError); diff --git a/config/config.txt b/config/config.txt index 3ffd92300f51..b9e622fb28b9 100644 --- a/config/config.txt +++ b/config/config.txt @@ -195,6 +195,14 @@ CHECK_RANDOMIZER ## Ban appeals URL - usually for a forum or wherever people should go to contact your admins. # BANAPPEALS http://justanotherday.example.com +## System command that invokes youtube-dl, used by Play Internet Sound. +## You can install youtube-dl with +## "pip install youtube-dl" if you have pip installed +## from https://github.com/rg3/youtube-dl/releases +## or your package manager +## The default value assumes youtube-dl is in your system PATH +# INVOKE_YOUTUBEDL youtube-dl + ## In-game features ##Toggle for having jobs load up from the .txt # LOAD_JOBS_FROM_TXT diff --git a/tgstation.dme b/tgstation.dme index 9a4c6b136686..215836145fcc 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -93,6 +93,7 @@ #include "code\__HELPERS\pronouns.dm" #include "code\__HELPERS\radio.dm" #include "code\__HELPERS\sanitize_values.dm" +#include "code\__HELPERS\shell.dm" #include "code\__HELPERS\text.dm" #include "code\__HELPERS\time.dm" #include "code\__HELPERS\type2type.dm"