// ==UserScript==
// @name Sharty Steganography Decoder
// @namespace janny - made by 4737, i love you bro
// @version 1.3
// @description Adds steganography decoding functionality to the sharty file approval queue and posts
// @author grok + claude
// @match https://soyjak.st/*
// @match https://www.soyjak.st/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const $ = window.jQuery || window.$;
// Shared with the modal opened from thread view, so both the
// file-approval scanner and the per-post button agree on which
// extensions are worth scanning.
const SUPPORTED_EXTENSIONS = ['.png', '.jpeg', '.jpg', '.webp'];
function hasSupportedExtension(url) {
const lower = url.toLowerCase();
return SUPPORTED_EXTENSIONS.some(ext => lower.endsWith(ext));
}
// ---------------------------------------------------------------------
// Shared canvas helpers (same logic previously duplicated inline in
// okaygoyim's slider/decode handlers, now used by both the file
// approval scanner AND the new in-thread popup modal). Because these
// always load from soyjak.st itself (same-origin), there's no CORS
// issue here, unlike the external gen-statis.github.io decoder page.
// ---------------------------------------------------------------------
function renderStegPreview(imgEl, sourceUrl, bits) {
try {
if (bits === 1) {
imgEl.src = imgEl.dataset.originalSrc || imgEl.src;
return;
}
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function() {
try {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
doUnhideImage(imageData, bits - 1);
ctx.putImageData(imageData, 0, 0);
imgEl.src = canvas.toDataURL();
} catch (err) {
console.error('Error processing image for steganography:', err);
}
};
img.onerror = function() {
console.error('Failed to load image for steganography:', sourceUrl);
};
img.src = sourceUrl;
} catch (err) {
console.error('Error in renderStegPreview:', err);
}
}
function decodeStegMessage(sourceUrl, onDone) {
try {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function() {
try {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const binaryMessage = [];
const pixel = imageData.data;
for (let i = 0, n = pixel.length; i < n; i += 4) {
for (let offset = 0; offset < 3; offset++) {
binaryMessage.push(pixel[i + offset] % 2);
}
}
let output = "";
for (let i = 0; i < binaryMessage.length; i += 8) {
if (i + 7 >= binaryMessage.length) break;
let c = 0;
for (let j = 0; j < 8; j++) {
c <<= 1;
c |= binaryMessage[i + j];
}
output += String.fromCharCode(c);
}
onDone(null, output);
} catch (err) {
onDone(err);
}
};
img.onerror = function() {
onDone(new Error('Failed to load image: ' + sourceUrl));
};
img.src = sourceUrl;
} catch (err) {
onDone(err);
}
}
function doUnhideImage(stegdata, bits) {
try {
const stegpix = stegdata.data;
const w = stegdata.width;
const h = stegdata.height;
for (let y = 0; y < h; y++) {
const stegy = y * w;
for (let x = 0; x < w; x++) {
const stegidx = 4 * (stegy + x);
// red
stegpix[stegidx] = (stegpix[stegidx] << (8 - bits)) & 0xff;
// green
stegpix[stegidx + 1] = (stegpix[stegidx + 1] << (8 - bits)) & 0xff;
// blue
stegpix[stegidx + 2] = (stegpix[stegidx + 2] << (8 - bits)) & 0xff;
}
}
} catch (err) {
console.error('Error in doUnhideImage function:', err);
}
}
// ---------------------------------------------------------------------
// File approval queue scanner (unchanged behavior, just now calling
// the shared helpers above instead of duplicated inline code)
// ---------------------------------------------------------------------
function okaygoyim(scope) {
try {
$(scope).find(".file-approval-item").each((i, item) => {
const file = $(item).find(".file-info")[0];
const thumbImg = $(item).find(".file-thumbnail img")[0];
const sourceLink = $(item).find("a[target='_blank']")[0];
if (!file || !thumbImg || !sourceLink) {
console.debug('Skipping item due to missing elements:', { file, thumbImg, sourceLink });
return;
}
const sourceUrl = sourceLink.href;
if (!hasSupportedExtension(sourceUrl)) {
console.debug('Skipping item due to unsupported extension:', sourceUrl);
return;
}
const existingP = $(file).find("p").filter((i, el) => $(el).text() === "Steganography Scan:").get(0);
const existingSlideContainer = file.querySelector(".slidecontainer");
const existingFlexDiv = file.querySelector("div[style*='flex']");
if (existingP && existingSlideContainer && existingFlexDiv) {
console.debug('Skipping item: elements already exist');
return;
}
const p = existingP || document.createElement("p");
if (!existingP) p.textContent = "Steganography Scan:";
const slideContainer = existingSlideContainer || document.createElement("div");
if (!existingSlideContainer) slideContainer.className = "slidecontainer";
const rangeInput = slideContainer.querySelector(".slider") || document.createElement("input");
if (!slideContainer.querySelector(".slider")) {
rangeInput.type = "range";
rangeInput.min = "1";
rangeInput.max = "8";
rangeInput.value = "1";
rangeInput.className = "slider";
rangeInput.style.width = "99%";
slideContainer.appendChild(rangeInput);
}
const flexDiv = existingFlexDiv || document.createElement("div");
if (!existingFlexDiv) {
flexDiv.style.display = "flex";
flexDiv.style.justifyContent = "center";
}
const decodeButton = flexDiv.querySelector(".decode-button") || document.createElement("input");
if (!flexDiv.querySelector(".decode-button")) {
decodeButton.className = "decode-button";
decodeButton.type = "button";
decodeButton.value = "Decode";
decodeButton.disabled = false;
flexDiv.appendChild(decodeButton);
}
if (!existingP) file.appendChild(p);
if (!existingSlideContainer) file.appendChild(slideContainer);
if (!existingFlexDiv) file.appendChild(flexDiv);
if (!thumbImg.dataset.originalSrc) {
thumbImg.dataset.originalSrc = thumbImg.src;
}
$(rangeInput).off("input").on("input", function() {
const bits = parseInt(this.value);
renderStegPreview(thumbImg, sourceLink.href, bits);
});
$(decodeButton).off("click").on("click", function() {
decodeButton.value = "Copying...";
decodeButton.disabled = true;
decodeStegMessage(sourceLink.href, (err, output) => {
decodeButton.value = "Decode";
decodeButton.disabled = false;
if (err) {
console.error('Error decoding image:', err);
return;
}
navigator.clipboard.writeText(output).then(() => {
alert("Decoded message copied to clipboard.");
}).catch(clipErr => {
alert("Failed to copy decoded message to clipboard.");
console.error('Clipboard error:', clipErr);
});
});
});
addStegLink(item);
});
} catch (err) {
console.error('Error in okaygoyim function:', err);
}
}
// ---------------------------------------------------------------------
// In-thread popup modal (new) — replaces the old link out to the
// external gen-statis.github.io page, which hit CORS errors because
// it had to fetch a cross-origin image into a canvas. This modal runs
// on soyjak.st itself, so the image fetch is same-origin and works
// exactly like the file-approval slider does.
// ---------------------------------------------------------------------
let modalEl = null;
function ensureModal() {
if (modalEl) return modalEl;
if (!$("#steg-modal-styles").length) {
$("<style>", { id: "steg-modal-styles" }).text(`
.steg-modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 99999;
display: none;
align-items: center;
justify-content: center;
}
.steg-modal-overlay.open { display: flex; }
.steg-modal-box {
background: #fff;
border-radius: 6px;
padding: 12px;
max-width: 90vw;
max-height: 90vh;
overflow: auto;
box-shadow: 0 4px 24px rgba(0,0,0,0.4);
text-align: center;
}
.steg-modal-box h3 {
margin: 0 0 8px 0;
font-size: 15px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.steg-modal-close {
cursor: pointer;
font-weight: bold;
border: none;
background: none;
font-size: 18px;
line-height: 1;
}
.steg-modal-img {
max-width: 70vw;
max-height: 60vh;
display: block;
margin: 0 auto 8px auto;
}
.steg-modal-status {
font-size: 12px;
color: #666;
min-height: 16px;
}
`).appendTo("head");
}
const overlay = $("<div>", { class: "steg-modal-overlay" });
const box = $("<div>", { class: "steg-modal-box" });
const header = $("<h3>").append(
$("<span>").text("Steganography Scan"),
$("<button>", { class: "steg-modal-close", type: "button" }).text("×")
);
const img = $("<img>", { class: "steg-modal-img" });
const slideContainer = $("<div>", { class: "slidecontainer" });
const rangeInput = $("<input>", { type: "range", min: "1", max: "8", value: "1", class: "slider" }).css("width", "99%");
slideContainer.append(rangeInput);
const flexDiv = $("<div>").css({ display: "flex", justifyContent: "center", gap: "8px" });
const decodeButton = $("<input>", { type: "button", value: "Decode", class: "decode-button" });
flexDiv.append(decodeButton);
const status = $("<div>", { class: "steg-modal-status" });
box.append(header, img, slideContainer, flexDiv, status);
overlay.append(box);
$("body").append(overlay);
function close() {
overlay.removeClass("open");
}
overlay.on("click", function(e) {
if (e.target === overlay[0]) close();
});
header.find(".steg-modal-close").on("click", close);
$(document).on("keydown", function(e) {
if (e.key === "Escape") close();
});
modalEl = { overlay, img: img[0], rangeInput: rangeInput[0], decodeButton: decodeButton[0], status: status[0], close };
return modalEl;
}
function openStegModal(sourceUrl) {
try {
const modal = ensureModal();
modal.img.dataset.originalSrc = sourceUrl;
modal.img.src = sourceUrl;
modal.rangeInput.value = "1";
modal.status.textContent = "";
$(modal.rangeInput).off("input").on("input", function() {
const bits = parseInt(this.value);
renderStegPreview(modal.img, sourceUrl, bits);
});
$(modal.decodeButton).off("click").on("click", function() {
modal.decodeButton.value = "Copying...";
modal.decodeButton.disabled = true;
modal.status.textContent = "Decoding...";
decodeStegMessage(sourceUrl, (err, output) => {
modal.decodeButton.value = "Decode";
modal.decodeButton.disabled = false;
if (err) {
modal.status.textContent = "Failed to decode image.";
console.error('Error decoding image:', err);
return;
}
navigator.clipboard.writeText(output).then(() => {
modal.status.textContent = "Copied to clipboard.";
}).catch(clipErr => {
modal.status.textContent = "Failed to copy to clipboard.";
console.error('Clipboard error:', clipErr);
});
});
});
modal.overlay.addClass("open");
} catch (err) {
console.error('Error in openStegModal:', err);
}
}
function addStegLink(item) {
try {
const url = window.location.href;
if (!url.match(/^https:\/\/(www\.)?soyjak\.st\/mod\.php\?\/.*/)) {
return;
}
const fileInfoP = $(item).find(".fileinfo");
if (fileInfoP.length) {
fileInfoP.each((i, p) => {
if ($(p).find("a.steg-link").length) {
return;
}
const downloadLink = $(p).find("a.filename-download-link");
const imgOpsLink = $(p).find("a[target='_blank']");
if (downloadLink.length && imgOpsLink.length) {
const imageSource = downloadLink.attr("href");
if (!imageSource || !hasSupportedExtension(imageSource)) {
console.debug('Skipping steg link due to unsupported extension:', imageSource);
return;
}
const baseUrl = url.startsWith("https://www.soyjak.st") ? "https://www.soyjak.st" : "https://soyjak.st";
const fullImageUrl = imageSource.startsWith("http") ? imageSource : `${baseUrl}${imageSource}`;
const stegLink = $("<a>")
.addClass("steg-link")
.attr("href", "#")
.text("[Steganography]")
.css("margin-left", "5px")
.on("click", function(e) {
e.preventDefault();
openStegModal(fullImageUrl);
});
imgOpsLink.after(stegLink);
}
});
}
} catch (err) {
console.error('Error in addStegLink function:', err);
}
}
try {
addStegLink(document);
$(document).on("new_post", (e, post) => {
try {
addStegLink(post);
} catch (err) {
console.error('Error in new_post handler:', err);
}
});
const url = window.location.href;
if (url.match(/^https:\/\/(www\.)?soyjak\.st\/mod\.php\?\/file_approval$/)) {
okaygoyim(document);
// Reduce polling frequency to 500ms to lower performance impact
window.setInterval(() => {
try {
okaygoyim(document);
} catch (err) {
console.error('Error in interval handler:', err);
}
}, 500);
}
} catch (err) {
console.error('Error in main script execution:', err);
}
})();13 views