Skribbl Image Saver

Automatically save images and chatlogs from skribbl.io

// ==UserScript==
// @name         Skribbl Image Saver
// @namespace    https://github.com/tntmod54321
// @version      1.1
// @description  Automatically save images and chatlogs from skribbl.io
// @author       tntmod54321
// @homepageURL  https://github.com/tntmod54321/skribbl-image-saver-userscript
// @match        https://skribbl.io*
// @grant        none
// @license      CC0
// ==/UserScript==

//initialize global variables
var imageArray=[]
var imageElementAlreadyCreated=false
var buttonToggle=false
var base64data
var stylesheets
var debugMode=false
const overlayPromptRE = new RegExp('The word was: ([a-zA-Z0-9\\- ]+)'); //don't match for characters that would make invalid filenames
const removeInvalidCharsRE = new RegExp('[^0-9a-zA-Z_\\- ]', 'g'); //sanitize characters for filenames.
// vars for caching names and prompts for downloads
var lastKnownName=null
var lastKnownPrompt=null
var roundEnded=null // if we are in a 'round end' state
const overlayPresentRE = new RegExp('display: none;$')
// prefs
var autoDLImgs=true
var autoDLImgdone=false
var autoDLChatLogs=true
var autoDLChatLogdone=false

var autoDLImgs_is_checked=false
var autoDLChatLogs_is_checked=false

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function setCookie(c_name, value, expiredays) {
    var exdate = new Date();
    exdate.setDate(exdate.getDate()+expiredays);
    document.cookie = c_name + "=" + escape(value) + ((expiredays==null) ?
        "" :
        ";expires="+exdate.toUTCString());
}

function getCookie(cname) {
  let name = cname + "=";
  let decodedCookie = decodeURIComponent(document.cookie);
  let ca = decodedCookie.split(';');
  for(let i = 0; i <ca.length; i++) {
    let c = ca[i];
    while (c.charAt(0) == ' ') {
      c = c.substring(1);
    }
    if (c.indexOf(name) == 0) {
      return c.substring(name.length, c.length);
    }
  }
  return "";
}

function getRoundState(){
    var state=false //round is not ended by default
    var overlay=document.getElementById("overlay")
    if (overlay.style.cssText.match(overlayPresentRE)) {state=false}
    else {state=true}
    return state
}

function getCurrentDrawer(queryisknown=false){
    var drawer = "unknown"
    var isknown = false

    //under class=player/class=avatar/
    //drawer: <div class="drawing"></div>
    //non-drawer: <div class="drawing" style="display: none;"></div>
    var playerContainer=document.getElementById("containerGamePlayers")
    var players = playerContainer.getElementsByClassName("player")

    for (let i = 0; i < players.length; i++) {
        var drawerElement = players[i].getElementsByClassName("drawing")[0] //only one per player (therefor 0 is the only index)
        if (!drawerElement.style.cssText) {drawer=players[i].getElementsByClassName("name")[0].innerText;isknown=true;break}
    }

    //parantheses only allowed when you're the drawer, so remove ' (You)'
    drawer=drawer.replace(new RegExp(' \\(You\\)$'), '')
    //try to sanitize username
    drawer=drawer.replace(removeInvalidCharsRE, '');
    if (queryisknown) {return [drawer, isknown]}
    return drawer
}

function getPrompt(queryisknown=false){
    var prompt = "unknown"
    var isknown = false

    var contentClass=document.getElementsByClassName('content')
    for (let i = 0; i < contentClass.length; i++) {
        var text = contentClass[i].getElementsByClassName("text")[0]
        var result = text.outerText.match(overlayPromptRE)
        if (result) {prompt=result[1];isknown=true;break}
    }

    //if we don't get a result use this fallback
    if (!result) {
        var fallbackElement=document.getElementById('currentWord')
        if (fallbackElement) { //don't crash if the element doesn't exist
            if (fallbackElement.outerText!="") {prompt = fallbackElement.outerText;isknown=true}
        }
    }
    if (queryisknown) {return [prompt, isknown]}
    return prompt
}

//create an element to be able to display the image
function displayImage(){
    if (imageElementAlreadyCreated){return}
    var body = document.getElementsByTagName("body")
    var a = document.createElement("center")
    var b = document.createElement("img")
    a.id="XD1"
    b.id="XD2"
    body[0].appendChild(a)
    var center = document.getElementById("XD1")
    center.appendChild(b)
    imageElementAlreadyCreated=true
}

//converts the canvas to a data blob
async function canvasToBlob() {
    var canvas = document.getElementById('canvasGame')
    const blob = await new Promise((resolve) => canvas.toBlob(resolve))
    const blobURL = URL.createObjectURL(blob)
    URL.revokeObjectURL(blobURL)
    return blob
}

//embed the data blob of the canvas into the page
async function mirrorImage(){
    console.clear()
    var Image = await canvasToBlob();
    displayImage()
    var HTMLImage = document.getElementById("XD2")
    var reader = new FileReader()
    reader.readAsDataURL(Image)
    reader.onloadend = function() {
        var base64data = reader.result
        HTMLImage.src=base64data
        if(buttonToggle){mirrorImage()}
        else{HTMLImage.src=""}
    }
}

//return the index of the cssrule for the second stylesheet that matches the passed (selector) argument
function getStyleSheet(selector){
    var stylesheets=document.styleSheets[1].cssRules
    for (var i in stylesheets){
        if (stylesheets[i].selectorText==selector){
            return i
        }
        i++
    }
    console.log("stylesheet not found :(")
}

//insert download button
function insertDownloadButton(){
    var leftSidebar = document.getElementById("containerPlayerlist")
    var downloadButton = document.createElement("button")
    downloadButton.classList.add('btn')
    downloadButton.classList.add('btn-block')
    downloadButton.id="downloadImage"
    downloadButton.style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(117, 142, 250);border-color: rgb(93, 113, 199);"
    var downloadText = document.createTextNode("Download image");

	downloadButton.onclick=function() {downloadBlobImage()}

	downloadButton.appendChild(downloadText)
    leftSidebar.appendChild(downloadButton)
}

//toggle mirror function
function mirBtnFunc(){
	mirrorImage()
	buttonToggle=(!buttonToggle)
}

//Download Image
//https://stackoverflow.com/questions/52817280/problem-downloading-a-pdf-blob-in-javascript
//!!do something with this ^ (you don't need to convert to data url)
async function downloadBlobImage(){
	var Image = await canvasToBlob();
    var reader = new FileReader()
    reader.readAsDataURL(Image)
    reader.onloadend = function() {
        var base64data = reader.result
        var a = document.createElement('a')
        a.setAttribute('href', base64data)
        var DL_Name="skribbl-"+(Date.now()/1000|0)+'-'+lastKnownName+"-"+lastKnownPrompt+".png" //round date.now to secs instead of millis
        a.setAttribute('download', DL_Name)
        document.body.appendChild(a)
        a.click()
        document.body.removeChild(a)
    }

}

//open the blob as an image in a new tab
//!!TRY TO ACTUALLY FIX THIS
//create basically <html><a></html> and in that way you can set the filename and probably have it work
async function openImageInNewTab(){
    var Image = await canvasToBlob()
    var fileURL = URL.createObjectURL(Image)
    var a = document.createElement('a')
    a.href = fileURL
    a.target = '_blank'
    //a.download = 'test.png'
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(fileURL)
}

//insert mirror button
function insertMirrorButton(){
	var leftSidebar = document.getElementById("containerPlayerlist")
	var mirrorButton = document.createElement("button")
	mirrorButton.classList.add('btn')
    mirrorButton.classList.add('btn-block')
    mirrorButton.id="mirrorImage"
	mirrorButton.style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(92, 218, 218);border-color: rgb(50, 214, 200);"
	mirrorButton.onclick=function() {mirBtnFunc()}
	var mirrorText = document.createTextNode("Mirror image")
	mirrorButton.appendChild(mirrorText)
    leftSidebar.appendChild(mirrorButton)
}

//insert the new tab button
function insertNewTabButton(){
    var leftSidebar = document.getElementById("containerPlayerlist")
    var newTabButton = document.createElement("button")
    newTabButton.classList.add('btn')
    newTabButton.classList.add('btn-block')
    newTabButton.id="openNewTab"
    newTabButton.style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(102, 218, 92);border-color: rgb(72, 214, 50);"

	newTabButton.onclick=function() {openImageInNewTab()}
    var newTabText = document.createTextNode("Open Image in New Tab");
	newTabButton.appendChild(newTabText)
    leftSidebar.appendChild(newTabButton)
}

//main function
function main(){
	//fix button spacing in css
	var stylesheetIndex=getStyleSheet("#containerPlayerlist")
	var stylesheets=document.styleSheets[1].cssRules[stylesheetIndex].style
    stylesheets.removeProperty("justify-content")
    // should read cookies here

	//insert buttons, in order they appear on the screen
	insertDownloadButton()
    if (debugMode) {insertNewTabButton()}
    insertDownloadChatlogButton()
    if (debugMode) {insertMirrorButton()}
    insertAutoDLImgsCheckbox()
    insertAutoDLChatLogsCheckbox()
    console.log("Skribbl Image Saver Loaded!")

    autoFuncs()
}

//open text in new tab
function openTextInNewTab(array){
    var chatLog=""
    for (var object in array){
        chatLog=chatLog.concat(array[object]+"\n")
    }
    if(chatLog==""){return}
    chatLog="<div id=\"boxMessages\" style=\"font-family: Helvetica Neue, Helvetica, Arial, sans-serif;\">\n".concat(chatLog)
    chatLog=chatLog.concat("</div>")
    var chatLogBlob = new Blob([chatLog],{ type: "text/html;charset=utf-8" });
    var fileURL = URL.createObjectURL(chatLogBlob)
    var a = document.createElement('a')
    a.href = fileURL
    a.target = '_blank'
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(fileURL)
}

//download text as an html file
function downloadText(array){
    var chatLog=""
    for (var object in array){
        chatLog=chatLog.concat(array[object]+"\n")
    }
    if(chatLog==""){return}
    chatLog="<html><body><div id=\"boxMessages\" style=\"font-family: Helvetica Neue, Helvetica, Arial, sans-serif;\">\n".concat(chatLog)
    chatLog=chatLog.concat("</div></body></html>")
    var chatLogBlob = new Blob([chatLog],{ type: "text/html;charset=utf-8" });
    var fileURL = URL.createObjectURL(chatLogBlob)
    var a = document.createElement('a')
    a.setAttribute('href', fileURL)
    var DL_Name="skribblchatlog-"+(Date.now()/1000|0)+'-'+lastKnownPrompt+".html" //round date.now to secs instead of millis
    a.setAttribute('download', DL_Name)
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(fileURL)
}

//function to save the chat as an html file
function saveChat(){
    var chatElement = document.getElementById("boxMessages")
    var chatChildren = chatElement.children
    var array2=[]
    for (var message in chatChildren){
        if (typeof chatChildren[message]=="object"){array2.push(chatChildren[message].outerHTML)}
    }
    downloadText(array2)
}

async function autoFuncs(){
    while (true) {
        await sleep(100);
        //check for drawer's name and prompt name
        var prompt=await getPrompt(true)
        if (prompt[1] && !roundEnded) {lastKnownPrompt=prompt[0];} // if roundend don't update lastknownprompt
        var lastFrameRoundEndState = roundEnded
        roundEnded=getRoundState()
        if (!roundEnded && lastFrameRoundEndState) {autoDLImgdone=false;autoDLChatLogdone=false;} // if a new round just started
        var drawer=await getCurrentDrawer(true)
        if (drawer[1]) {lastKnownName=drawer[0]} // if the name is known to be valid AND isn't at the end of the round

        if (autoDLImgs && roundEnded && !autoDLImgdone){await downloadBlobImage();autoDLImgdone=true;}

        if (autoDLChatLogs && roundEnded && !autoDLChatLogdone){await saveChat();autoDLChatLogdone=true;}

        //make sure to have button checkboxe(s) for this
        //https://stackoverflow.com/questions/13452626/create-a-cookie-with-javascript-in-greasemonkey
        //use cookie to save prefs?
    }
}

function insertDownloadChatlogButton(){
    var leftSidebar = document.getElementById("containerPlayerlist")
    var chatlogButton = document.createElement("button")
    chatlogButton.classList.add('btn')
    chatlogButton.classList.add('btn-block')
    chatlogButton.id="downloadChatLog"
    chatlogButton.style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(117, 142, 250);border-color: rgb(93, 113, 199);"

	chatlogButton.onclick=function() {saveChat()}
    var newTabText = document.createTextNode("Download Chatlog");
	chatlogButton.appendChild(newTabText)
    leftSidebar.appendChild(chatlogButton)
}

function updateAudoDLImgsPref(){ //set button to green or red?
    if (autoDLImgs_is_checked) {
        // color button red
        document.getElementById("autoDlImgsToggle").style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(214, 45, 45);border-color: rgb(214, 45, 45);"
        autoDLImgs_is_checked=false
        autoDLImgs=false
    }
    else {
        // color button green
        document.getElementById("autoDlImgsToggle").style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(102, 218, 92);border-color: rgb(102, 218, 92);"
        // make it not autodownload in the middle of a round if you toggle the button (idk if this is real)
        autoDLImgs_is_checked=true
        autoDLImgs=true
    }
    console.log("Skribble Image Saver: save_images", autoDLImgs_is_checked)
    setCookie("SIS_autodlimgs", autoDLImgs_is_checked, 365);
    // update cookie(s) (cookie=autoDLImgs_is_checked)
}

function insertAutoDLImgsCheckbox(){
    var leftSidebar = document.getElementById("containerPlayerlist")
    var autodlimgsButton = document.createElement("checkbox")
    autodlimgsButton.classList.add('btn')
    autodlimgsButton.classList.add('btn-block')
    autodlimgsButton.id="autoDlImgsToggle"
    //read cookie and set style accordingly
    var adlimgs_cookie = getCookie("SIS_autodlimgs")
    if (adlimgs_cookie=="") { // if no cookie set to false
        console.log("no adlimgs cookie set")
        autoDLImgs_is_checked=false
        autoDLImgs=false
        adlimgs_cookie=false
        setCookie("SIS_autodlimgs", false, 365);
    }
    if (adlimgs_cookie=="false"||adlimgs_cookie==false){
        // color button red
        autodlimgsButton.style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(214, 45, 45);border-color: rgb(214, 45, 45);"
        autoDLImgs_is_checked=false
        autoDLImgs=false
    }
    else {
        // color button green
        autodlimgsButton.style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(102, 218, 92);border-color: rgb(102, 218, 92);"
        // make it not autodownload in the middle of a round if you toggle the button (idk if this is real)
        autoDLImgs_is_checked=true
        autoDLImgs=true
    }
	autodlimgsButton.onclick=function() {updateAudoDLImgsPref()}
    var newTabText = document.createTextNode("AutoDL Images");
	autodlimgsButton.appendChild(newTabText)
    leftSidebar.appendChild(autodlimgsButton)
}

function updateAudoDLChatLogsPref(){ //set button to green or red?
    if (autoDLChatLogs_is_checked) {
        // color button red
        document.getElementById("autoDlChatLogsToggle").style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(214, 45, 45);border-color: rgb(214, 45, 45);"
        autoDLChatLogs_is_checked=false
        autoDLChatLogs=false
    }
    else {
        // color button green
        document.getElementById("autoDlChatLogsToggle").style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(102, 218, 92);border-color: rgb(102, 218, 92);"
        // make it not autodownload in the middle of a round if you toggle the button (idk if this is real)
        autoDLChatLogs_is_checked=true
        autoDLChatLogs=true
    }
    console.log("Skribble Image Saver: save_chatlogs", autoDLChatLogs_is_checked)
    setCookie("SIS_autodlchatlogs", autoDLChatLogs_is_checked, 365);
}

function insertAutoDLChatLogsCheckbox(){
    var leftSidebar = document.getElementById("containerPlayerlist")
    var autodllogsButton = document.createElement("checkbox")
    autodllogsButton.classList.add('btn')
    autodllogsButton.classList.add('btn-block')
    autodllogsButton.id="autoDlChatLogsToggle"
    //read cookie and set style accordingly
    var adllogs_cookie = getCookie("SIS_autodlchatlogs")
    if (adllogs_cookie=="") { // if no cookie set to false
        console.log("no adlcl cookie set")
        autoDLChatLogs_is_checked=false
        autoDLChatLogs=false
        adllogs_cookie=false
        setCookie("SIS_autodlchatlogs", false, 365);
    }
    if (adllogs_cookie=="false"||adllogs_cookie==false){
        // color button red
        autodllogsButton.style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(214, 45, 45);border-color: rgb(214, 45, 45);"
        autoDLChatLogs_is_checked=false
        autoDLChatLogs=false
    }
    else {
        // color button green
        autodllogsButton.style="margin-top:8px;color: rgb(255, 255, 255);background-color: rgb(102, 218, 92);border-color: rgb(102, 218, 92);"
        // make it not autodownload in the middle of a round if you toggle the button (idk if this is real)
        autoDLChatLogs_is_checked=true
        autoDLChatLogs=true
    }
	autodllogsButton.onclick=function() {updateAudoDLChatLogsPref()}
    var newTabText = document.createTextNode("AutoDL Chatlogs");
	autodllogsButton.appendChild(newTabText)
    leftSidebar.appendChild(autodllogsButton)
}

//test any function with e
/*
document.onkeyup=function(e){
    if(e.which == 69) {
        //console.log()
        console.log(getCookie("dfjskfjksd"))
    }
};
*/
main()