PEE/src/main.ts

364 lines
14 KiB
TypeScript
Raw Permalink Normal View History

2021-12-22 20:05:17 +00:00
import { Buffer } from "buffer";
2022-01-07 04:43:28 +00:00
import { appState, settings } from "./stores";
2022-01-06 05:59:52 +00:00
import globalCss from './global.css';
2022-01-02 14:37:19 +00:00
2022-01-05 01:14:23 +00:00
import png from "./png";
import webm from "./webm";
import gif from "./gif";
import thirdeye from "./thirdeye";
2022-01-03 22:29:28 +00:00
2022-01-04 15:36:43 +00:00
import { GM_fetch, GM_head, headerStringToObject } from "./requests";
2022-01-04 15:36:43 +00:00
import App from "./App.svelte";
2022-01-07 04:43:28 +00:00
import ScrollHighlighter from "./ScrollHighlighter.svelte";
2022-01-04 15:36:43 +00:00
import SettingsButton from './SettingsButton.svelte';
2022-01-04 20:26:05 +00:00
import Embedding from './Embedding.svelte';
2022-01-05 20:50:44 +00:00
import EyeButton from './EyeButton.svelte';
import { fileTypeFromBuffer } from "file-type";
import { buf } from "crc-32";
2022-01-05 01:14:23 +00:00
export interface ImageProcessor {
skip?: true;
match(fn: string): boolean;
has_embed(b: Buffer, fn?: string): boolean | Promise<boolean>;
extract(b: Buffer, fn?: string): EmbeddedFile | Promise<EmbeddedFile>;
inject?(b: File, c: File): Buffer | Promise<Buffer>;
}
2022-01-04 15:36:43 +00:00
let csettings: any;
2022-01-05 01:14:23 +00:00
let processors: ImageProcessor[] =
[thirdeye, png, webm, gif];
2022-01-07 06:45:30 +00:00
let cappState: Parameters<typeof appState['set']>[0];
2022-01-05 01:14:23 +00:00
settings.subscribe(b => {
csettings = b;
processors = [...(!csettings.te ? [thirdeye] : []), png, webm, gif
2022-01-05 01:14:23 +00:00
];
});
2021-12-25 01:50:40 +00:00
2022-01-07 06:45:30 +00:00
appState.subscribe(v => {
cappState = v;
});
2022-01-04 15:36:43 +00:00
// most pngs are encoded with 65k idat chunks
async function* streamRemote(url: string, chunkSize = 16 * 1024, fetchRestOnNonCanceled = true) {
const headers = await GM_head(url);
const h = headerStringToObject(headers);
const size = +h['content-length'];
let ptr = 0;
let fetchSize = chunkSize;
while (ptr != size) {
const res = await GM_fetch(url, { headers: { range: `bytes=${ptr}-${ptr + fetchSize - 1}` } }) as any as Tampermonkey.Response<any>;
const obj = headerStringToObject(res.responseHeaders);
2022-01-04 15:36:43 +00:00
if (!('content-length' in obj)) {
console.warn("no content lenght???", url);
break;
} const len = +obj['content-length'];
ptr += len;
if (fetchRestOnNonCanceled)
fetchSize = size;
2022-01-04 15:36:43 +00:00
const val = Buffer.from(await (res as any).arrayBuffer());
const e = (yield val) as boolean;
if (e) {
break;
}
}
2022-01-04 15:36:43 +00:00
//console.log("streaming ended, ", ptr, size);
}
2022-01-05 01:14:23 +00:00
type EmbeddedFileWithPreview = {
2022-01-07 04:43:28 +00:00
page?: string; // can be a booru page
source?: string; // can be like a twitter post this was posted in originally
2022-01-05 01:14:23 +00:00
thumbnail: Buffer;
filename: string;
data: (lisn?: EventTarget) => Promise<Buffer>;
2022-01-05 01:14:23 +00:00
};
2021-12-22 20:05:17 +00:00
2022-01-05 01:14:23 +00:00
type EmbeddedFileWithoutPreview = {
2022-01-07 04:43:28 +00:00
page: undefined;
source: undefined;
2022-01-05 01:14:23 +00:00
thumbnail: undefined;
filename: string;
data: Buffer;
};
export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview;
const processImage = async (src: string, fn: string, hex: string): Promise<[EmbeddedFile, boolean] | undefined> => {
2022-01-05 01:14:23 +00:00
const proc = processors.find(e => e.match(fn));
2022-01-01 18:52:50 +00:00
if (!proc)
2021-12-22 20:05:17 +00:00
return;
2022-01-05 01:14:23 +00:00
if (proc.skip) {
// skip file downloading, file is referenced from the filename
// basically does things like filtering out blacklisted tags
const md5 = Buffer.from(hex, 'base64');
if (await proc.has_embed(md5, fn) === true)
return [await proc.extract(md5, fn), true];
2022-01-05 01:14:23 +00:00
return;
}
const iter = streamRemote(src);
2022-01-04 15:36:43 +00:00
if (!iter)
return;
let cumul = Buffer.alloc(0);
let found: boolean | undefined;
let chunk: ReadableStreamDefaultReadResult<Buffer> = { done: true };
do {
const { value, done } = await iter.next(found === false);
if (done) {
chunk = { done: true } as ReadableStreamDefaultReadDoneResult;
} else {
chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>;
}
if (!done)
cumul = Buffer.concat([cumul, value!]);
2022-01-05 01:14:23 +00:00
found = await proc.has_embed(cumul);
2022-01-04 15:36:43 +00:00
} while (found !== false && !chunk.done);
await iter.next(false);
if (found === false) {
//console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`);
2021-12-22 20:05:17 +00:00
return;
2022-01-04 15:36:43 +00:00
}
2022-01-05 20:50:44 +00:00
return [await proc.extract(cumul), false];
2021-12-22 20:05:17 +00:00
};
2022-01-02 13:12:19 +00:00
const textToElement = <T = HTMLElement>(s: string) =>
document.createRange().createContextualFragment(s).children[0] as any as T;
2022-01-01 18:52:50 +00:00
const processPost = async (post: HTMLDivElement) => {
const thumb = post.querySelector("a.fileThumb") as HTMLAnchorElement;
const origlink = post.querySelector('.file-info > a[target*="_blank"]') as HTMLAnchorElement;
2022-01-03 22:29:28 +00:00
if (!thumb || !origlink)
2021-12-22 20:05:17 +00:00
return;
const res2 = await processImage(origlink.href,
(origlink.querySelector('.fnfull') || origlink).textContent || '',
post.querySelector("[data-md5]")?.getAttribute('data-md5') || '');
2022-01-05 20:50:44 +00:00
if (!res2)
2021-12-22 20:05:17 +00:00
return;
2022-01-05 20:50:44 +00:00
const [res, external] = res2;
2022-01-01 18:52:50 +00:00
const replyBox = post.querySelector('.post');
2022-01-05 20:50:44 +00:00
if (external)
replyBox?.classList.add('hasext');
2022-01-07 06:45:30 +00:00
else
replyBox?.classList.add('hasembed');
if (!cappState.foundPosts.includes(replyBox as HTMLElement))
cappState.foundPosts.push(replyBox as HTMLElement);
appState.set(cappState);
2022-01-05 15:56:45 +00:00
const isCatalog = replyBox?.classList.contains('catalog-post');
2021-12-22 20:05:17 +00:00
// add buttons
2022-01-05 15:56:45 +00:00
if (!isCatalog) {
const ft = post.querySelector('div.file') as HTMLDivElement;
const info = post.querySelector("span.file-info") as HTMLSpanElement;
const filehost: HTMLElement | null = ft.querySelector('.filehost');
const eyehost: HTMLElement | null = info.querySelector('.eyehost');
const imgcont = filehost || document.createElement('div');
const eyecont = eyehost || document.createElement('span');
2022-01-05 15:56:45 +00:00
if (!filehost) {
ft.append(imgcont);
2022-01-05 15:56:45 +00:00
imgcont.classList.add("fileThumb");
imgcont.classList.add("filehost");
2022-01-05 15:56:45 +00:00
} else {
imgcont.innerHTML = '';
}
if (!eyehost) {
info.append(eyecont);
eyecont.classList.add("eyehost");
} else {
2022-01-05 20:50:44 +00:00
eyecont.innerHTML = '';
2022-01-05 15:56:45 +00:00
}
2022-01-05 20:50:44 +00:00
const id = ~~(Math.random() * 20000000);
2022-01-07 04:43:28 +00:00
const emb = new Embedding({
2022-01-05 15:56:45 +00:00
target: imgcont,
props: {
2022-01-05 20:50:44 +00:00
file: res,
id: '' + id
}
});
new EyeButton({
target: eyecont,
props: {
2022-01-05 22:20:20 +00:00
file: res,
2022-01-07 04:43:28 +00:00
inst: emb,
2022-01-05 20:50:44 +00:00
id: '' + id
2022-01-05 15:56:45 +00:00
}
});
} else {
2022-01-05 15:56:45 +00:00
const opFile = post.querySelector('.catalog-link');
2022-01-05 20:50:44 +00:00
const ahem = opFile?.querySelector('.catalog-host');
const imgcont = ahem || document.createElement('div');
imgcont.className = "catalog-host";
if (ahem) {
imgcont.innerHTML = '';
}
2022-01-05 15:56:45 +00:00
const emb = new Embedding({
target: imgcont,
props: {
file: res
}
});
2022-01-05 20:50:44 +00:00
if (!ahem)
2022-01-06 00:10:12 +00:00
opFile?.append(imgcont);
}
2021-12-22 20:05:17 +00:00
2022-01-04 20:26:05 +00:00
post.setAttribute('data-processed', "true");
2022-01-01 18:52:50 +00:00
};
2021-12-22 20:05:17 +00:00
const startup = async () => {
2022-01-03 22:29:28 +00:00
await Promise.all([...document.querySelectorAll('.postContainer')].filter(e => e.textContent?.includes("191 KB")).map(e => processPost(e as any)));
2021-12-23 01:19:08 +00:00
2022-01-02 05:00:28 +00:00
// Basically this is a misnommer: fires even when inlining existings posts, also posts are inlined through some kind of dom projection
2022-01-03 22:29:28 +00:00
document.addEventListener('ThreadUpdate', <any>(async (e: CustomEvent<any>) => {
2022-01-04 15:36:43 +00:00
const newPosts = e.detail.newPosts;
2022-01-03 22:29:28 +00:00
for (const post of newPosts) {
2022-01-04 15:36:43 +00:00
const postContainer = document.getElementById("pc" + post.substring(post.indexOf(".") + 1)) as HTMLDivElement;
2022-01-03 22:29:28 +00:00
processPost(postContainer);
}
}));
2022-01-02 05:00:28 +00:00
2022-01-04 15:36:43 +00:00
// keep this to handle posts getting inlined
2022-01-02 05:00:28 +00:00
const mo = new MutationObserver(reco => {
for (const rec of reco)
if (rec.type == "childList")
rec.addedNodes.forEach(e => {
2022-01-02 07:30:38 +00:00
if (!(e instanceof HTMLElement))
return;
// apparently querySelector cannot select the root element if it matches
let el = (e as any).querySelectorAll('.postContainer:not([class*="noFile"])');
if (!el && e.classList.contains('postContainer'))
el = e;
2022-01-02 05:00:28 +00:00
if (el)
2022-01-02 07:30:38 +00:00
[...el].map(el => processPost(el as any));
2022-01-02 05:00:28 +00:00
});
});
2022-01-02 07:30:38 +00:00
document.querySelectorAll('.board').forEach(e => {
mo.observe(e!, { childList: true, subtree: true });
});
const posts = [...document.querySelectorAll('.postContainer:not([class*="noFile"])')];
2022-01-02 13:12:19 +00:00
const scts = document.getElementById('shortcuts');
const button = textToElement(`<span></span>`);
2022-01-04 15:36:43 +00:00
const settingsButton = new SettingsButton({
2022-01-02 13:12:19 +00:00
target: button
});
scts?.appendChild(button);
2021-12-22 20:05:17 +00:00
2022-01-04 15:36:43 +00:00
const appHost = textToElement(`<div class="pee-settings"></div>`);
const appInstance = new App({ target: appHost });
document.body.append(appHost);
2022-01-07 07:11:37 +00:00
2022-01-07 04:43:28 +00:00
const scrollHost = textToElement(`<div class="pee-scroll"></div>`);
new ScrollHighlighter({ target: scrollHost });
2022-01-07 07:11:37 +00:00
document.body.append(scrollHost);
2022-01-07 04:43:28 +00:00
2022-01-07 06:45:30 +00:00
appState.set({
...cappState,
isCatalog: !!document.querySelector('.catalog-small'),
});
2022-01-02 13:12:19 +00:00
await Promise.all(posts.map(e => processPost(e as any)));
2021-12-22 20:05:17 +00:00
};
2022-01-02 14:37:19 +00:00
const getSelectedFile = () => {
return new Promise<File>(res => {
document.addEventListener('QRFile', e => res((e as any).detail), { once: true });
document.dispatchEvent(new CustomEvent('QRGetFile'));
});
};
2022-01-04 15:36:43 +00:00
2021-12-22 20:05:17 +00:00
document.addEventListener('4chanXInitFinished', startup);
2022-01-02 14:37:19 +00:00
document.addEventListener('QRDialogCreation', <any>((e: CustomEvent<string>) => {
const target = e.target as HTMLDivElement;
const bts = target.querySelector('#qr-filename-container');
const i = document.createElement('i');
i.className = "fa fa-magnet";
const a = document.createElement('a');
a.appendChild(i);
a.title = "Embed File (Select a file before...)";
bts?.appendChild(a);
a.onclick = async (e) => {
const file = await getSelectedFile();
if (!file)
return;
const input = document.createElement('input') as HTMLInputElement;
input.setAttribute("type", "file");
const type = file.type;
input.onchange = (async ev => {
if (input.files) {
try {
2022-01-05 01:14:23 +00:00
const proc = processors.find(e => e.match(file.name));
2022-01-02 14:37:19 +00:00
if (!proc)
throw new Error("Container filetype not supported");
2022-01-05 01:14:23 +00:00
if (!proc.inject)
return;
const buff = await proc.inject(file, input.files[0]);
2022-01-02 14:37:19 +00:00
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 successfully embedded!',
lifetime: 3
}
}));
} catch (err) {
const e = err as Error;
document.dispatchEvent(new CustomEvent("CreateNotification", {
detail: {
type: 'error',
content: "Couldn't embed file: " + e.message,
lifetime: 3
}
}));
}
}
});
input.click();
};
}), { once: true });
2022-01-01 18:52:50 +00:00
const customStyles = document.createElement('style');
2022-01-04 16:17:22 +00:00
2022-01-06 05:59:52 +00:00
customStyles.appendChild(document.createTextNode(globalCss));
2022-01-02 13:12:19 +00:00
2022-01-01 18:52:50 +00:00
document.documentElement.insertBefore(customStyles, null);
2022-01-04 16:37:17 +00:00
//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;
// }
// };
// };
//}