Browse Source

Offload extraction to WebWorker

pull/46/head
coomdev 2 years ago
parent
commit
708ac6e116
  1. 2
      README.md
  2. 18
      build-chrome.js
  3. 38
      build-ff.js
  4. 23
      build.js
  5. 23
      chrome/dist/background.js
  6. 16473
      chrome/dist/main.js
  7. 2
      chrome/manifest.json
  8. 16432
      dist/main.js
  9. 23
      firefox/dist/background.js
  10. 16473
      firefox/dist/main.js
  11. 2
      firefox/manifest.json
  12. 2
      firefox_update.json
  13. 2
      main.d.ts
  14. 2
      main.meta.js
  15. 16434
      main.user.js
  16. 4
      src/Components/Embedding.svelte
  17. 2
      src/Components/EyeButton.svelte
  18. 4
      src/bitstream.ts
  19. 6
      src/jpg.ts
  20. 534
      src/main.ts
  21. 43
      src/platform.ts
  22. 31
      src/pngv3.ts
  23. 15
      src/pomf.ts
  24. 215
      src/processor.worker.ts
  25. 6
      src/stores.ts
  26. 11
      src/thirdeye.ts
  27. 30
      src/utils.ts
  28. 2
      src/webm.ts
  29. 6
      src/websites/index.ts

2
README.md

@ -25,7 +25,7 @@ Please report any issue you have with those (only for mainstream browsers)
Also, use this if you plan to use b4k's archive.
- [Install 4chanX (recommended)](https://www.4chan-x.net/builds/4chan-X.user.js)
- Install the correct WebExtension for your Browser ([Firefox](https://git.coom.tech/fuckjannies/lolipiss/raw/branch/%E4%B8%AD%E5%87%BA%E3%81%97/pngextraembedder-0.291.xpi) or Chrome-based (Down for "maintainance"))
- Install the correct WebExtension for your Browser ([Firefox](https://git.coom.tech/fuckjannies/lolipiss/raw/branch/%E4%B8%AD%E5%87%BA%E3%81%97/pngextraembedder-0.292.xpi) or Chrome-based (Down for "maintainance"))
For FF users, the extension is signed so you can just drag and drop it on your about:addons tab.

18
build-chrome.js

@ -95,6 +95,24 @@ const manif3 = {
esbuildSvelte({
compilerOptions: { css: true, accessors: true },
preprocess: sveltePreprocess(),
}),
inlineWorkerPlugin({
minify: false,
bundle: true,
treeShaking: true,
target: "es2021",
loader: {
'.css': 'text',
'.png': 'binary'
},
define: {
global: 'self',
execution_mode: '"worker"',
manifest: lmanif.version,
isBackground: 'false',
BUILD_VERSION: JSON.stringify([0, rev])
},
inject: ['./esbuild.inject.js'],
})
],
loader: {

38
build-ff.js

@ -2,6 +2,7 @@
import { spawnSync } from 'child_process';
import { writeFileSync, readFileSync, copyFileSync } from 'fs'
import inlineWorkerPlugin from 'esbuild-plugin-inline-worker';
import esbuild from "esbuild";
import esbuildSvelte from "esbuild-svelte";
@ -83,14 +84,14 @@ const manif = {
],
"web_accessible_resources": ["*.html", "*.js",],
// "background": {
// persistent: true,
// "scripts": [
// "polyfill.min.js",
// "browser-polyfill.min.js",
// "dist/background.js"
// ]
// }
// "background": {
// persistent: true,
// "scripts": [
// "polyfill.min.js",
// "browser-polyfill.min.js",
// "dist/background.js"
// ]
// }
};
(async () => {
@ -114,8 +115,25 @@ const manif = {
esbuildSvelte({
compilerOptions: { css: true, accessors: true },
preprocess: sveltePreprocess(),
})
],
}),
inlineWorkerPlugin({
minify: false,
bundle: true,
treeShaking: true,
target: "es2021",
loader: {
'.css': 'text',
'.png': 'binary'
},
define: {
global: 'self',
execution_mode: '"worker"',
manifest: manif.version,
isBackground: 'false',
BUILD_VERSION: JSON.stringify([0, rev])
},
inject: ['./esbuild.inject.js'],
})],
loader: {
'.css': 'text',
'.png': 'binary'

23
build.js

@ -2,6 +2,7 @@
import { spawnSync } from 'child_process';
import { writeFileSync, readFileSync } from 'fs'
import inlineWorkerPlugin from 'esbuild-plugin-inline-worker';
import esbuild from "esbuild";
import esbuildSvelte from "esbuild-svelte";
@ -23,7 +24,7 @@ let rev = +res.stdout;
define: {
global: 'window',
execution_mode: JSON.stringify(process.argv[2] || 'userscript'),
isBackground: JSON.stringify('false'),
isBackground: 'false',
BUILD_VERSION: JSON.stringify([0, rev])
},
inject: ['./esbuild.inject.js'],
@ -31,8 +32,24 @@ let rev = +res.stdout;
esbuildSvelte({
compilerOptions: { css: true, accessors: true },
preprocess: sveltePreprocess(),
})
],
}),
inlineWorkerPlugin({
minify: false,
bundle: true,
treeShaking: true,
target: "es2021",
loader: {
'.css': 'text',
'.png': 'binary'
},
define: {
global: 'self',
execution_mode: '"worker"',
isBackground: 'false',
BUILD_VERSION: JSON.stringify([0, rev])
},
inject: ['./esbuild.inject.js'],
})],
loader: {
'.css': 'text',
'.png': 'binary'

23
chrome/dist/background.js

@ -1876,9 +1876,12 @@
var lqueue = {};
var localLoad = (key, def) => "__pee__" + key in localStorage ? JSON.parse(localStorage.getItem("__pee__" + key)) : def;
var localSet = (key, value) => localStorage.setItem("__pee__" + key, JSON.stringify(value));
var { port1, port2 } = new MessageChannel();
var port1;
console.log("chrome_api", true);
if (false) {
const nmc = new MessageChannel();
port1 = nmc.port1;
port2 = nmc.port2;
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.name = location.origin;
@ -1886,7 +1889,6 @@
iframe.onload = _;
});
iframe.src = `${chrome.runtime.getURL("")}options.html`;
const meself2 = new URL(chrome.runtime.getURL("")).origin;
document.documentElement.appendChild(iframe);
iframeloaded.then(() => {
iframe.contentWindow?.postMessage("", "*", [port2]);
@ -1895,12 +1897,27 @@
lqueue[ev.data.id](ev.data);
};
}
console.log("chrome_api");
if (false) {
port1 = {
onmessage(ev) {
lqueue[ev.data.id](ev.data);
},
postMessage(msg, tr) {
postMessage({
type: "ipc",
tr,
msg
}, tr);
}
};
}
var gid = 0;
var sendCmd = (cmd, tr) => {
const prom = new Promise((_) => {
const id = gid++;
lqueue[id] = (e) => {
_(e.res);
_(e);
delete lqueue[id];
};
port1.postMessage({ id, ...cmd }, tr || []);

16473
chrome/dist/main.js

File diff suppressed because one or more lines are too long

2
chrome/manifest.json

@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "PngExtraEmbedder",
"description": "Discover embedded files on 4chan and archives!",
"version": "0.291",
"version": "0.292",
"icons": {
"64": "1449696017588.png"
},

16432
dist/main.js

File diff suppressed because one or more lines are too long

23
firefox/dist/background.js

@ -1876,9 +1876,12 @@
var lqueue = {};
var localLoad = (key, def) => "__pee__" + key in localStorage ? JSON.parse(localStorage.getItem("__pee__" + key)) : def;
var localSet = (key, value) => localStorage.setItem("__pee__" + key, JSON.stringify(value));
var { port1, port2 } = new MessageChannel();
var port1;
console.log("ff_api", true);
if (false) {
const nmc = new MessageChannel();
port1 = nmc.port1;
port2 = nmc.port2;
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.name = location.origin;
@ -1886,7 +1889,6 @@
iframe.onload = _;
});
iframe.src = `${chrome.runtime.getURL("")}options.html`;
const meself2 = new URL(chrome.runtime.getURL("")).origin;
document.documentElement.appendChild(iframe);
iframeloaded.then(() => {
iframe.contentWindow?.postMessage("", "*", [port2]);
@ -1895,12 +1897,27 @@
lqueue[ev.data.id](ev.data);
};
}
console.log("ff_api");
if (false) {
port1 = {
onmessage(ev) {
lqueue[ev.data.id](ev.data);
},
postMessage(msg, tr) {
postMessage({
type: "ipc",
tr,
msg
}, tr);
}
};
}
var gid = 0;
var sendCmd = (cmd, tr) => {
const prom = new Promise((_) => {
const id = gid++;
lqueue[id] = (e) => {
_(e.res);
_(e);
delete lqueue[id];
};
port1.postMessage({ id, ...cmd }, tr || []);

16473
firefox/dist/main.js

File diff suppressed because one or more lines are too long

2
firefox/manifest.json

@ -7,7 +7,7 @@
},
"name": "PngExtraEmbedder",
"description": "Discover embedded files on 4chan and archives!",
"version": "0.291",
"version": "0.292",
"icons": {
"64": "1449696017588.png"
},

2
firefox_update.json

@ -1 +1 @@
{"addons":{"{34ac4994-07f2-44d2-8599-682516a6c6a6}":{"updates":[{"version":"0.291","update_link":"https://git.coom.tech/fuckjannies/lolipiss/raw/branch/%E4%B8%AD%E5%87%BA%E3%81%97/pngextraembedder-0.291.xpi"}]}}}
{"addons":{"{34ac4994-07f2-44d2-8599-682516a6c6a6}":{"updates":[{"version":"0.292","update_link":"https://git.coom.tech/fuckjannies/lolipiss/raw/branch/%E4%B8%AD%E5%87%BA%E3%81%97/pngextraembedder-0.292.xpi"}]}}}

2
main.d.ts

@ -140,7 +140,7 @@ declare module "jpeg-js/lib/decoder" {
declare const QR: any;
declare const BUILD_VERSION: [number, number];
declare const execution_mode: 'userscript' | 'chrome_api' | 'ff_api';
declare const execution_mode: 'userscript' | 'chrome_api' | 'ff_api' | 'worker';
declare const isBackground: boolean;
declare const chrome: typeof browser;
declare const _DOMParser: typeof DOMParser;

2
main.meta.js

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

16434
main.user.js

File diff suppressed because one or more lines are too long

4
src/Components/Embedding.svelte

@ -50,7 +50,7 @@ import { peeTarget } from "../utils";
const thumb = file.thumbnail || file.data;
let type: FileTypeResult | undefined;
if (typeof thumb != "string") {
let buff = Buffer.isBuffer(thumb) ? thumb : await thumb();
let buff = thumb instanceof Uint8Array ? thumb : await thumb();
type = await fileTypeFromBuffer(buff);
if (
!type &&
@ -116,7 +116,7 @@ import { peeTarget } from "../utils";
lisn.addEventListener("progress", (e: any) => {
progress = e.detail;
});
let full = Buffer.isBuffer(file.data) ? file.data : await file.data(lisn);
let full = file.data instanceof Uint8Array ? file.data : await file.data(lisn);
type = await fileTypeFromBuffer(full);
if (
!type &&

2
src/Components/EyeButton.svelte

@ -31,7 +31,7 @@
a.style.display = "none";
let url: string;
if (typeof file.data != "string") {
const thumb = Buffer.isBuffer(file.data) ? file.data : await file.data();
const thumb = file.data instanceof Uint8Array ? file.data : await file.data();
const type = await fileTypeFromBuffer(thumb);
url = URL.createObjectURL(new Blob([thumb], { type: type?.mime }));
} else url = file.data;

4
src/bitstream.ts

@ -1,5 +1,3 @@
import { BitstreamReader as br, BitstreamWriter as bw } from '@astronautlabs/bitstream';
export const revbyte = (n: number, len = 8) => {
let acc = 0;
let n2 = n;
@ -142,8 +140,6 @@ export class BitstreamWriter {
}
setBit(b: number) {
if (b)
debugger;
let byte = this.buffer[0];
byte |= b << (this._offset & 7);
this.buffer[0] = byte;

6
src/jpg.ts

@ -1,5 +1,5 @@
import { Buffer } from "buffer";
import type { ImageProcessor } from "./main";
import type { ImageProcessor } from "./processor.worker";
import { f5stego } from './f5stego';
import { settings } from "./stores";
import { decodeCoom3Payload } from "./utils";
@ -19,7 +19,7 @@ const inject = async (b: File, links: string[]) => {
// unfortunately, because of the way f5 work, we can't determine
// if there's an embedded message until we have the complete file
// but the way PEE was designed forces us to just try to extract something until it works
const has_embed = (b: Buffer) => {
const has_embed = (b: Uint8Array) => {
try {
const res = f5inst.extract(b);
if (!res)
@ -35,7 +35,7 @@ const has_embed = (b: Buffer) => {
}
};
const extract = (b: Buffer, ex: string) => {
const extract = (b: Uint8Array, ex: string) => {
// if we reached here then ex is heckin cute and valid
return decodeCoom3Payload(Buffer.from(ex));
};

534
src/main.ts

@ -1,12 +1,14 @@
import 'process';
/// <reference lib="ES2021" />
/// <reference lib="dom" />
import { Buffer } from "buffer";
import { appState, settings, initial_settings } from "./stores";
import { debounce } from './debounce';
import globalCss from './global.css';
import pngv3 from "./pngv3";
import webm from "./webm";
import gif from "./gif";
//import webm from "./webm";
//import gif from "./gif";
import jpg from "./jpg";
import thirdeye from "./thirdeye";
import pomf from "./pomf";
@ -21,27 +23,26 @@ import NotificationsHandler from './Components/NotificationsHandler.svelte';
import { fireNotification, getEmbedsFromCache, getSelectedFile } from "./utils";
import { getQueryProcessor, QueryProcessor } from "./websites";
import { ifetch, Platform, streamRemote, supportedAltDomain, supportedMainDomain } from "./platform";
import { ifetch, Platform, sendCmd, lqueue, supportedAltDomain, supportedMainDomain } from "./platform";
import TextEmbeddingsSvelte from "./Components/TextEmbeddings.svelte";
import { HydrusClient } from "./hydrus";
import { registerPlugin } from 'linkifyjs';
import ViewCountSvelte from "./Components/ViewCount.svelte";
import type { ImageProcessor, WorkerEmbeddedFile } from './processor.worker';
import ProcessWorkerAny from './processor.worker';
import { headerStringToObject } from "./requests";
const ProcessWorker = ProcessWorkerAny as () => Worker;
if (!supportedMainDomain(location.host) && !supportedAltDomain(location.host))
throw "PEE not supported here, skipping";
export interface ImageProcessor {
skip?: true;
match(fn: string): boolean;
has_embed(b: Buffer, fn?: string, prevurl?: string): boolean | string | undefined | Promise<boolean | string | undefined>;
extract(b: Buffer, fn?: string): EmbeddedFile[] | Promise<EmbeddedFile[]>;
inject?(b: File, c: string[]): Buffer | Promise<Buffer>;
}
let qp: QueryProcessor;
export let csettings: Parameters<typeof settings['set']>[0];
let processors: ImageProcessor[] =
[thirdeye, pomf, pngv3, jpg, webm, gif];
const processors: ImageProcessor[] =
[thirdeye, pomf, pngv3, jpg];//, webm, gif
let cappState: Parameters<typeof appState['set']>[0];
settings.subscribe(async b => {
@ -64,9 +65,9 @@ settings.subscribe(async b => {
}
}
csettings = b;
processors = [...(!csettings.te ? [thirdeye] : []),
pngv3, pomf, jpg, webm, gif
];
//processors = [...(!csettings.te ? [thirdeye] : []),
// pngv3, pomf, jpg, webm, gif
//];
});
@ -79,7 +80,7 @@ type EmbeddedFileWithPreview = {
source?: string; // can be like a twitter post this was posted in originally
thumbnail: string | Buffer;
filename: string;
data: EmbeddedFileWithoutPreview['data'] | ((lisn?: EventTarget) => Promise<Buffer>);
data: EmbeddedFileWithoutPreview['data'] | ((lisn?: EventTarget) => Promise<Uint8Array>);
};
type EmbeddedFileWithoutPreview = {
@ -92,6 +93,7 @@ type EmbeddedFileWithoutPreview = {
export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview;
/*
const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string, hex: string, prevurl: string) => {
const ret = await Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
if (proc.skip) {
@ -109,7 +111,7 @@ const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string
try {
const n = await srcs.next();
if (n.done)
return; // no more links to try
return; // no more links to try
const iter = streamRemote(n.value);
if (!iter)
return;
@ -129,7 +131,7 @@ const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string
}
found = v;
}
} while (found !== false && !chunk.done /* Because we only embed links now, it's safe to assume we get everything we need in the first chunk */);
} while (found !== false && !chunk.done);
succ = true;
await iter.next(true);
if (found !== true) {
@ -143,7 +145,7 @@ const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string
} while (!succ);
}));
return ret.filter(e => e).map(e => e!);
};
};*/
const textToElement = <T = HTMLElement>(s: string) =>
document.createRange().createContextualFragment(s).children[0] as any as T;
@ -163,6 +165,8 @@ const buildCumFun = <T extends any[], U>(f: (args: T[]) => void, ...r: Parameter
};
let pendingPosts: { id: number, op: number }[] = [];
let pendingNoPosts: { id: number, op: number }[] = [];
// should be equivalent to buildCumFun(signalNewEmbeds, 5000, {trailing: true})
const signalNewEmbeds = debounce(async () => {
// ensure user explicitely enabled telemetry
@ -174,17 +178,22 @@ const signalNewEmbeds = debounce(async () => {
const boardname = location.pathname.match(/\/([^/]*)\//)![1];
// restructure to minimize redundancy
const reshaped = Object.fromEntries([...new Set(pendingPosts.map(e => e.op))].map(e => [e, pendingPosts.filter(p => p.op == e).map(e => e.id)]));
console.log(reshaped);
const reshaped2 = Object.fromEntries([...new Set(pendingNoPosts.map(e => e.op))].map(e => [e, pendingPosts.filter(p => p.op == e).map(e => e.id)]));
//console.log(reshaped);
const res = await ifetch("https://shoujo.coom.tech/listing/" + boardname, {
method: "POST",
body: JSON.stringify(reshaped),
body: JSON.stringify({
emb: reshaped,
noemb: reshaped2
}),
headers: {
'content-type': 'application/json'
}
});
await res.json();
pendingPosts = [];
pendingNoPosts = [];
} catch (e) {
// silently fail
console.error(e);
@ -201,6 +210,197 @@ const shouldUseCache = () => {
: location.hostname.includes('b4k');
};
let cp: CommandProcessor;
class CommandProcessor {
processor = ProcessWorker();
genid = 0;
pendinggens: Record<number, AsyncGenerator> = {};
cmdid = 0;
pendingprom: Record<number, (v?: any) => void> = {};
constructor() {
this.processor.onmessage = async (msg) => {
let gen: AsyncGenerator;
let res: IteratorResult<any, any>;
switch (msg.data.type) {
case 'ipc':
{
const id = msg.data.msg.id;
if (execution_mode != "userscript") {
if (msg.data.msg.name == 'corsFetch') {
sendCmd(msg.data.msg, msg.data.tr);
lqueue[id] = (res: any) => {
this.processor.postMessage({
type: 'ipc',
id,
res
});
};
} else {
// for complitude, but technically the webworker doesn't run anything besides corsFetch
const repl: any = await sendCmd(msg.data.msg, msg.data.tr);
repl.id = id;
this.processor.postMessage({
type: 'ipc',
id,
res: repl
});
}
} else {
if (msg.data.msg.name == 'fullyRead') {
// ignore
this.processor.postMessage({
type: 'ipc',
res: {
id,
ok: 1
}
});
}
if (msg.data.msg.name == 'corsFetch') {
const { args } = msg.data.msg;
const res = await ifetch(args[0], args[1]);
// don't report progress because monkeys don't have a way to expose partial responses anyway
const headersStr = (res as any).responseHeaders;
const headerObj = headerStringToObject(headersStr);
this.processor.postMessage({
type: 'ipc',
id,
res: {
id,
ok: res.ok || true,
setRes: true,
headers: headerObj,
responseHeaders: headersStr,
redirected: res.redirected,
type: res.type,
url: res.url,
status: res.status,
bodyUsed: res.bodyUsed,
statusText: res.statusText,
}
});
if (!args[1].method || ['GET', 'POST'].includes(args[1].method)) {
const data = await res.arrayBuffer();
this.processor.postMessage({
type: 'ipc',
id,
res: {
id,
pushData: {
data
}
}
}, [data]);
}
// let's hope these are delivered in order :%)
this.processor.postMessage({
type: 'ipc',
id,
res: {
id,
pushData: {
}
}
}, []);
}
// ignore other commands
}
} break;
case 'reply':
if (msg.data.id in this.pendingprom) {
this.pendingprom[msg.data.id](msg.data.res);
delete this.pendingprom[msg.data.id];
}
break;
case 'ag':
gen = this.pendinggens[msg.data.id];
res = await gen.next(msg.data.args);
if (res.done) {
delete this.pendinggens[msg.data.id];
}
this.processor.postMessage({
type: 'ag',
id: msg.data.id,
res
});
break;
}
};
}
serializeArg(m: any) {
if (m[Symbol.toStringTag] == 'AsyncGenerator') {
const genid = this.genid++;
this.pendinggens[genid] = m;
return {
type: 'AsyncGenerator',
id: genid
};
}
return m;
}
sendCmd(cmd: string, ...args: any[]) {
const id = this.cmdid++;
this.processor.postMessage({
type: 'cmd',
id,
fun: cmd,
args: args.map(a => this.serializeArg(a))
});
return new Promise<any>(res => {
this.pendingprom[id] = res;
});
}
sendAg(id: number, res: IteratorResult<any, any>) {
this.processor.postMessage({
type: 'ag',
id, res // todo: call serializeArg?
});
}
processImage(origlink: AsyncGenerator<string>, fn: string, md5: string, thumb: string): Promise<[WorkerEmbeddedFile[], boolean][]> {
return this.sendCmd('processImage', origlink, fn, md5, thumb);
}
}
const convertToLocalEmbed = (wef: WorkerEmbeddedFile) => {
let ret: EmbeddedFileWithPreview;
ret = wef as any;
// handles bigger files where data is represented as a {url, header} object
if (typeof wef.data == "object") {
if (!(wef.data instanceof Uint8Array)) {
const ref = wef.data;
if (!wef.thumbnail)
return wef;
ret = {
...wef,
thumbnail: Buffer.from(wef.thumbnail),
data: async (lsn) => {
return Buffer.from(await (await ifetch(ref.url, { headers: ref.headers }, lsn)).arrayBuffer());
}
};
}
}
if (wef.data instanceof Uint8Array) {
ret.data = Buffer.from(wef.data);
}
if (wef.thumbnail instanceof Uint8Array) {
ret.thumbnail = Buffer.from(wef.thumbnail);
}
return ret!;
};
const processPost = async (post: HTMLDivElement) => {
const origlink = qp.getImageLink(post);
if (!origlink)
@ -208,17 +408,17 @@ const processPost = async (post: HTMLDivElement) => {
const thumbLink = qp.getThumbnailLink(post);
if (!thumbLink)
return;
let res2: [EmbeddedFile[], boolean][] | undefined = undefined;
let res2: [WorkerEmbeddedFile[], boolean][] | undefined = undefined;
const op = +location.pathname.match(/\/thread\/(.*)/)![1];
const reportEmbed = () => {
if (!csettings)
return false;
return;
if (csettings.tm) {
// dont report results from archive, only live threads
if (['boards.4chan.org', 'boards.4channel.org'].includes(location.host)) {
if (!cappState.isCatalog) { // only save from within threads
// we must be in a thread, thus the following is valid
const op = +location.pathname.match(/\/thread\/(.*)/)![1];
pendingPosts.push({ id: +(post.id.match(/([0-9]+)/)![1]), op });
signalNewEmbeds(); // let it run async
}
@ -226,12 +426,29 @@ const processPost = async (post: HTMLDivElement) => {
}
};
const reportNoEmbed = () => {
if (!csettings)
return;
if (csettings.tm) {
// dont report results from archive, only live threads
if (['boards.4chan.org', 'boards.4channel.org'].includes(location.host)) {
if (!cappState.isCatalog) { // only save from within threads
// we must be in a thread, thus the following is valid
pendingNoPosts.push({ id: +(post.id.match(/([0-9]+)/)![1]), op });
signalNewEmbeds(); // let it run async
}
}
}
};
try {
if (shouldUseCache()) {
res2 = await getEmbedsFromCache(qp.getCurrentBoard(), +qp.getCurrentThread()!, post.id);
}
if (!res2) {
res2 = await processImage(origlink, qp.getFilename(post), qp.getMD5(post), thumbLink);
res2 = [];
const tmp = await cp.processImage(origlink, qp.getFilename(post), qp.getMD5(post), thumbLink);
res2.push(...tmp);
res2 = res2?.filter(e => e);
}
} catch (e) {
@ -239,10 +456,10 @@ const processPost = async (post: HTMLDivElement) => {
return;
}
if (!res2 || res2.length == 0)
return;
return reportNoEmbed();
reportEmbed();
post.querySelector('.post')?.classList.add("embedfound");
processAttachments(post, res2?.flatMap(e => e![0].map(k => [k, e![1]] as [EmbeddedFile, boolean])));
processAttachments(post, res2?.flatMap(e => e![0].map(k => [convertToLocalEmbed(k), e![1]] as [EmbeddedFile, boolean])));
};
const versionCheck = async () => {
@ -269,156 +486,118 @@ function copyTextToClipboard(text: string) {
}
const scrapeBoard = async (self: HTMLButtonElement) => {
if (!csettings)
return false;
if (csettings.tm) {
fireNotification("success", "Scrapping board with telemetry on! Thank you for your service, selfless stranger ;_;7");
}
self.disabled = true;
self.textContent = "Searching...";
const boardname = location.pathname.match(/\/([^/]*)\//)![1];
const res = await ifetch(`https://a.4cdn.org/${boardname}/threads.json`);
const pages = await res.json() as Page[];
type Page = { threads: Thread[] }
type Thread = { no: number; posts: Post[] };
type BasePost = { no: number, resto: number, tim: number };
type PostWithFile = BasePost & { tim: number, ext: string, md5: string, filename: string };
type PostWithoutFile = BasePost & Record<string, unknown>;
type Post = (PostWithoutFile | PostWithFile);
fireNotification("info", "Fetching all threads...");
const threads = (await Promise.all(pages
.reduce((a: Thread[], b: Page) => [...a, ...b.threads], [])
.map(e => e.no)
.map(async id => {
try {
const res = await ifetch(`https://a.4cdn.org/${boardname}/thread/${id}.json`);
return await res.json() as Thread;
} catch {
return undefined;
}
}))).filter(e => e).map(e => e as Thread);
const filenames = threads
.reduce((a, b) => [...a, ...b.posts.filter(p => p.ext)
.map(p => p as PostWithFile)], [] as PostWithFile[]).filter(p => p.ext != '.webm' && p.ext != '.gif')
.map(p => [p.resto || p.no, `https://i.4cdn.org/${boardname}/${p.tim}${p.ext}`, p.md5, p.filename + p.ext, p.no] as [number, string, string, string, number]);
console.log(filenames);
fireNotification("info", "Analyzing images...");
const n = 1;
//console.log(posts);
const processFile = (src: string, fn: string, hex: string) => {
return Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
if (proc.skip) {
const md5 = Buffer.from(hex, 'base64');
return await proc.has_embed(md5, fn);
}
// TODO: Move this outside the loop?
const iter = streamRemote(src);
if (!iter)
return false;
let cumul = Buffer.alloc(0);
let found: boolean | undefined;
let chunk: ReadableStreamDefaultReadResult<Buffer> = { done: true };
do {
const { value, done } = await iter.next(typeof found === "boolean");
if (done) {
chunk = { done: true } as ReadableStreamDefaultReadDoneResult;
} else {
chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>;
cumul = Buffer.concat([cumul, value!]);
const v = await proc.has_embed(cumul);
if (typeof v == "string") {
return true;
}
found = v;
/* if (!csettings)
return false;
if (csettings.tm) {
fireNotification("success", "Scrapping board with telemetry on! Thank you for your service, selfless stranger ;_;7");
}
self.disabled = true;
self.textContent = "Searching...";
const boardname = location.pathname.match(/\/([^/]*)\//)![1];
const res = await ifetch(`https://a.4cdn.org/${boardname}/threads.json`);
const pages = await res.json() as Page[];
type Page = { threads: Thread[] }
type Thread = { no: number; posts: Post[] };
type BasePost = { no: number, resto: number, tim: number };
type PostWithFile = BasePost & { tim: number, ext: string, md5: string, filename: string };
type PostWithoutFile = BasePost & Record<string, unknown>;
type Post = (PostWithoutFile | PostWithFile);
fireNotification("info", "Fetching all threads...");
const threads = (await Promise.all(pages
.reduce((a: Thread[], b: Page) => [...a, ...b.threads], [])
.map(e => e.no)
.map(async id => {
try {
const res = await ifetch(`https://a.4cdn.org/${boardname}/thread/${id}.json`);
return await res.json() as Thread;
} catch {
return undefined;
}
} while (found !== false && !chunk.done);
await iter.next(true);
return found === true;
}));
};
const range = ~~(filenames.length / n) + 1;
const hasEmbed: typeof filenames = [];
const total = filenames.length;
let processed = 0;
const int = setInterval(() => {
fireNotification("info", `Processed [${processed} / ${total}] files`);
}, 5000);
await Promise.all([...new Array(n + 1)].map(async (e, i) => {
const postsslice = filenames.slice(i * range, (i + 1) * range);
for (const post of postsslice) {
try {
const res = await processFile(post[1], post[3], post[2]);
processed++;
if (res.some(e => e)) {
hasEmbed.push(post);
// dont report results from archive, only live threads
if (['boards.4chan.org', 'boards.4channel.org'].includes(location.host)) {
pendingPosts.push({ id: post[4], op: post[0] });
signalNewEmbeds(); // let it run async
}))).filter(e => e).map(e => e as Thread);
const filenames = threads
.reduce((a, b) => [...a, ...b.posts.filter(p => p.ext)
.map(p => p as PostWithFile)], [] as PostWithFile[]).filter(p => p.ext != '.webm' && p.ext != '.gif')
.map(p => [p.resto || p.no, `https://i.4cdn.org/${boardname}/${p.tim}${p.ext}`, p.md5, p.filename + p.ext, p.no] as [number, string, string, string, number]);
console.log(filenames);
fireNotification("info", "Analyzing images...");
const n = 1;
//console.log(posts);
const processFile = (src: string, fn: string, hex: string) => {
return Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
if (proc.skip) {
const md5 = Buffer.from(hex, 'base64');
return await proc.has_embed(md5, fn);
}
// TODO: Move this outside the loop?
const iter = streamRemote(src);
if (!iter)
return false;
let cumul = Buffer.alloc(0);
let found: boolean | undefined;
let chunk: ReadableStreamDefaultReadResult<Buffer> = { done: true };
do {
const { value, done } = await iter.next(typeof found === "boolean");
if (done) {
chunk = { done: true } as ReadableStreamDefaultReadDoneResult;
} else {
chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>;
cumul = Buffer.concat([cumul, value!]);
const v = await proc.has_embed(cumul);
if (typeof v == "string") {
return true;
}
found = v;
}
} while (found !== false && !chunk.done);
await iter.next(true);
return found === true;
}));
};
const range = ~~(filenames.length / n) + 1;
const hasEmbed: typeof filenames = [];
const total = filenames.length;
let processed = 0;
const int = setInterval(() => {
fireNotification("info", `Processed [${processed} / ${total}] files`);
}, 5000);
await Promise.all([...new Array(n + 1)].map(async (e, i) => {
const postsslice = filenames.slice(i * range, (i + 1) * range);
for (const post of postsslice) {
try {
const res = await processFile(post[1], post[3], post[2]);
processed++;
if (res.some(e => e)) {
hasEmbed.push(post);
// dont report results from archive, only live threads
if (['boards.4chan.org', 'boards.4channel.org'].includes(location.host)) {
pendingPosts.push({ id: post[4], op: post[0] });
signalNewEmbeds(); // let it run async
}
}
} catch (e) {
console.log(e);
}
} catch (e) {
console.log(e);
}
}
}));
clearInterval(int);
const counters: Record<number, number> = {};
for (const k of hasEmbed)
counters[k[0]] = k[0] in counters ? counters[k[0]] + 1 : 1;
console.log(counters);
fireNotification("success", "Processing finished! Results pasted in the clipboard");
const text = Object.entries(counters).sort((a, b) => b[1] - a[1]).map(e => `>>${e[0]} (${e[1]})`).join('\n');
console.log(text);
copyTextToClipboard(text);
self.textContent = "Copy Results";
self.disabled = false;
self.onclick = () => {
copyTextToClipboard(text);
};
};
const __DOMParser = execution_mode == "userscript" ? _DOMParser : DOMParser;
const cleanupHTML = (ndom: Document) => {
const evalWhenReady: string[] = [];
const addFromSource = (elem: HTMLElement, url: string) => {
const scr = document.createElement('script');
scr.type = 'text/javascript';
scr.src = url;
elem.append(scr);
};
const addFromCode = (elem: HTMLElement, sr: string) => {
const scr = document.createElement('script');
scr.type = 'text/javascript';
scr.innerText = sr;
elem.append(scr);
};
const rm = (e: any) => e.remove();
[...ndom.head.children].filter(e => e.tagName == "SCRIPT").forEach(rm);
[...ndom.body.children].filter(e => e.tagName == "SCRIPT").forEach(rm);
addFromSource(ndom.body, "https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js");
addFromSource(ndom.head, "https://based.coom.tech/highlight.pack.js");
/*
addFromCode(ndom.body, `document.documentElement.className = "";document.documentElement.lang = "en";document.documentElement.dataset.site = "arch.b4k.co";`);
addFromCode(ndom.body, `hljs.configure({ tableReplace: ' ' }); $('pre,code').each(function(i, block) { hljs.highlightBlock(block); }); var backend_vars = {"user_name":false,"user_email":false,"user_pass":"9fOK4K8","site_url":"https://arch.b4k.co/","default_url":"https://arch.b4k.co/","archive_url":"https://arch.b4k.co/","system_url":"https://arch.b4k.co/","api_url":"https://arch.b4k.co/","cookie_domain":null,"cookie_prefix":"foolfuuka_a2e7d4_","selected_theme":"foolz/foolfuuka-theme-foolfuuka","csrf_token_key":"csrf_token","images":{"banned_image":"https://arch.b4k.co/foolfuuka/foolz/foolfuuka-theme-foolfuuka/assets-1.2.28/images/banned-image.png","banned_image_width":150,"banned_image_height":150,"missing_image":"https://arch.b4k.co/foolfuuka/foolz/foolfuuka-theme-foolfuuka/assets-1.2.28/images/missing-image.jpg","missing_image_width":150,"missing_image_height":150},"gettext":{"submit_state":"Submitting","thread_is_real_time":"This thread is being displayed in real time.","update_now":"Update now","ghost_mode":"This thread has entered ghost mode. Your reply will be marked as a ghost post and will only affect the ghost index."},"board_shortname":"v"};`);
// head
//body
await addFromSource(ndom.body, "https://based.coom.tech/bootstrap.min.js");
await addFromSource(ndom.body, "https://based.coom.tech/plugins.js");
}));
await addFromSource(ndom.body, "https://based.coom.tech/board.js");
await addFromSource(ndom.body, "https://based.coom.tech/fuuka.js");
await addFromSource(ndom.body, "https://based.coom.tech/lazyload.js");
*/
return [ndom.documentElement.innerHTML, evalWhenReady] as [string, string[]];
clearInterval(int);
const counters: Record<number, number> = {};
for (const k of hasEmbed)
counters[k[0]] = k[0] in counters ? counters[k[0]] + 1 : 1;
console.log(counters);
fireNotification("success", "Processing finished! Results pasted in the clipboard");
const text = Object.entries(counters).sort((a, b) => b[1] - a[1]).map(e => `>>${e[0]} (${e[1]})`).join('\n');
console.log(text);
copyTextToClipboard(text);
self.textContent = "Copy Results";
self.disabled = false;
self.onclick = () => {
copyTextToClipboard(text);
};*/
};
let gmo: MutationObserver;
@ -564,6 +743,13 @@ const startup = async (is4chanX = true) => {
await bodyInit;
}
try {
cp = new CommandProcessor();
} catch {
alert("You may be using 4chanX\n\nGo to 4chanX's settings, Advanced > JS Whitelist and add 'blob:' without quotes to the list.");
return;
}
if (!is4chanX && location.host.startsWith('boards.4chan')) {
const notificationHost = document.createElement('span');
new NotificationsHandler({
@ -793,7 +979,7 @@ function processAttachments(post: HTMLDivElement, ress: [EmbeddedFile, boolean][
target: textInsertCursor,
props: {
files: ress.map(e => e[0]).filter(e =>
Buffer.isBuffer(e.data) && e.filename.endsWith('.txt') && e.filename.startsWith('message')
(Buffer.isBuffer(e.data) || e.data instanceof Uint8Array) && e.filename.endsWith('.txt') && e.filename.startsWith('message')
)
}
});

43
src/platform.ts

@ -1,6 +1,6 @@
import { GM_fetch, GM_head, headerStringToObject } from './requests';
const lqueue = {} as any;
export const lqueue = {} as any;
const localLoad = <T>(key: string, def: T) =>
('__pee__' + key) in localStorage
@ -10,9 +10,19 @@ const localLoad = <T>(key: string, def: T) =>
const localSet = (key: string, value: any) =>
localStorage.setItem('__pee__' + key, JSON.stringify(value));
const { port1, port2 } = new MessageChannel();
export let port1: MessagePort;
let port2: MessagePort;
console.log(execution_mode, isBackground);
if (execution_mode != 'userscript' && !isBackground) {
/*
A web worker has no access to the dom, so things like remote fetches are proxied through the main frame
*/
if (execution_mode != 'userscript' && !isBackground && execution_mode != 'worker') {
const nmc = new MessageChannel();
port1 = nmc.port1;
port2 = nmc.port2;
// It has to be a content script
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
@ -21,7 +31,7 @@ if (execution_mode != 'userscript' && !isBackground) {
iframe.onload = _;
});
iframe.src = `${chrome.runtime.getURL('')}options.html`;
const meself = new URL(chrome.runtime.getURL('')).origin;
//const meself = new URL(chrome.runtime.getURL('')).origin;
document.documentElement.appendChild(iframe);
iframeloaded.then(() => {
iframe.contentWindow?.postMessage('', '*', [port2]);
@ -31,6 +41,24 @@ if (execution_mode != 'userscript' && !isBackground) {
};
}
console.log(execution_mode);
if (execution_mode == "worker") {
port1 = {
onmessage(ev) {
lqueue[ev.data.id](ev.data);
},
postMessage(msg, tr?: Transferable[]) {
(postMessage as any)({
type: 'ipc',
tr,
msg
}, tr);
}
} as MessagePort;
}
// hack
let gid = 0;
const visit = (e: any, cb: (e: any) => true | undefined) => {
@ -42,11 +70,11 @@ const visit = (e: any, cb: (e: any) => true | undefined) => {
cb(e);
};
const sendCmd = <V>(cmd: any, tr?: Transferable[]) => {
export const sendCmd = <V>(cmd: any, tr?: Transferable[]) => {
const prom = new Promise<V>(_ => {
const id = gid++;
lqueue[id] = (e: any) => {
_(e.res);
_(e);
delete lqueue[id];
};
port1.postMessage({ id, ...cmd }, tr || []);
@ -55,7 +83,7 @@ const sendCmd = <V>(cmd: any, tr?: Transferable[]) => {
};
const bridge = <U extends any[], V, T extends (...args: U) => V>(name: string, f: T) => {
if (execution_mode != 'userscript' && !isBackground)
if (execution_mode != 'userscript' && !isBackground && execution_mode != 'worker')
return f;
// It has to be the background script
return (...args: U) => {
@ -386,7 +414,6 @@ export async function* streamRemote(url: string, chunkSize = 72 * 1024, fetchRes
while (ptr != size) {
//console.log('doing a fetch of ', url, ptr, ptr + fetchSize - 1);
let obj: Record<string, string>;
const fres = await ifetch(url, { headers: { range: `bytes=${ptr}-${ptr + fetchSize - 1}` } });
if (execution_mode == "userscript") {
obj = headerStringToObject((fres as any as Tampermonkey.Response<any>).responseHeaders);

31
src/pngv3.ts

@ -1,5 +1,5 @@
import { Buffer } from "buffer";
import type { EmbeddedFile, ImageProcessor } from "./main";
import type { WorkerEmbeddedFile, ImageProcessor } from "./processor.worker";
import { PNGDecoder, PNGEncoder } from "./png";
import { decodeCoom3Payload } from "./utils";
import { settings } from "./stores";
@ -141,10 +141,10 @@ const extractFromRawDeflate = (b: Buffer) => {
return false; // possibly incorrect?
};
const extract = async (png: Buffer) => {
const extract = async (png: Buffer, doextract = true) => {
const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader, false);
const ret: EmbeddedFile[] = [];
const ret: WorkerEmbeddedFile[] = [];
let w: Buffer | undefined;
if (!csettings)
throw new Error("Settings uninit");
@ -159,22 +159,28 @@ const extract = async (png: Buffer) => {
case 'tEXt':
buff = chunk;
if (buff.slice(4, 4 + CUM3.length).equals(CUM3)) {
if (!doextract)
return true;
const k = await decodeCoom3Payload(buff.slice(4 + CUM3.length));
ret.push(...k.filter(e => e).map(e => e as EmbeddedFile));
ret.push(...k.filter(e => e));
}
if (buff.slice(4, 4 + CUM4.length).equals(CUM4)) {
if (!doextract)
return true;
const passed = buff.slice(4 + CUM4.length);
xor(passed, password);
const k = await decodeCoom3Payload(passed);
ret.push(...k.filter(e => e).map(e => e as EmbeddedFile));
ret.push(...k.filter(e => e));
}
if (buff.slice(4, 4 + CUM5.length).equals(CUM5)) {
if (!doextract)
return true;
const passed = buff.slice(4 + CUM5.length);
const decoded = Buffer.from(passed.toString(), 'base64').toString().split(' ').map(e => {
return `https://${rprefs[e[0]]}/${e.slice(1)}`;
}).join(' ');
const k = await decodeCoom3Payload(Buffer.from(decoded));
ret.push(...k.filter(e => e).map(e => e as EmbeddedFile));
ret.push(...k.filter(e => e));
}
// eslint-disable-next-line no-cond-assign
@ -194,8 +200,10 @@ const extract = async (png: Buffer) => {
// should also check if the id has a len of 6-8 or ends in .pee
return `https://${rprefs[e[0]]}/${e.slice(1)}`;
}).join(' ');
if (!doextract)
return true;
const k = await decodeCoom3Payload(Buffer.from(decoded));
ret.push(...k.filter(e => e).map(e => e as EmbeddedFile));
ret.push(...k.filter(e => e));
} catch (e) {
//
}
@ -231,10 +239,13 @@ const extract = async (png: Buffer) => {
// should also check if the id has a len of 6-8 or ends in .pee
return `https://${rprefs[e[0]]}/${e.slice(1)}`;
}).join(' ');
return decodeCoom3Payload(Buffer.from(dec));
if (doextract)
return decodeCoom3Payload(Buffer.from(dec));
return true;
}
} catch (e) {
console.error(e);
if (e != "Uhh")
console.error(e);
} finally {
reader.releaseLock();
}
@ -359,7 +370,7 @@ const inject = async (container: File, links: string[]) => {
};
const has_embed = async (png: Buffer) => {
const r = await extract(png);
const r = await extract(png, false);
return !!r;
};

15
src/pomf.ts

@ -1,4 +1,5 @@
import type { EmbeddedFile, ImageProcessor } from "./main";
import type { ImageProcessor, WorkerEmbeddedFile } from './processor.worker';
import { Buffer } from "buffer";
import thumbnail from "./assets/hasembed.png";
import { settings } from "./stores";
@ -16,7 +17,7 @@ settings.subscribe(b => {
});
const getExt = (fn: string) => {
// const isDum = fn!.match(/^[a-z0-9]{6}\./i);
// const isDum = fn!.match(/^[a-z0-9]{6}\./i);
const isB64 = fn!.match(/^((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=))?\.(gif|jpe?g|png|webm)/);
const isExt = fn!.match(/\[.*=(.*)\]/);
let ext;
@ -61,15 +62,9 @@ const extract = async (b: Buffer, fn?: string) => {
return [{
filename: ext,
data: csettings.hotlink ? rsource! : async (lsn) => {
try {
return Buffer.from(await (await ifetch(rsource, undefined, lsn)).arrayBuffer());
} catch (e) {
//404
}
},
data: csettings.hotlink ? rsource! : { url: rsource! },
thumbnail: Buffer.from(thumbnail)
} as EmbeddedFile];
} as WorkerEmbeddedFile];
};
const has_embed = async (b: Buffer, fn?: string) => {

215
src/processor.worker.ts

@ -1,16 +1,199 @@
//export default ((() => {/* */ }) as () => Worker);
//const exports = {};
//import * as platform from './platform';
//
//console.log("Worker started");
//
//const deserializeMessage = (m: any) => {
//
//}
//
//(async () => {
// onmessage = (msg: MessageEvent<any>) => {
// const des = deserializeMessage(msg);
// };
//})();
/// <reference lib="ES2021" />
/// <reference lib="webworker" />
import * as platform from './platform';
import pngv3 from "./pngv3";
//import webm from "./webm";
//import gif from "./gif";
import jpg from "./jpg";
import thirdeye from "./thirdeye";
import pomf from "./pomf";
console.log("Worker started");
const pendinggens: Record<number, ((res: IteratorResult<any>) => void)[]> = {};
const pendingcmds: Record<number, ((res: any) => void)[]> = {};
const proxyAsyncGen = (id: number) => {
return {
next(arg) {
postMessage({
type: 'ag',
id
});
return new Promise<IteratorResult<any>>(res => {
if (!pendinggens[id])
pendinggens[id] = [];
pendinggens[id].push(res);
});
},
return(v) {
postMessage({
type: 'ag',
id
});
return new Promise<IteratorResult<any>>(res => {
if (!pendinggens[id])
pendinggens[id] = [];
pendinggens[id].push(res);
});
},
throw() {
postMessage({
type: 'ag',
id
});
return new Promise<IteratorResult<any>>(res => {
if (!pendinggens[id])
pendinggens[id] = [];
pendinggens[id].push(res);
});
},
[Symbol.asyncIterator]() {
return this as AsyncGenerator;
}
} as AsyncGenerator;
};
const deserializeMessage = (m: any) => {
if (typeof m == "object" && m.type == 'AsyncGenerator') {
return proxyAsyncGen(m.id);
}
if (typeof m == "object") {
for (const p in m) {
m[p] = deserializeMessage(m[p]);
}
}
return m;
};
type WorkerEmbeddedFileWithPreview = {
page?: { title: string, url: string }; // can be a booru page
source?: string; // can be like a twitter post this was posted in originally
thumbnail: string | Uint8Array;
filename: string;
data: WorkerEmbeddedFileWithoutPreview['data'] | { url: string, headers?: Record<string, string> };
};
type WorkerEmbeddedFileWithoutPreview = {
page: undefined;
source: undefined;
thumbnail?: string;
filename: string;
data: string | Uint8Array;
};
export type WorkerEmbeddedFile = WorkerEmbeddedFileWithPreview | WorkerEmbeddedFileWithoutPreview;
export interface ImageProcessor {
skip?: true;
match(fn: string): boolean;
has_embed(b: Uint8Array, fn?: string, prevurl?: string): boolean | string | undefined | Promise<boolean | string | undefined>;
extract(b: Uint8Array, fn?: string): WorkerEmbeddedFile[] | Promise<WorkerEmbeddedFile[]>;
inject?(b: File, c: string[]): Buffer | Promise<Buffer>;
}
const processors: ImageProcessor[] =
[thirdeye, pomf, pngv3, jpg]; //, webm, gif
const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string, hex: string, prevurl: string) => {
const ret = await Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
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, prevurl) === true) {
return [await proc.extract(md5, fn), true] as [WorkerEmbeddedFile[], boolean];
}
return;
}
let succ = false;
let cumul: Buffer;
do {
try {
const n = await srcs.next();
if (n.done)
return; // no more links to try
const iter = platform.streamRemote(n.value);
if (!iter)
return;
cumul = Buffer.alloc(0);
let found: boolean | undefined;
let chunk: ReadableStreamDefaultReadResult<Buffer> = { done: true };
do {
const { value, done } = await iter.next(typeof found === "boolean");
if (done) {
chunk = { done: true } as ReadableStreamDefaultReadDoneResult;
} else {
chunk = { done: false, value } as ReadableStreamDefaultReadValueResult<Buffer>;
cumul = Buffer.concat([cumul, value!]);
const v = await proc.has_embed(cumul);
if (typeof v == "string") {
return [await proc.extract(cumul, v), false] as [WorkerEmbeddedFile[], boolean];
}
found = v;
}
} while (found !== false && !chunk.done /* Because we only embed links now, it's safe to assume we get everything we need in the first chunk */);
succ = true;
await iter.next(true);
if (found !== true) {
//console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`);
return;
}
return [await proc.extract(cumul), false] as [WorkerEmbeddedFile[], boolean];
} catch(err) {
console.log(err);
// ignore error and retry with another link
}
} while (!succ);
}));
return ret.filter(e => e).map(e => e!);
};
(async () => {
onmessage = async (msg: MessageEvent<any>) => {
const des = deserializeMessage(msg.data);
switch (des.type) {
case 'ipc': {
if (platform.port1.onmessage)
platform.port1.onmessage(new MessageEvent("message", { data: des.res }));
break;
}
case 'cmd': {
switch (des.fun) {
case 'processImage': {
//console.log('Received process image command', des);
const res = await processImage(des.args[0], des.args[1], des.args[2], des.args[3]);
//console.log('Finished process image command', des);
const tr: Transferable[] = [];
for (const ef of res) {
for (const e of ef[0]) {
if (Buffer.isBuffer(e.thumbnail) || e.thumbnail instanceof Uint8Array)
tr.push(e.thumbnail.buffer);
if (Buffer.isBuffer(e.data) || e.data instanceof Uint8Array)
tr.push(e.data.buffer);
}
}
//console.log('Sent reply', res, des);
postMessage({
type: 'reply',
id: des.id,
res
}, [...new Set(tr)]);
}
}
break;
}
case 'ag': {
const cb = pendinggens[des.id].shift();
if (cb) {
cb(des.res);
}
break;
}
}
};
})();

6
src/stores.ts

@ -4,6 +4,9 @@ import type { Booru } from "./thirdeye";
// Todo: use GM get/setValue instead?
export const localLoad = async <T>(key: string, def: T) => {
if (execution_mode == "worker") {
return def;
}
const isinls = ('__pee__' + key) in localStorage;
let ret: T;
if (isinls) {
@ -31,6 +34,9 @@ export const localLoad = async <T>(key: string, def: T) => {
};
const localSet = (key: string, value: any) => {
if (execution_mode == "worker") {
return;
}
if (execution_mode != "userscript")
chrome.storage.local.set({ [key]: JSON.stringify(value) });
else

11
src/thirdeye.ts

@ -1,4 +1,5 @@
import type { EmbeddedFile, ImageProcessor } from "./main";
import type { ImageProcessor } from "./processor.worker";
import type { WorkerEmbeddedFile } from "./processor.worker";
import { localLoad, settings } from "./stores";
import { Buffer } from "buffer";
import { decode } from 'jpeg-js';
@ -173,12 +174,8 @@ const extract = async (b: Buffer, fn?: string) => {
},
filename: fn!.substring(0, 33) + result[0].ext,
thumbnail: csettings.hotlink ? (prev || full) : Buffer.from(await (await ifetch(prev || full)).arrayBuffer()),
data: csettings.hotlink ? (full || prev) : (async (lsn) => {
if (!cachedFile)
cachedFile = (await (await ifetch(full || prev, undefined, lsn)).arrayBuffer());
return Buffer.from(cachedFile);
})
} as EmbeddedFile];
data: csettings.hotlink ? (full || prev) : { url: full || prev }
} as WorkerEmbeddedFile];
};
const phash = (b: Buffer) => {

30
src/utils.ts

@ -1,6 +1,7 @@
import { Buffer } from "buffer";
import thumbnail from "./assets/hasembed.png";
import type { EmbeddedFile } from './main';
import type { EmbeddedFile } from "./main";
import type { WorkerEmbeddedFile } from "./processor.worker";
import { settings } from "./stores";
import { filehosts } from "./filehosts";
import { getHeaders, ifetch, Platform } from "./platform";
@ -171,7 +172,7 @@ export const getThreadDataCache = async (board: string, op: number) => {
/*
Semantic difference: Empty array means there is nothing, undefined means it wasn't found in the cache
*/
export const getEmbedsFromCache = async (board: string, op: number, pid: string): Promise<[EmbeddedFile[], boolean][] | undefined> => {
export const getEmbedsFromCache = async (board: string, op: number, pid: string): Promise<[WorkerEmbeddedFile[], boolean][] | undefined> => {
if (!csettings)
throw new Error("Settings uninit");
await getThreadDataCache(board, op);
@ -179,7 +180,7 @@ export const getEmbedsFromCache = async (board: string, op: number, pid: string)
const cachedData = cthreadDataCache![target];
if (!cachedData)
return;
const ret: [EmbeddedFile[], boolean][] = [];
const ret: [WorkerEmbeddedFile[], boolean][] = [];
if ('pee' in cachedData.data) {
const files = await decodeCoom3Payload(Buffer.from(cachedData.data.pee.join(' ')));
ret.push([files, false]);
@ -203,12 +204,8 @@ export const getEmbedsFromCache = async (board: string, op: number, pid: string)
},
filename: fn,
thumbnail: csettings.hotlink ? (prev || full) : Buffer.from(await (await ifetch(prev || full)).arrayBuffer()),
data: csettings.hotlink ? (full || prev) : (async (lsn) => {
if (!cachedFile)
cachedFile = (await (await ifetch(full || prev, undefined, lsn)).arrayBuffer());
return Buffer.from(cachedFile);
})
} as EmbeddedFile];
data: csettings.hotlink ? (full || prev) : { url: full || prev }
} as WorkerEmbeddedFile];
ret.push([end, true]);
}
return ret;
@ -259,7 +256,7 @@ export const decodeCoom3Payload = async (buff: Buffer) => {
let [ptr, ptr2] = [hptr + 1, hptr + 1];
let fn = 'embedded';
let tags = [];
let thumb: EmbeddedFile['thumbnail'] = Buffer.from(thumbnail);
let thumb: WorkerEmbeddedFile['thumbnail'] = Buffer.from(thumbnail);
if (hasFn) {
while (header[ptr2] != 0)
ptr2++;
@ -275,25 +272,24 @@ export const decodeCoom3Payload = async (buff: Buffer) => {
if (hasThumbnail) {
thumbsize = header.readInt32LE(ptr);
ptr += 4;
if (header.byteLength < ptr + thumbsize)
if (header.byteLength >= ptr + thumbsize)
thumb = header.slice(ptr, ptr + thumbsize);
else
thumb = Buffer.from(await (await ifetch(pee, { headers: { 'user-agent': '', range: `bytes=${ptr}-${ptr + thumbsize}` } })).arrayBuffer());
ptr += thumbsize;
}
const unzip = async (lsn?: EventTarget) =>
Buffer.from(await (await ifetch(pee, { headers: { 'user-agent': '', range: `bytes=${ptr}-${size - 1}` } }, lsn)).arrayBuffer());
const unzip = { url: pee, headers: { 'user-agent': '', range: `bytes=${ptr}-${size - 1}` } };
let data;
data = unzip;
if (size < 3072) {
thumb = data = await unzip();
thumb = data = Buffer.from(await (await ifetch(unzip.url, { headers: unzip.headers })).arrayBuffer());
}
return {
filename: fn,
// if file is small, then just get it fully
data,
thumbnail: thumb,
} as EmbeddedFile;
} as WorkerEmbeddedFile;
} catch (e) {
// meanies trying to heck with bad links
console.warn(e);
@ -339,11 +335,11 @@ export const getSelectedFile = () => {
export async function embeddedToBlob(...efs: EmbeddedFile[]) {
return (await Promise.all(efs.map(async ef => {
let buff: Buffer;
let buff: Uint8Array;
if (typeof ef.data == "string") {
const req = await ifetch(ef.data);
buff = Buffer.from(await req.arrayBuffer());
} else if (!Buffer.isBuffer(ef.data))
} else if (!(ef.data instanceof Uint8Array))
buff = await ef.data();
else
buff = ef.data;

2
src/webm.ts

@ -1,6 +1,6 @@
import { Buffer } from "buffer";
import * as ebml from "ts-ebml";
import type { ImageProcessor } from "./main";
import type { ImageProcessor } from "./processor.worker";
import { decodeCoom3Payload, uploadFiles } from "./utils";
// unused, but will in case 4chan does file sig checks

6
src/websites/index.ts

@ -74,11 +74,7 @@ export const FoolFuuka: QueryProcessor = {
catalogControlHost: () => document.getElementById("index-options") as HTMLDivElement,
getImageLink: async function *(post: HTMLElement) {
if (location.host == "arch.b4k.co") { //get hecked
const pid = post.id.match(/\d+/)![0];
const board = location.pathname.match(/\/(..?.?)\//)![1];
const res = await ifetch(`https://archive.wakarimasen.moe/_/api/chan/post/?board=${board}&num=${pid}`);
const data = await res.json();
yield data.media.media_link;
return;
}
yield post.querySelector('a[rel]')?.getAttribute('href') || '';
},

Loading…
Cancel
Save