// ==UserScript== // @name Kick.com - Dodawanie emotek u streamerów // @namespace http://tampermonkey.net/ // @version 1.1 // @description Łatwy sposób na rozpierdalanie kanałów własnymi emotkami // @author @adamcy // @match https://kick.com/dashboard/community/emotes // @icon https://www.google.com/s2/favicons?sz=64&domain=kick.com // @grant none // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/470413/Kickcom%20-%20Dodawanie%20emotek%20u%20streamer%C3%B3w.user.js // @updateURL https://update.greasyfork.icu/scripts/470413/Kickcom%20-%20Dodawanie%20emotek%20u%20streamer%C3%B3w.meta.js // ==/UserScript== (function() { 'use strict'; let Module = function () { let mainView = null; let emoteViews = null; Module.Views = { EmoteController: function () { let view = createView(); let emote = null; let emoteName = view.querySelector('[data-test="emote-name"]'); let image = view.querySelector('img') let delete_emote = view.querySelector('[data-test="delete-emote"]') emoteName.addEventListener("focus", function () { emoteName.select(); }); emoteName.addEventListener("focusout", function () { emote.name = Module.Utility.ensureStringLength(emoteName.value, 15); }); image.onload = () => { URL.revokeObjectURL(image.src); } delete_emote.onclick = function () { Module.EmoteListController.deleteEmote(emote).then(() => { deleteEmote(); }); } mainView.addEventListener('EMOTE_UPLOADED', e => { if (e.detail.emote === emote) { update(emote); } }) mainView.addEventListener('EMOTE_DELETED', e => { if (e.detail.emote === emote) { deleteEmote(); } }) function deleteEmote() { update(null); let parent = view.parentNode; view.remove(); parent.appendChild(view); } function createView() { return Module.Utility.htmlToElement(`
  • `) } view.disabled = true; function getView() { return view; } function getEmote() { return emote; } function setIndex(index) { view.dataset.index = index; } function update($emote) { emoteName.value = $emote?.name || ""; if (!$emote?.source) { image.removeAttribute('src'); } else { if ($emote !== emote) { image.src = $emote?.source; } } view.setAttribute('uploaded', $emote?.uploaded || false); view.setAttribute('empty', $emote === null); emote = $emote; emoteName.disabled = !$emote || $emote.uploaded; } return { update: update, getView: getView, getEmote: getEmote, setIndex: setIndex } }, MainView: function () { return Module.Utility.htmlToElement(`
    `) } } Module.Utility = { htmlToElement: function (html) { const template = document.createElement('template'); html = html.trim(); template.innerHTML = html; return template.content.firstChild; }, randomUUID: function (segments = 8) { let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXWZ"; let numbers = "0123456789"; let result = ""; for (let i = 0; i < segments; i++) { let a = alphabet.charAt(alphabet.length * Math.random()); let b = numbers.charAt(numbers.length * Math.random()); result += a + b; if (i !== segments - 1) { result += "-"; } } return result; }, getEmoteURL(id) { return `https://d2egosedh0nm8l.cloudfront.net/emotes/${id}/fullsize`; }, ensureStringLength(string, length) { let result = string; if (string.length > 15) { result = string.substring(0, 15); } return result; },isValidFileExtension(fileName) { let extension = fileName.substring(fileName.lastIndexOf('.'), fileName.length).toLowerCase(); return ['.png', '.gif'].indexOf(extension) !== -1; } } mainView = Module.Views.MainView(); document.body.appendChild(mainView); let Emote = function () { this.name = ""; this.uploaded = true; this.source = null; this.type = null; this.id = Module.Utility.randomUUID(); this.file = null; } Module.Initialize = function () { Module.EmoteListController.initialize(); } Module.MainController = new function () { const Button_SelectFile = document.querySelector('[data-test=select-files]'); const Input_Files = document.querySelector('[data-test=files]'); const Button_DeleteEmotes = document.querySelector('[data-test=delete-all-emotes]') const Button_Close = document.querySelector('[data-test="close"]'); const Button_Upload = document.querySelector('[data-test="upload"]'); emoteViews = document.querySelector('.image-file-list').children; Button_Close.onclick = () => { mainView.remove(); } Button_Upload.onclick = () => { disable() Module.EmoteManager.uploadEmotes().then(e => { enable(); }); } Button_DeleteEmotes.onclick = () => { if (!window.confirm("are you sure you want to delete all uploaded emotes?")) return; disable() Module.EmoteManager.deleteAllEmotes().then(() => { enable() }); } function enable() { mainView.removeAttribute('disabled'); } function disable() { mainView.setAttribute('disabled', ''); } Button_SelectFile.addEventListener("click", (e) => { if (Input_Files) { Input_Files.click(); } e.preventDefault(); }, false); Input_Files.addEventListener("change", e => { onFilesSelected(e.target.files); }, false); setupDragDrop(); function setupDragDrop() { let dropZone = mainView.querySelector('.drop-zone'); ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, highlight, false); }); ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, unhighlight, false); }); ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); }, false) }); dropZone.addEventListener('drop', handleDrop, false) function handleDrop(e) { let dt = e.dataTransfer; let files = dt.files; onFilesSelected(files); } function highlight(e) { if (e.dataTransfer.types.indexOf('Files') === -1) return; dropZone.classList.add('active'); } function unhighlight() { dropZone.classList.remove('active') } } function onFilesSelected(files) { let filesToBeUploaded = []; for (let file of files) { filesToBeUploaded.push(file) } if (filesToBeUploaded.length !== 0) { for (let i = 0; i < filesToBeUploaded.length; i++) { let file = filesToBeUploaded[i]; if (file.size >= (1024 * 1024) || !Module.Utility.isValidFileExtension(file.name)) { continue } let emote = new Emote(); emote.name = Module.Utility.ensureStringLength(file.name.replace('.png', '').replace('.gif', ''), 15); emote.source = URL.createObjectURL(file); emote.type = file.name.replace(/.*?\./, "").toLowerCase(); emote.uploaded = false; emote.file = file; if (!Module.EmoteListController.addEmote(emote)) { break; } } } } this.updateUploadButton = function (text) { Button_Upload.innerText = text; } this.updatesDeleteEmotesButton = function (text) { Button_DeleteEmotes.innerText = text; } } Module.EmoteListController = new function () { let emotes = []; let controllers = []; let reference = this; this.initialize = function () { let imageFileList = document.querySelector('.image-file-list') console.log(imageFileList) for (let i = 0; i <= 60; i++) { let controller = new Module.Views.EmoteController(); controller.setIndex(i); controllers.push(controller) imageFileList.appendChild(controller.getView()); } Module.EmoteManager.retrieveUploadedEmotes().then(emotes => { for (let emote of emotes) { reference.addEmote(emote) } }) } this.addEmote = function (emote) { let controller = getNextAvailableController(); if (!controller) return false; controller.update(emote); emotes.push(emote); return true; } this.deleteEmote = async function (emote) { if (emote.uploaded === true) { await Module.EmoteManager.deleteEmote(emote).then(); } emotes.splice(emotes.indexOf(emote)); } function getNextAvailableController() { for (let i = 0; i < emoteViews.length; i++) { let emoteView = emoteViews[i]; let controller = controllers[emoteView.dataset.index]; if (!controller.getEmote()) { return controller; } } } this.getEmotes = function () { return emotes; } } Module.EmoteManager = new function () { async function deleteAllEmotes() { console.log("deleting emotes...") mainView.disabled = true; let emotesToBeRemoved = Module.EmoteListController.getEmotes().filter(emote => { return emote.uploaded === true; }); for (let i = 0; i < Module.EmoteListController.getEmotes().length; i++) { let emote = emotesToBeRemoved[i]; Module.MainController.updatesDeleteEmotesButton(`deleting ${i + 1}/${emotesToBeRemoved.length}`) await deleteEmote(emote); mainView.dispatchEvent(new CustomEvent('EMOTE_DELETED', {detail: {emote}})); console.log(`deleted {${emote.name}}`) } Module.MainController.updatesDeleteEmotesButton('Delete all uploaded emotes'); mainView.disabled = false; console.log("deleting complete...") } async function deleteEmote(emote) { await fetch(`https://kick.com/emotes/${emote.id}`, { method: 'DELETE', headers: getHeaders() }); } async function retrieveUploadedEmotes() { let response = await fetch('https://kick.com/emotes', { method: 'GET', headers: getHeaders() }) let json = await response.json(); let _emotes = json['emotes']; let emotes = []; for (let _emote of _emotes) { let emote = new Emote(); emote.id = _emote.id; emote.name = _emote.name; emote.uploaded = true; emote.source = Module.Utility.getEmoteURL(emote.id); emotes.push(emote); } return emotes; } async function uploadEmotes() { console.log(Module.EmoteListController.getEmotes()) let emotesToBeUploaded = Module.EmoteListController.getEmotes().filter(emote => { return emote.uploaded === false; }) console.log(`Uploading ${emotesToBeUploaded.length} emotes`) for (let i = 0; i < emotesToBeUploaded.length; i++) { let emote = emotesToBeUploaded[i]; let progressText = `Uploading ${i + 1}/${emotesToBeUploaded.length}`; Module.MainController.updateUploadButton(progressText) await uploadEmote(emote); } Module.MainController.updateUploadButton('Upload') } async function uploadEmote(emote) { let uploadData = await getUploadData(); await new Promise(function (resolve, reject) { let xhr = new XMLHttpRequest(); xhr.open('PUT', uploadData.url, true); let headers = uploadData.headers; for (let key in headers) { if (key.toLowerCase() === 'host') continue; xhr.setRequestHeader(key, headers[key]); } xhr.onload = function (e) { resolve() } xhr.send(emote.file); }) let response = await fetch('https://kick.com/emotes', { method: 'POST', body: JSON.stringify({ "uuid": uploadData.uuid, "key": uploadData.key, "name": emote.name, "subscribers_only": 1 }), headers: getHeaders() }) let json = await response.json(); console.log(json) if (json && json['id']) { emote.id = json['id']; emote.uploaded = true; } mainView.dispatchEvent(new CustomEvent('EMOTE_UPLOADED', {detail: {emote}})); } function getHeaders(accept, contentType) { return ({ 'Accept': accept || 'application/json, text/plain, */*', 'Content-Type': contentType || 'application/json', 'Accept-Encoding': 'gzip', "X-XSRF-TOKEN": getCookie('XSRF-TOKEN') }) } async function getUploadData() { let response = await fetch('/vapor/signed-storage-url', { method: 'POST', headers: getHeaders(), body: JSON.stringify({ "bucket": "", "content_type": "image/gif", "expires": "", "visibility": "public-read" }) }); return await response.json(); } function getCookie(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift()); } this.uploadEmotes = uploadEmotes; this.uploadEmote = uploadEmote; this.deleteAllEmotes = deleteAllEmotes; this.deleteEmote = deleteEmote; this.retrieveUploadedEmotes = retrieveUploadedEmotes; } Module.Initialize(); } setTimeout(e => { new Module(); }, 2000) })();