diff options
| author | ilotterytea <iltsu@alright.party> | 2025-12-10 12:49:54 +0500 |
|---|---|---|
| committer | ilotterytea <iltsu@alright.party> | 2025-12-10 12:49:54 +0500 |
| commit | 23da921c43b1c925710b55f93b87fef92dedd179 (patch) | |
| tree | 3775739383ef5dbe0656ada2677be6e998bddf2b | |
| parent | 0fd795e1d7374745e5df3be58f00ddb69a55e408 (diff) | |
feat: emote reupload
| -rw-r--r-- | emotes/reupload.php | 331 | ||||
| -rw-r--r-- | lib/config.php | 3 | ||||
| -rw-r--r-- | lib/partials.php | 11 |
3 files changed, 342 insertions, 3 deletions
diff --git a/emotes/reupload.php b/emotes/reupload.php new file mode 100644 index 0000000..b6598f9 --- /dev/null +++ b/emotes/reupload.php @@ -0,0 +1,331 @@ +<?php +include "{$_SERVER['DOCUMENT_ROOT']}/lib/accounts.php"; +include_once "{$_SERVER['DOCUMENT_ROOT']}/lib/config.php"; +include_once "{$_SERVER['DOCUMENT_ROOT']}/lib/alert.php"; +include_once "{$_SERVER['DOCUMENT_ROOT']}/lib/partials.php"; + +if (!CONFIG['reupload']['enable']) { + generate_alert("/404.php", "Emote reupload is disabled", 403); + exit; +} + +authorize_user(); +?> +<!DOCTYPE html> +<html> + +<head> + <title>Reupload emotes - <?= CONFIG['instance']['name'] ?></title> + <link rel="stylesheet" href="/static/style.css"> + <link rel="shortcut icon" href="/static/favicon.ico" type="image/x-icon"> +</head> + +<body> + <div class="container"> + <div class="wrapper"> + <?php html_navigation_bar() ?> + <?php display_alert() ?> + + <noscript>JavaScript is required.</noscript> + + <section class="content row"> + <section class="column small-gap" style="width:20em; max-width: 20em;"> + <section class="box"> + <div class="box navtab">Reupload Emote</div> + <form class="box content" id="reupload-form"> + <table class="vertical left"> + <tr> + <th>URL:</th> + <td><input type="text" name="url" required + placeholder="https://7tv.app/emotes/ABCDEF"></td> + </tr> + </table> + <button type="submit">Re-upload</button> + </form> + </section> + + <section class="box"> + <p> + This runs entirely on the client. + Confirm platform availability before starting. + <?php if (!CONFIG['emote']['urlupload']): ?> + <u>The process uses significant memory because images are loaded into RAM; large batches + can exceed several gigabytes.</u> + <?php endif; ?> + </p> + <p>Supported platforms: <b>7TV</b>, <b>BetterTTV</b></p> + </section> + + <section class="box" id="actions"> + </section> + </section> + <section class="column grow"> + <section class="box"> + <div class="box navtab"> + <p>Reuploaded emotes</p> + </div> + <div class="box content items flex" id="reuploaded-emotes"> + </div> + </section> + </section> + </section> + </div> + </div> +</body> + +<script> + async function uploadEmote(r) { + logAction(`Uploading ${r.name}...`); + const form = new FormData(); + + <?php if (CONFIG['emote']['urlupload']): ?> + form.append("file", r.url); + <?php else: ?> + // fetching image + const imageResponse = await fetch(r.url); + if (!imageResponse.ok) { + throw new Error(`Failed to fetch image: ${r.url} (${imageResponse})`); + } + let blob = await imageResponse.blob(); + let file = new File([blob], r.filename, { type: blob.type }); + form.append("file", file); + <?php endif; ?> + + form.append("visibility", "1"); + form.append("notes", r.notes); + form.append("source", r.source); + form.append("tos", "1"); + form.append("code", r.name); + + return await fetch("/emotes/upload.php", { + body: form, + method: "POST", + headers: { + Accept: "application/json" + } + }) + .then((r) => r.json()) + .then((r) => { + <?php if (!CONFIG['emote']['urlupload']): ?> + blob = null; + file = null; + <?php endif; ?> + return r; + }); + } + + function processEmote(provider, j) { + const id = j.id; + + const output = { + url: null, + filename: null, + name: null, + source: null, + notes: null + }; + + if (provider == "7tv") { + j.host.files.forEach((x) => { + if (x.name == "4x.gif") { + output.url = `https://cdn.7tv.app/emote/${id}/4x.gif`; + output.filename = x.name; + } else if (x.name == "4x.png") { + output.url = `https://cdn.7tv.app/emote/${id}/4x.png`; + output.filename = x.name; + } + }); + output.name = j.name; + output.source = `https://7tv.app/emotes/${id}`; + output.notes = `Reuploaded from ${output.source} - ${j.name}`; + if (j.owner) { + output.notes += ` by ${j.owner.username}`; + } + } else if (provider == "betterttv") { + output.name = j.code; + output.source = `https://betterttv.com/emotes/${id}`; + output.notes = `Reuploaded from ${output.source} - ${j.code}`; + if (j.user) { + output.notes += ` by ${j.user.name}`; + } + output.url = `https://cdn.betterttv.net/emote/${id}/3x.${j.imageType}`; + output.filename = `3x.${j.imageType}`; + } + + return output; + } + + function getEmotesByID(provider, host, id) { + let url = null; + switch (provider) { + case "7tv": + url = `https://7tv.io/v3/emotes/${id}`; + break; + case "betterttv": + url = `https://api.betterttv.net/3/emotes/${id}`; + break; + default: + break; + } + + return fetch(url) + .then((r) => { + if (!r.ok) { + throw new Error("Emotes not found!"); + } + return r.json(); + }) + .then((j) => { + logAction("Parsing emotes..."); + return [processEmote(provider, j)]; + }); + } + + function getEmotesByUser(provider, host, id) { + let url = null; + switch (provider) { + case "7tv": + url = `https://7tv.io/v3/users/${id}`; + break; + case "betterttv": + url = `https://api.betterttv.net/3/users/${id}`; + break; + default: + break; + } + + return fetch(url) + .then((r) => { + if (!r.ok) { + throw new Error("Emotes not found!"); + } + return r.json(); + }) + .then((j) => { + logAction("Parsing emotes..."); + const emotes = []; + if (provider == "7tv") { + j.connections.forEach((c) => { + c.emote_set.emotes.forEach((x) => { + emotes.push(processEmote(provider, x.data)); + }); + }); + } + else if (provider == "betterttv") { + [...j.sharedEmotes, ...j.channelEmotes].forEach((x) => { + emotes.push(processEmote(provider, x)); + }); + } + return emotes; + }); + } + + function displayEmote(data) { + const root = document.getElementById("reuploaded-emotes"); + const emote = document.createElement("a"); + emote.classList.add("box", "emote", "column", "justify-center", "items-center"); + emote.href = `/emotes/?id=${data.id}`; + + const imageWrapper = document.createElement("div"); + imageWrapper.classList.add("flex", "justify-center", "items-center", "grow", "emote-icon"); + emote.append(imageWrapper); + + const image = document.createElement("img"); + image.src = `/static/userdata/emotes/${data.id}/2x.webp`; + image.alt = data.code; + image.loading = "lazy"; + imageWrapper.append(image); + + const descWrapper = document.createElement("div"); + descWrapper.classList.add("flex", "column", "justify-bottom", "items-center", "emote-desc"); + emote.append(descWrapper); + + const name = document.createElement("h1"); + name.textContent = data.code; + name.title = data.code; + descWrapper.append(name); + + const author = document.createElement("p"); + author.textContent = data.uploaded_by.username; + descWrapper.append(author); + + root.append(emote); + } + + function logAction(text) { + const root = document.getElementById("actions"); + + const action = document.createElement("div"); + action.classList.add("small-gap", "row"); + + const content = document.createElement("p"); + content.innerHTML = text; + action.append(content); + + if (root.lastElementChild) { + root.removeChild(root.lastElementChild); + } + root.prepend(action); + } + + window.onload = () => { + const form = document.getElementById("reupload-form"); + let formBlocked = false; + + form.addEventListener("submit", (e) => { + e.preventDefault(); + if (formBlocked) { + alert('Please wait until the current work is completed!'); + return; + } + + formBlocked = true; + + const f = new FormData(form); + form.reset(); + + const url = new URL(f.get("url")); + let pathname = url.pathname; + if (pathname[0] == '/') { + pathname = pathname.substring(1); + } + const pathParts = pathname.split("/"); + + let emoteCount = 0; + let uploadedCount = 0; + + switch (url.host) { + case "betterttv.com": + case "7tv.app": + const p = url.host.split(".")[0]; + let func = null; + if (pathParts[0] == 'emotes') { + logAction(`Retrieving emote ID ${pathParts[1]} from ${p}...`); + func = () => getEmotesByID(p, url.origin, pathParts[1]); + } else if (pathParts[0] == 'users') { + logAction(`Retrieving user ID ${pathParts[1]} from ${p}...`); + func = () => getEmotesByUser(p, url.origin, pathParts[1]); + } + + func().then((r) => { + emoteCount = r.length; + r.forEach((x) => { + uploadEmote(x).then((x) => { + uploadedCount++; + displayEmote(x.data); + logAction(`Uploaded ${x.data.code} emote! (${uploadedCount}/${emoteCount})`); + if (uploadedCount >= emoteCount) { + formBlocked = false; + } + }); + }); + }); + break; + default: + break; + } + }); + }; +</script> + +</html>
\ No newline at end of file diff --git a/lib/config.php b/lib/config.php index b182d3a..fac6e02 100644 --- a/lib/config.php +++ b/lib/config.php @@ -28,6 +28,9 @@ $cfg = [ 'storeoriginal' => true, 'urlupload' => true ], + 'reupload' => [ + 'enable' => true + ], 'rating' => [ 'enable' => true, 'names' => "-1=COAL\n1=GEM", diff --git a/lib/partials.php b/lib/partials.php index 71a429a..2d6cd44 100644 --- a/lib/partials.php +++ b/lib/partials.php @@ -20,9 +20,14 @@ function html_navigation_bar() <a href="/users.php" class="button">Users</a> <?php endif; ?> - <?php if (CONFIG['emote']['upload'] && (CONFIG['anonymous']['upload'] || (isset($_SESSION["user_role"]) && $_SESSION["user_role"]["permission_upload"]))) { - echo '<a href="/emotes/upload.php" class="button">Upload</a>'; - } ?> + <?php if (CONFIG['emote']['upload'] && (CONFIG['anonymous']['upload'] || (isset($_SESSION["user_role"]) && $_SESSION["user_role"]["permission_upload"]))): ?> + <a href="/emotes/upload.php" class="button">Upload</a> + <?php endif; ?> + + <?php if (CONFIG['reupload']['enable'] && isset($_SESSION["user_role"]) && $_SESSION["user_role"]["permission_upload"]): ?> + <a href="/emotes/reupload.php" class="button">Re-upload</a> + <?php endif; ?> + <a href="/account" class="button">Account</a> <?php if (isset($_SESSION["user_id"])) { |
