NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Hugin oRO-Charinfo-Crawler // @namespace hugin // @version 0.2 // @description Grab oRO Character level and EXP info. // @author Sacer // @match https://cp.originsro.org/masteraccount/characters/ // @grant none // @licence MIT // ==/UserScript== /*jshint esversion: 8 */ (async function() { 'use strict'; 'use esversion: 8'; const DEV = false; // Cache-Schlüssel const lsName = 'hugin-name'; const lsSecret = 'hugin-secret'; const lsLastUpdated = 'hugin-last-updated'; // Munin URL const muninUrl = 'https://munin.info:8081/api/update'; // Funktion um die Chardaten zu sammeln. const getCharData = async function() { console.log('getchardata'); // Control Panel URL const cpUrl = 'https://cp.originsro.org'; // DOMParser um aus reinem Text (Strings) ein für JavaScript // verarbeitbares Element zu generieren. // Wird in Zeile 42 (:D) benötigt) const domParser = new DOMParser(); // Liste die später alle hält Charinfos. const chars = []; // Hauptcontainer der Webseite in der die Tabelle liegt. const main = document.getElementsByClassName('main'); // Die Tabelle innerhalb dieses Containers. const charTable = main[0].getElementsByClassName('table'); // Die Links zu den Charakterdetailseiten: const charLinks = charTable[0].getElementsByClassName('link-to-character'); // Schleife über die Links zu den Charaktern. // Heißt: Ich behandle einen Link (Element) nach dem anderen for (let i = 0; i < (DEV ? 1 : charLinks.length); i++) { // Das HTML Element mit dem Link, // ergo das einzelne Element // aus der Liste `charLinks` // an der stelle `i` // `i` ist anfangs 0 und wird nach jedem // Durchgang +1 nach oben gerechnet. const tag = charLinks[i]; // Endlich der Link zu den Charakterdetails! const url = tag.getAttribute('href'); // Den Inhalt (HTML) der Charakterdetails holen const charHTMLResponse = await fetch(cpUrl + url); // Die Antwort von "gib mir die Seite (fetch)" in Text "umwandeln" const charHTMLText = await charHTMLResponse.text(); // Den Text zu Element umwandeln const charHTML = domParser.parseFromString(charHTMLText, 'text/html'); // Jetzt werden 2 Tabellen benötigt: // 1. Character Info // 2. Details // Beide liegen in "main", glücklicherweise in der passenden Reihenfolge const charMain = charHTML.getElementsByClassName('main'); const charTables = charMain[0].getElementsByClassName('table'); const charInfoTable = charTables[0]; const charDetailsTable = charTables[1]; // Aus den Tabellen brauchen wir nun die Reihen // und aus diesen Reihen wiederum die Zellen const charInfoRows = charInfoTable.getElementsByTagName('tr'); const charDetailsRows = charDetailsTable.getElementsByTagName('tr'); // Aus den einzelnen Zellen können wir uns dann die Informationen holen, // die wir für unsere Charlist/EXP-Chart benötigen. // Um Missverständnisse auszuschließen, hier eine kurze Liste, // welches Element welche Information hält. // Kurzer Exkurs: // Listen werden vom Computer ab 0 gezählt. // Um ein Element zu erhalten, muss ich eine Nummer in []-Klammers anhängen. // // Info: "<- *" = "Diese Info wird an den Dienst übertragen" // // Character Info: // 1. charInfo[0] = Name <- * // 2. charInfo[1] = Character ID // 3. charInfo[2] = Char Deletion Pin // 4. charInfo[3] = Account // 5. charInfo[4] = Character Slot // 6. charInfo[5] = Job Class <- * // 7. charInfo[6] = Level <- * // 8. charInfo[7] = Troubleshooting // // Details: // 1. charDetails[0] = Base EXP <- * // 2. charDetails[1] = Base EXP % // 3. charDetails[2] = Job EXP <- * // 4. charDetails[3] = Job EXP % // 5. charDetails[4] = Location // 6. charDetails[5] = Save Point // 7. charDetails[6] = Zeny // 8. charDetails[7] = Jobchange Level // 9. charDetails[8] = Last Online // 10. charDetails[9] = Total Online Time const charInfoName = charInfoRows[0].getElementsByTagName('td'); const charInfoJob = charInfoRows[5].getElementsByTagName('td'); const charInfoLevel = charInfoRows[6].getElementsByTagName('td'); const charDetailsBaseEXP = charDetailsRows[0].getElementsByTagName('td'); const charDetailsJobEXP = charDetailsRows[2].getElementsByTagName('td'); // Wir Bereiten uns ein sog. "Objekt" vor, dass unsere Informationen hält // .trim() entfernt dabei Leerzeichen vorn und hinten, macht aus " Susi " also "Susi" const char = { name: charInfoName[0].innerHTML, jobClass: charInfoJob[0].innerHTML.trim(), level: charInfoLevel[0].innerHTML.trim(), exp: { base: charDetailsBaseEXP[0].innerHTML.trim(), job: charDetailsJobEXP[0].innerHTML.trim(), }, }; // Nun müssen die Daten noch etwas bereinigt werden. // Zum einen den Inhalt aus den Zellen holen, // zum anderen Zeug drumrum entfernen, dass wir wirklich nur // den reinen Wert haben. // Beim Namen ist noch ein Icon enthalten, das muss raus char.name = char.name.replace(/<span.*>\s+/gmi, '').trim(); // Base und JobEXP sind noch "Strings", also Text und ggf. mit Trennkommas. char.exp.base = parseInt(char.exp.base.replace(/,/g,'')); char.exp.job = parseInt(char.exp.job.replace(/,/g,'')); // Base und Job Level sind im Format "b / j" vorhanden, // das muss noch getrennt werden: const level = char.level.split('/'); char.level = { base: parseInt(level[0].trim()), job: parseInt(level[1].trim()), }; // Charinfo der Liste hinzufügen. chars.push(char); } // Ausgabe der zu übertragenden Daten in der Konsole. // Auf der Seite mit der Charliste Rechtsklick -> Untersuchen -> Reiter "Console" // zum Überprüfen öffnen. console.log(chars); window.huginCharData = chars; return chars; }; // Funktion um Daten an Munin zu senden const sendData = function(name, secret, data) { // Daten zusammenfassen zum Absenden fetch(muninUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({name, secret, data}) // body data type must match "Content-Type" header }).then(function(resp) { // Todo: Auf post response reagieren. localStorage.setItem(lsName, name); localStorage.setItem(lsSecret, secret); localStorage.setItem(lsLastUpdated, new Date()); }); }; // Funktion zur Erstellung des Parser-Control-Panels const createControlPanel = function() { const render = function(contents=[], style='') { const main = document.getElementsByClassName("main"); const panelHeading = main[0].getElementsByClassName("panel-heading"); const cpBody = document.createElement("div"); // Controlpanel vorbereiten cpBody.id = "cp-hugin"; cpBody.innerHTML = contents.join("\n") + "<style>" + style + "</style>"; // Controlpanel einhängen panelHeading[0].append(cpBody); }; // Cache auslesen const cache = { name: localStorage.getItem(lsName), secret: localStorage.getItem(lsSecret), lastUpdated: localStorage.getItem(lsLastUpdated), }; const hasUpdateTimeout = (cache.lastUpdated && new Date().getTime() - new Date(cache.lastUpdated).getTime() <= 1000 * 60 * 60 * 24); const contents = []; let style = "#cp-hugin{background-color:#d4e3d2;margin-top:10px;padding:10px 12px;border:2px solid #599956;border-radius:5px;}"; style += "#cp-hugin > p{margin:0 0 10px 0;}"; style += "#cp-hugin > p:last-child{margin-bottom:0;}"; // CP Title contents.push( '<p class="cp-title">HUGIN <small>(oRO Charinfo Crawler)</small></p>' ); style += "#cp-hugin p.cp-title{color:#a73d16;font-weight:900;}"; style += "#cp-hugin p.cp-title small{color:#000;font-size:75%;}"; // Loading Indicator contents.push( '<div id="hugin-loading">' + "<p>Sammle Charinfos...</p>" + '<div class="cp-progress"><div class="indeterminate"></div></div>' + "</div>" ); style += '#cp-hugin .cp-progress{position:relative;height:4px;display:block;width:100%;background-color:#a0c49b;border-radius:2px;background-clip:padding-box;margin:.5rem 0 1rem 0;overflow:hidden}#cp-hugin .cp-progress .indeterminate{background-color:#527d57}#cp-hugin .cp-progress .indeterminate:before{content:"";position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left,right;-webkit-animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite;animation:indeterminate 2.1s cubic-bezier(.65,.815,.735,.395) infinite}#cp-hugin .cp-progress .indeterminate:after{content:"";position:absolute;background-color:inherit;top:0;left:0;bottom:0;will-change:left,right;-webkit-animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;animation:indeterminate-short 2.1s cubic-bezier(.165,.84,.44,1) infinite;-webkit-animation-delay:1.15s;animation-delay:1.15s}@-webkit-keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@keyframes indeterminate{0%{left:-35%;right:100%}60%{left:100%;right:-90%}100%{left:100%;right:-90%}}@-webkit-keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}@keyframes indeterminate-short{0%{left:-200%;right:100%}60%{left:107%;right:-8%}100%{left:107%;right:-8%}}'; // Mainframe let mainframe = '<div id="hugin-contents">' + '<form id="hugin-data-form">' + '<label for="hugin-name">Name</label>' + '<input type="text" placeholder="Name..." name="name" id="hugin-name" ' + (cache.name ? 'value="' + cache.name + '"' : '') + '/>' + '<label for="hugin-name">Secret (<u>!!NICHT!!</u> dein Passwort!!)</label>' + '<input type="text" placeholder="Secret..." name="secret" id="hugin-secret" ' + (cache.secret ? 'value="' + cache.secret + '"' : '') + '/>'; // 24h Cache -> ms * seconds * minutes * hours // 1000 ms = 60 Sekunden // 60 Sekunden = 1 Minute usw... // Darum 1000(ms) * 60(s) * 60(m) * 24(h) if (hasUpdateTimeout) { const updated = new Date(cache.lastUpdated); mainframe += '<p class="cp-last-updated">Zuletzt aktualisiert: ' + updated.getDate()+'.'+(updated.getMonth()+1)+'.'+updated.getFullYear()+' '+updated.getHours()+':'+updated.getMinutes()+':'+updated.getSeconds() + '</p>'; } else { const btnLabel = cache.name && cache.secret && cache.lastUpdated ? 'Daten aktualisieren' : 'Daten senden'; mainframe += '<p class="tos-warning">ToS-WARNUNG!<br />Das automatische Sammeln von Daten aus der oRO-Webseite ist <i>eigentlich</i> nicht erlaubt.</br>' + 'Die Regel wurde vor Allem wegen Marketcrawlern eingeführt, weniger für diesen Fall.<br />' + 'Dennoch: Nutzung auf eigene Gefahr!<br />' + 'Siehe: <a href="https://originsro.org/tos#s6t4-website-crawling" target="_blank">ToS#Website-Crawling</a></p>'; mainframe += '<input type="button" id="hugin-form-submit" value="'+btnLabel+'" />'; style += '#cp-hugin .tos-warning{font-weight:900;color:#a73d16;}'; } mainframe += '</form>' + '</div>' + '</div>'; contents.push(mainframe); style += '#hugin-contents{display:none;}'; style += 'form#hugin-data-form label[for="hugin-name"]{display: block;}'; style += "form#hugin-data-form input{margin-bottom: 10px;}"; style += 'form#hugin-data-form input[type="button"]{display: block;}'; render(contents, style); if (hasUpdateTimeout) { // Ladebalken entfernen document.getElementById('hugin-loading').remove(); // Formular anzeigen document.getElementById('hugin-contents').style = 'display:block;'; } else { getCharData().then(function(data, res) { // Ladebalken entfernen document.getElementById('hugin-loading').remove(); // Formular anzeigen document.getElementById('hugin-contents').style = 'display:block;'; }); } // Submitbutton rausholen const submit = document.getElementById("hugin-form-submit"); // Auf Klick reagieren. submit && submit.addEventListener("click", function(e) { e.preventDefault(); const form = e.currentTarget.parentElement; const name = form.querySelector("#hugin-name"); const secret = form.querySelector("#hugin-secret"); if (name.value.length === 0) { alert('Name ist leer'); return; } else if(name.value.length > 26 || !name.value.match(/^[a-zA-Z]+$/g)) { alert('Name enthält ungültige Zeichen oder ist zu lang (max 26 Zeichen). Erlaubt sind a-z und A-Z. Kein Leerzeichen, keine Sonderzeichen, keine Umlaute etc. (Nein James, der Bindestrich in "a-z" oder "A-Z" heißt nicht, dass er eine Sondererlaubnis hat)'); return; } if (name.value.length === 0) { alert('Secret ist leer'); return; } else if(name.value.length > 64 || !name.value.match(/^[a-zA-Z0-9]+$/g)) { alert('Name enthält ungültige Zeichen oder ist zu lang (max 64 Zeichen). Erlaubt sind a-z, A-Z und 0-9. Kein Leerzeichen, keine Sonderzeichen, keine Umlaute etc. (Nein James, der Bindestrich in "a-z", "0-9" oder "A-Z" heißt nicht, dass er eine Sondererlaubnis hat)'); return; } sendData(name.value, secret.value, window.huginCharData); }); }; createControlPanel(); } )();