Browse Source

Implement hidden catbox embedding

pull/46/head
coomdev 2 years ago
parent
commit
be39da31f8
  1. 2
      main.meta.js
  2. 1660
      main.user.js
  3. 137
      src/main.ts
  4. 30
      src/pngv3.ts
  5. 7
      src/requests.ts
  6. 102
      src/utils.ts

2
main.meta.js

@ -1,7 +1,7 @@
// ==UserScript==
// @name PNGExtraEmbed
// @namespace https://coom.tech/
// @version 0.120
// @version 0.122
// @description uhh
// @author You
// @match https://boards.4channel.org/*

1660
main.user.js

File diff suppressed because it is too large

137
src/main.ts

@ -3,6 +3,7 @@ import { appState, settings } from "./stores";
import globalCss from './global.css';
import png from "./png";
import pngv3 from "./pngv3";
import webm from "./webm";
import gif from "./gif";
import thirdeye from "./thirdeye";
@ -16,6 +17,8 @@ import SettingsButton from './SettingsButton.svelte';
//import Embedding from './Embedding.svelte';
import Embeddings from './Embeddings.svelte';
import EyeButton from './EyeButton.svelte';
import { buildPeeFile, fireNotification } from "./utils";
import { fileTypeFromBuffer } from "file-type";
export interface ImageProcessor {
skip?: true;
@ -27,12 +30,12 @@ export interface ImageProcessor {
export let csettings: Parameters<typeof settings['set']>[0];
let processors: ImageProcessor[] =
[thirdeye, pomf, png, webm, gif];
[thirdeye, pomf, png, pngv3, webm, gif];
let cappState: Parameters<typeof appState['set']>[0];
settings.subscribe(b => {
csettings = b;
processors = [...(!csettings.te ? [thirdeye] : []), png, pomf, webm, gif
processors = [...(!csettings.te ? [thirdeye] : []), png, pngv3, pomf, webm, gif
];
});
@ -269,27 +272,15 @@ document.addEventListener('QRDialogCreation', <any>((e: CustomEvent<HTMLElement>
const proc = processors.filter(e => e.inject).find(e => e.match(file.name));
if (!proc)
throw new Error("Container filetype not supported");
const buff = await proc.inject!(file, [...input.files]);
const buff = await proc.inject!(file, [...input.files].slice(0, 5));
document.dispatchEvent(new CustomEvent('QRSetFile', {
//detail: { file: new Blob([buff]), name: file.name, type: file.type }
detail: { file: new Blob([buff], { type }), name: file.name }
}));
document.dispatchEvent(new CustomEvent("CreateNotification", {
detail: {
type: 'success',
content: `File${input.files.length > 1 ? 's' : ''} successfully embedded!`,
lifetime: 3
}
}));
fireNotification('success', `File${input.files.length > 1 ? 's' : ''} successfully embedded!`);
} catch (err) {
const e = err as Error;
document.dispatchEvent(new CustomEvent("CreateNotification", {
detail: {
type: 'error',
content: "Couldn't embed file: " + e.message,
lifetime: 3
}
}));
fireNotification('error', "Couldn't embed file: " + e.message);
}
}
});
@ -377,36 +368,82 @@ function processAttachments(post: HTMLDivElement, ress: [EmbeddedFile, boolean][
post.setAttribute('data-processed', "true");
}
//if ((window as any)['pagemode']) {
// onload = () => {
// console.log("loaded");
// const resbuf = async (s: EmbeddedFile['data']) => Buffer.isBuffer(s) ? s : await s();
// const container = document.getElementById("container") as HTMLInputElement;
// const injection = document.getElementById("injection") as HTMLInputElement;
// container.onchange = injection.onchange = async () => {
// console.log('eval changed');
// if (container.files?.length && injection.files?.length) {
// const dlr = document.getElementById("dlr") as HTMLAnchorElement;
// const dle = document.getElementById("dle") as HTMLAnchorElement;
// console.log(buf(new Uint8Array(await container.files[0].arrayBuffer())));
// console.log(buf(new Uint8Array(await injection.files[0].arrayBuffer())));
// const res = await gif.inject!(container.files[0], injection.files[0]);
// console.log('inj done', buf(res));
// const result = document.getElementById("result") as HTMLImageElement;
// const extracted = document.getElementById("extracted") as HTMLImageElement;
// const res2 = new Blob([res], { type: (await fileTypeFromBuffer(res))?.mime });
// result.src = URL.createObjectURL(res2);
// dlr.href = result.src;
// console.log('url created');
// const embedded = await gif.extract(res);
// console.log(buf(new Uint8Array(await resbuf(embedded.data))));
// if (!embedded) {
// debugger;
// return;
// }
// extracted.src = URL.createObjectURL(new Blob([await resbuf(embedded.data!)]));
// dle.href = extracted.src;
// }
// };
// };
//}
function parseForm(data: object) {
const form = new FormData();
Object.entries(data)
.filter(([key, value]) => value !== null)
.map(([key, value]) => form.append(key, value));
return form;
}
if ((window as any)['pagemode']) {
onload = () => {
const resbuf = async (s: EmbeddedFile['data']) => typeof s != "string" && (Buffer.isBuffer(s) ? s : await s());
const container = document.getElementById("container") as HTMLInputElement;
const injection = document.getElementById("injection") as HTMLInputElement;
container.onchange = async () => {
const ret = await fetch("https://catbox.moe/user/api.php", {
method: 'POST',
body: parseForm({
reqtype: 'fileupload',
fileToUpload: container.files![0]
})
});
console.log(ret);
console.log(await ret.text());
};
};
}
if ((window as any)['pagemode']) {
onload = () => {
const extraction = document.getElementById("extraction") as HTMLInputElement;
/* extraction.onchange = async () => {
const pee = await buildPeeFile(extraction.files![0]);
const dlr = document.getElementById("dlr") as HTMLAnchorElement;
dlr.href = URL.createObjectURL(pee);
};*/
document.addEventListener("CreateNotification", (e: any) => console.log(e.detail));
console.log("loaded");
//const resbuf = async (s: any) => ((Buffer.isBuffer(s) ? s : await s()));
const container = document.getElementById("container") as HTMLInputElement;
const injection = document.getElementById("injection") as HTMLInputElement;
injection.multiple = true;
extraction.onchange = async () => {
const embedded = await pngv3.extract(Buffer.from(await extraction.files![0].arrayBuffer()));
const d = document.createElement('div');
new Embeddings({
target: d,
props: {files: embedded}
});
document.body.append(d);
console.log(embedded);
};
container.onchange = injection.onchange = async () => {
console.log('eval changed');
if (container.files?.length && injection.files?.length) {
const dlr = document.getElementById("dlr") as HTMLAnchorElement;
//const dle = document.getElementById("dle") as HTMLAnchorElement;
const res = await pngv3.inject!(container.files[0], [...injection.files]);
const result = document.getElementById("result") as HTMLImageElement;
//const extracted = document.getElementById("extracted") as HTMLImageElement;
const res2 = new Blob([res], { type: (await fileTypeFromBuffer(res))?.mime });
result.src = URL.createObjectURL(res2);
dlr.href = result.src;
console.log('url created');
//const embedded = await pngv3.extract(res);
//if (!embedded) {
// debugger;
return;
//}
//extracted.src = URL.createObjectURL(new Blob([await resbuf(embedded.data!)]));
//dle.href = extracted.src;
}
};
};
}

30
src/pngv3.ts

@ -2,7 +2,8 @@ import { buf } from "crc-32";
import { Buffer } from "buffer";
import type { ImageProcessor } from "./main";
import { PNGDecoder, PNGEncoder } from "./png";
import { decodeCoom3Payload } from "./utils";
import { buildPeeFile, decodeCoom3Payload, fireNotification } from "./utils";
import { GM_fetch } from "./requests";
const CUM3 = Buffer.from("CUM\0" + "3");
@ -74,11 +75,35 @@ export const BufferWriteStream = () => {
return [ret, () => b] as [WritableStream<Buffer>, () => Buffer];
};
function parseForm(data: object) {
const form = new FormData();
Object.entries(data)
.filter(([key, value]) => value !== null)
.map(([key, value]) => form.append(key, value));
return form;
}
const inject = async (container: File, injs: File[]) => {
const [writestream, extract] = BufferWriteStream();
const encoder = new PNGEncoder(writestream);
const decoder = new PNGDecoder(container.stream().getReader());
let total = 0;
fireNotification('info', `Uploading ${injs.length} files...`);
const links = await Promise.all(injs.map(async inj => {
const ret = (await GM_fetch("https://catbox.moe/user/api.php", {
method: 'POST',
body: parseForm({
reqtype: 'fileupload',
fileToUpload: await buildPeeFile(inj)
})
})).text();
fireNotification('info', `Uploaded files [${++total}/${injs.length}]`);
return ret;
}));
let magic = false;
for await (const [name, chunk, crc, offset] of decoder.chunks()) {
if (magic && name != "IDAT")
@ -89,8 +114,7 @@ const inject = async (container: File, injs: File[]) => {
}
await encoder.insertchunk([name, chunk, crc, offset]);
}
const injb = Buffer.alloc(4);
// TODO
const injb = Buffer.from(links.join('\0'));
await encoder.insertchunk(["IDAT", buildChunk("IDAT", injb), 0, 0]);
await encoder.insertchunk(["IEND", buildChunk("IEND", Buffer.from([])), 0, 0]);
return extract();

7
src/requests.ts

@ -33,7 +33,7 @@ export function GM_head(...[url, opt]: Parameters<typeof fetch>) {
});
}
export function GM_fetch(...[url, opt, lisn]: [...Parameters<typeof fetch>, EventTarget?]) {
export let GM_fetch = (...[url, opt, lisn]: [...Parameters<typeof fetch>, EventTarget?]) => {
function blobTo(to: string, blob: Blob) {
if (to == "arrayBuffer" && blob.arrayBuffer)
return blob.arrayBuffer();
@ -81,7 +81,10 @@ export function GM_fetch(...[url, opt, lisn]: [...Parameters<typeof fetch>, Even
};
xmlhttprequest(gmopt);
});
}
};
if ((window as any)['pagemode'])
GM_fetch = fetch as any;
const makePoolable = <T extends any[], U>(fun: (...args: T) => Promise<U>, getPoolSize: () => number) => {
const pool = [];

102
src/utils.ts

@ -3,6 +3,68 @@ import { GM_fetch, headerStringToObject } from "./requests";
import thumbnail from "./assets/hasembed.png";
import type { EmbeddedFile } from './main';
const generateThumbnail = async (f: File): Promise<Buffer> => {
const can = document.createElement("canvas");
can.width = 125;
can.height = 125;
const ctx = can.getContext("2d");
if (!ctx)
return Buffer.alloc(0);
const [sw, sh] = [125, 125];
const url = URL.createObjectURL(f);
if (f.type.startsWith("image")) {
const imgElem = document.createElement('img');
imgElem.src = url;
await new Promise(_ => imgElem.onload = _);
const [iw, ih] = [imgElem.naturalWidth, imgElem.naturalHeight];
const scale = Math.min(1, sw / iw, sh / ih);
const dims = [~~(iw * scale), ~~(ih * scale)] as [number, number];
ctx.drawImage(imgElem, 0, 0, dims[0], dims[1]);
} else if (f.type.startsWith("video")) {
const vidElem = document.createElement('video');
vidElem.src = url;
await new Promise(_ => vidElem.onloadedmetadata = _);
const [iw, ih] = [vidElem.videoWidth, vidElem.videoHeight];
const scale = Math.min(1, sw / iw, sh / ih);
const dims = [~~(iw * scale), ~~(ih * scale)] as [number, number];
ctx.drawImage(vidElem, 0, 0, dims[0], dims[1]);
} else
return Buffer.alloc(0);
const blob = await new Promise<Blob | null>(_ => can.toBlob(_, "image/jpg"));
if (!blob)
return Buffer.alloc(0);
return new Buffer(await blob.arrayBuffer());
};
export const buildPeeFile = async (f: File) => {
//const isMemeBrowser = navigator.userAgent.indexOf("Chrome") == -1;
let thumbnail = Buffer.alloc(0);
thumbnail = await generateThumbnail(f);
const namebuf = Buffer.from(f.name);
const ret = Buffer.alloc(4 /* Magic */ +
1 /* Flags */ + namebuf.byteLength + 1 +
(4 + thumbnail.byteLength) /* TSize + Thumbnail */ +
f.size /*Teh file*/);
let ptr = 0;
ret.write('PEE\0', 0);
ptr += 4;
ret[ptr++] = 1 | ((+(thumbnail.length != 0)) << 2);
namebuf.copy(ret, ptr);
ptr += namebuf.byteLength;
ret[ptr++] = 0;
if (thumbnail.length > 0) {
ret.writeInt32LE(thumbnail.byteLength, ptr);
ptr += 4;
thumbnail.copy(ret, ptr);
ptr += thumbnail.byteLength;
}
new Buffer(await f.arrayBuffer()).copy(ret, ptr);
return new Blob([ret]);
};
/*
header (must be < 2k): [1 byte bitfield](if hasfilename: null terminated string)(if has tags: [X null terminated string, tags are whitespace-separated])
(if has thumbnail: [thumbnail size X]
@ -12,40 +74,58 @@ rest: [X bytes of thumbnail data])[file bytes]
&4 => has thumbnail
*/
export const decodeCoom3Payload = async (buff: Buffer) => {
const pees = buff.toString().split('\0');
const pees = buff.toString().split('\0').slice(0, 5);
return Promise.all(pees.map(async pee => {
const res = await GM_fetch(pee, { headers: { ranges: 'bytes=0-2048' } });
const res = await GM_fetch(pee, {
headers: { ranges: 'bytes=0-2048' },
mode: 'cors',
referrerPolicy: 'no-referrer',
});
const size = +(res.headers.get('content-size') || 0);
const header = Buffer.from(await res.arrayBuffer());
const flags = header[0];
const hasFn = flags & 1;
const hasTags = flags & 2;
const hasThumbnail = flags & 4;
let [ptr, ptr2] = [1, 1];
let hptr = 0;
if (header.slice(0, 4).toString() == "PEE\0")
hptr += 4;
const flags = header[hptr];
const hasFn = !!(flags & 1);
const hasTags = !!(flags & 2);
const hasThumbnail = !!(flags & 4);
let [ptr, ptr2] = [hptr + 1, hptr + 1];
let fn = 'embedded';
let tags = [];
let thumb: EmbeddedFile['thumbnail'] = Buffer.from(thumbnail);
if (hasFn) {
while (buff[ptr2] != 0)
while (header[ptr2] != 0)
ptr2++;
fn = header.slice(ptr, ptr2).toString();
ptr = ++ptr2;
}
if (hasTags) {
while (buff[ptr2] != 0)
while (header[ptr2] != 0)
ptr2++;
tags = header.slice(ptr, ptr2).toString().split(/\s+/);
}
let thumbsize = 0;
if (hasThumbnail) {
thumbsize = header.readInt32LE(ptr);
thumb = Buffer.from(await (await GM_fetch(pee, { headers: { range: `bytes: ${ptr + 4}-${ptr + 4 + thumbsize}` } })).arrayBuffer());
console.log("Thumbnail size of ", thumbsize);
thumb = Buffer.from(await (await GM_fetch(pee, { headers: { range: `bytes=${ptr + 4}-${ptr + 4 + thumbsize}` } })).arrayBuffer());
}
return {
filename: fn,
data: async (lsn) =>
Buffer.from(await (await GM_fetch(pee, { headers: { range: `bytes: ${ptr + 4 + thumbsize}-${size-1}` } }, lsn)).arrayBuffer()),
Buffer.from(await (await GM_fetch(pee, { headers: { range: `bytes=${ptr + 4 + thumbsize}-${size - 1}` } }, lsn)).arrayBuffer()),
thumbnail: thumb,
} as EmbeddedFile;
}));
};
export const fireNotification = (level: 'success' | 'error' | 'info' | 'warning', text: string, lifetime = 3) => {
document.dispatchEvent(new CustomEvent("CreateNotification", {
detail: {
type: level,
content: text,
lifetime
}
}));
};
Loading…
Cancel
Save