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. 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 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. 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({ esbuildSvelte({
compilerOptions: { css: true, accessors: true }, compilerOptions: { css: true, accessors: true },
preprocess: sveltePreprocess(), 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: { loader: {

38
build-ff.js

@ -2,6 +2,7 @@
import { spawnSync } from 'child_process'; import { spawnSync } from 'child_process';
import { writeFileSync, readFileSync, copyFileSync } from 'fs' import { writeFileSync, readFileSync, copyFileSync } from 'fs'
import inlineWorkerPlugin from 'esbuild-plugin-inline-worker';
import esbuild from "esbuild"; import esbuild from "esbuild";
import esbuildSvelte from "esbuild-svelte"; import esbuildSvelte from "esbuild-svelte";
@ -83,14 +84,14 @@ const manif = {
], ],
"web_accessible_resources": ["*.html", "*.js",], "web_accessible_resources": ["*.html", "*.js",],
// "background": { // "background": {
// persistent: true, // persistent: true,
// "scripts": [ // "scripts": [
// "polyfill.min.js", // "polyfill.min.js",
// "browser-polyfill.min.js", // "browser-polyfill.min.js",
// "dist/background.js" // "dist/background.js"
// ] // ]
// } // }
}; };
(async () => { (async () => {
@ -114,8 +115,25 @@ const manif = {
esbuildSvelte({ esbuildSvelte({
compilerOptions: { css: true, accessors: true }, compilerOptions: { css: true, accessors: true },
preprocess: sveltePreprocess(), 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: { loader: {
'.css': 'text', '.css': 'text',
'.png': 'binary' '.png': 'binary'

23
build.js

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

23
chrome/dist/background.js

@ -1876,9 +1876,12 @@
var lqueue = {}; var lqueue = {};
var localLoad = (key, def) => "__pee__" + key in localStorage ? JSON.parse(localStorage.getItem("__pee__" + key)) : def; 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 localSet = (key, value) => localStorage.setItem("__pee__" + key, JSON.stringify(value));
var { port1, port2 } = new MessageChannel(); var port1;
console.log("chrome_api", true); console.log("chrome_api", true);
if (false) { if (false) {
const nmc = new MessageChannel();
port1 = nmc.port1;
port2 = nmc.port2;
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.style.display = "none"; iframe.style.display = "none";
iframe.name = location.origin; iframe.name = location.origin;
@ -1886,7 +1889,6 @@
iframe.onload = _; iframe.onload = _;
}); });
iframe.src = `${chrome.runtime.getURL("")}options.html`; iframe.src = `${chrome.runtime.getURL("")}options.html`;
const meself2 = new URL(chrome.runtime.getURL("")).origin;
document.documentElement.appendChild(iframe); document.documentElement.appendChild(iframe);
iframeloaded.then(() => { iframeloaded.then(() => {
iframe.contentWindow?.postMessage("", "*", [port2]); iframe.contentWindow?.postMessage("", "*", [port2]);
@ -1895,12 +1897,27 @@
lqueue[ev.data.id](ev.data); 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 gid = 0;
var sendCmd = (cmd, tr) => { var sendCmd = (cmd, tr) => {
const prom = new Promise((_) => { const prom = new Promise((_) => {
const id = gid++; const id = gid++;
lqueue[id] = (e) => { lqueue[id] = (e) => {
_(e.res); _(e);
delete lqueue[id]; delete lqueue[id];
}; };
port1.postMessage({ id, ...cmd }, tr || []); 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, "manifest_version": 3,
"name": "PngExtraEmbedder", "name": "PngExtraEmbedder",
"description": "Discover embedded files on 4chan and archives!", "description": "Discover embedded files on 4chan and archives!",
"version": "0.291", "version": "0.292",
"icons": { "icons": {
"64": "1449696017588.png" "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 lqueue = {};
var localLoad = (key, def) => "__pee__" + key in localStorage ? JSON.parse(localStorage.getItem("__pee__" + key)) : def; 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 localSet = (key, value) => localStorage.setItem("__pee__" + key, JSON.stringify(value));
var { port1, port2 } = new MessageChannel(); var port1;
console.log("ff_api", true); console.log("ff_api", true);
if (false) { if (false) {
const nmc = new MessageChannel();
port1 = nmc.port1;
port2 = nmc.port2;
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.style.display = "none"; iframe.style.display = "none";
iframe.name = location.origin; iframe.name = location.origin;
@ -1886,7 +1889,6 @@
iframe.onload = _; iframe.onload = _;
}); });
iframe.src = `${chrome.runtime.getURL("")}options.html`; iframe.src = `${chrome.runtime.getURL("")}options.html`;
const meself2 = new URL(chrome.runtime.getURL("")).origin;
document.documentElement.appendChild(iframe); document.documentElement.appendChild(iframe);
iframeloaded.then(() => { iframeloaded.then(() => {
iframe.contentWindow?.postMessage("", "*", [port2]); iframe.contentWindow?.postMessage("", "*", [port2]);
@ -1895,12 +1897,27 @@
lqueue[ev.data.id](ev.data); 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 gid = 0;
var sendCmd = (cmd, tr) => { var sendCmd = (cmd, tr) => {
const prom = new Promise((_) => { const prom = new Promise((_) => {
const id = gid++; const id = gid++;
lqueue[id] = (e) => { lqueue[id] = (e) => {
_(e.res); _(e);
delete lqueue[id]; delete lqueue[id];
}; };
port1.postMessage({ id, ...cmd }, tr || []); 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", "name": "PngExtraEmbedder",
"description": "Discover embedded files on 4chan and archives!", "description": "Discover embedded files on 4chan and archives!",
"version": "0.291", "version": "0.292",
"icons": { "icons": {
"64": "1449696017588.png" "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 QR: any;
declare const BUILD_VERSION: [number, number]; 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 isBackground: boolean;
declare const chrome: typeof browser; declare const chrome: typeof browser;
declare const _DOMParser: typeof DOMParser; declare const _DOMParser: typeof DOMParser;

2
main.meta.js

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name PNGExtraEmbed // @name PNGExtraEmbed
// @namespace https://coom.tech/ // @namespace https://coom.tech/
// @version 0.291 // @version 0.292
// @description uhh // @description uhh
// @author You // @author You
// @match https://boards.4channel.org/* // @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; const thumb = file.thumbnail || file.data;
let type: FileTypeResult | undefined; let type: FileTypeResult | undefined;
if (typeof thumb != "string") { if (typeof thumb != "string") {
let buff = Buffer.isBuffer(thumb) ? thumb : await thumb(); let buff = thumb instanceof Uint8Array ? thumb : await thumb();
type = await fileTypeFromBuffer(buff); type = await fileTypeFromBuffer(buff);
if ( if (
!type && !type &&
@ -116,7 +116,7 @@ import { peeTarget } from "../utils";
lisn.addEventListener("progress", (e: any) => { lisn.addEventListener("progress", (e: any) => {
progress = e.detail; 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); type = await fileTypeFromBuffer(full);
if ( if (
!type && !type &&

2
src/Components/EyeButton.svelte

@ -31,7 +31,7 @@
a.style.display = "none"; a.style.display = "none";
let url: string; let url: string;
if (typeof file.data != "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); const type = await fileTypeFromBuffer(thumb);
url = URL.createObjectURL(new Blob([thumb], { type: type?.mime })); url = URL.createObjectURL(new Blob([thumb], { type: type?.mime }));
} else url = file.data; } 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) => { export const revbyte = (n: number, len = 8) => {
let acc = 0; let acc = 0;
let n2 = n; let n2 = n;
@ -142,8 +140,6 @@ export class BitstreamWriter {
} }
setBit(b: number) { setBit(b: number) {
if (b)
debugger;
let byte = this.buffer[0]; let byte = this.buffer[0];
byte |= b << (this._offset & 7); byte |= b << (this._offset & 7);
this.buffer[0] = byte; this.buffer[0] = byte;

6
src/jpg.ts

@ -1,5 +1,5 @@
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import type { ImageProcessor } from "./main"; import type { ImageProcessor } from "./processor.worker";
import { f5stego } from './f5stego'; import { f5stego } from './f5stego';
import { settings } from "./stores"; import { settings } from "./stores";
import { decodeCoom3Payload } from "./utils"; 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 // unfortunately, because of the way f5 work, we can't determine
// if there's an embedded message until we have the complete file // 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 // 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 { try {
const res = f5inst.extract(b); const res = f5inst.extract(b);
if (!res) 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 // if we reached here then ex is heckin cute and valid
return decodeCoom3Payload(Buffer.from(ex)); 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 { Buffer } from "buffer";
import { appState, settings, initial_settings } from "./stores"; import { appState, settings, initial_settings } from "./stores";
import { debounce } from './debounce'; import { debounce } from './debounce';
import globalCss from './global.css'; import globalCss from './global.css';
import pngv3 from "./pngv3"; import pngv3 from "./pngv3";
import webm from "./webm"; //import webm from "./webm";
import gif from "./gif"; //import gif from "./gif";
import jpg from "./jpg"; import jpg from "./jpg";
import thirdeye from "./thirdeye"; import thirdeye from "./thirdeye";
import pomf from "./pomf"; import pomf from "./pomf";
@ -21,27 +23,26 @@ import NotificationsHandler from './Components/NotificationsHandler.svelte';
import { fireNotification, getEmbedsFromCache, getSelectedFile } from "./utils"; import { fireNotification, getEmbedsFromCache, getSelectedFile } from "./utils";
import { getQueryProcessor, QueryProcessor } from "./websites"; 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 TextEmbeddingsSvelte from "./Components/TextEmbeddings.svelte";
import { HydrusClient } from "./hydrus"; import { HydrusClient } from "./hydrus";
import { registerPlugin } from 'linkifyjs'; import { registerPlugin } from 'linkifyjs';
import ViewCountSvelte from "./Components/ViewCount.svelte"; 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)) if (!supportedMainDomain(location.host) && !supportedAltDomain(location.host))
throw "PEE not supported here, skipping"; 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; let qp: QueryProcessor;
export let csettings: Parameters<typeof settings['set']>[0]; export let csettings: Parameters<typeof settings['set']>[0];
let processors: ImageProcessor[] = const processors: ImageProcessor[] =
[thirdeye, pomf, pngv3, jpg, webm, gif]; [thirdeye, pomf, pngv3, jpg];//, webm, gif
let cappState: Parameters<typeof appState['set']>[0]; let cappState: Parameters<typeof appState['set']>[0];
settings.subscribe(async b => { settings.subscribe(async b => {
@ -64,9 +65,9 @@ settings.subscribe(async b => {
} }
} }
csettings = b; csettings = b;
processors = [...(!csettings.te ? [thirdeye] : []), //processors = [...(!csettings.te ? [thirdeye] : []),
pngv3, pomf, jpg, webm, gif // pngv3, pomf, jpg, webm, gif
]; //];
}); });
@ -79,7 +80,7 @@ type EmbeddedFileWithPreview = {
source?: string; // can be like a twitter post this was posted in originally source?: string; // can be like a twitter post this was posted in originally
thumbnail: string | Buffer; thumbnail: string | Buffer;
filename: string; filename: string;
data: EmbeddedFileWithoutPreview['data'] | ((lisn?: EventTarget) => Promise<Buffer>); data: EmbeddedFileWithoutPreview['data'] | ((lisn?: EventTarget) => Promise<Uint8Array>);
}; };
type EmbeddedFileWithoutPreview = { type EmbeddedFileWithoutPreview = {
@ -92,6 +93,7 @@ type EmbeddedFileWithoutPreview = {
export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview; export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview;
/*
const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string, hex: string, prevurl: string) => { 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 => { const ret = await Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
if (proc.skip) { if (proc.skip) {
@ -109,7 +111,7 @@ const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string
try { try {
const n = await srcs.next(); const n = await srcs.next();
if (n.done) if (n.done)
return; // no more links to try return; // no more links to try
const iter = streamRemote(n.value); const iter = streamRemote(n.value);
if (!iter) if (!iter)
return; return;
@ -129,7 +131,7 @@ const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string
} }
found = v; 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; succ = true;
await iter.next(true); await iter.next(true);
if (found !== true) { if (found !== true) {
@ -143,7 +145,7 @@ const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string
} while (!succ); } while (!succ);
})); }));
return ret.filter(e => e).map(e => e!); return ret.filter(e => e).map(e => e!);
}; };*/
const textToElement = <T = HTMLElement>(s: string) => const textToElement = <T = HTMLElement>(s: string) =>
document.createRange().createContextualFragment(s).children[0] as any as T; 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 pendingPosts: { id: number, op: number }[] = [];
let pendingNoPosts: { id: number, op: number }[] = [];
// should be equivalent to buildCumFun(signalNewEmbeds, 5000, {trailing: true}) // should be equivalent to buildCumFun(signalNewEmbeds, 5000, {trailing: true})
const signalNewEmbeds = debounce(async () => { const signalNewEmbeds = debounce(async () => {
// ensure user explicitely enabled telemetry // ensure user explicitely enabled telemetry
@ -174,17 +178,22 @@ const signalNewEmbeds = debounce(async () => {
const boardname = location.pathname.match(/\/([^/]*)\//)![1]; const boardname = location.pathname.match(/\/([^/]*)\//)![1];
// restructure to minimize redundancy // 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)])); 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, { const res = await ifetch("https://shoujo.coom.tech/listing/" + boardname, {
method: "POST", method: "POST",
body: JSON.stringify(reshaped), body: JSON.stringify({
emb: reshaped,
noemb: reshaped2
}),
headers: { headers: {
'content-type': 'application/json' 'content-type': 'application/json'
} }
}); });
await res.json(); await res.json();
pendingPosts = []; pendingPosts = [];
pendingNoPosts = [];
} catch (e) { } catch (e) {
// silently fail // silently fail
console.error(e); console.error(e);
@ -201,6 +210,197 @@ const shouldUseCache = () => {
: location.hostname.includes('b4k'); : 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 processPost = async (post: HTMLDivElement) => {
const origlink = qp.getImageLink(post); const origlink = qp.getImageLink(post);
if (!origlink) if (!origlink)
@ -208,17 +408,17 @@ const processPost = async (post: HTMLDivElement) => {
const thumbLink = qp.getThumbnailLink(post); const thumbLink = qp.getThumbnailLink(post);
if (!thumbLink) if (!thumbLink)
return; return;
let res2: [EmbeddedFile[], boolean][] | undefined = undefined; let res2: [WorkerEmbeddedFile[], boolean][] | undefined = undefined;
const op = +location.pathname.match(/\/thread\/(.*)/)![1];
const reportEmbed = () => { const reportEmbed = () => {
if (!csettings) if (!csettings)
return false; return;
if (csettings.tm) { if (csettings.tm) {
// dont report results from archive, only live threads // dont report results from archive, only live threads
if (['boards.4chan.org', 'boards.4channel.org'].includes(location.host)) { if (['boards.4chan.org', 'boards.4channel.org'].includes(location.host)) {
if (!cappState.isCatalog) { // only save from within threads if (!cappState.isCatalog) { // only save from within threads
// we must be in a thread, thus the following is valid // 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 }); pendingPosts.push({ id: +(post.id.match(/([0-9]+)/)![1]), op });
signalNewEmbeds(); // let it run async 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 { try {
if (shouldUseCache()) { if (shouldUseCache()) {
res2 = await getEmbedsFromCache(qp.getCurrentBoard(), +qp.getCurrentThread()!, post.id); res2 = await getEmbedsFromCache(qp.getCurrentBoard(), +qp.getCurrentThread()!, post.id);
} }
if (!res2) { 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); res2 = res2?.filter(e => e);
} }
} catch (e) { } catch (e) {
@ -239,10 +456,10 @@ const processPost = async (post: HTMLDivElement) => {
return; return;
} }
if (!res2 || res2.length == 0) if (!res2 || res2.length == 0)
return; return reportNoEmbed();
reportEmbed(); reportEmbed();
post.querySelector('.post')?.classList.add("embedfound"); 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 () => { const versionCheck = async () => {
@ -269,156 +486,118 @@ function copyTextToClipboard(text: string) {
} }
const scrapeBoard = async (self: HTMLButtonElement) => { const scrapeBoard = async (self: HTMLButtonElement) => {
if (!csettings) /* if (!csettings)
return false; return false;
if (csettings.tm) { if (csettings.tm) {
fireNotification("success", "Scrapping board with telemetry on! Thank you for your service, selfless stranger ;_;7"); fireNotification("success", "Scrapping board with telemetry on! Thank you for your service, selfless stranger ;_;7");
} }
self.disabled = true; self.disabled = true;
self.textContent = "Searching..."; self.textContent = "Searching...";
const boardname = location.pathname.match(/\/([^/]*)\//)![1]; const boardname = location.pathname.match(/\/([^/]*)\//)![1];
const res = await ifetch(`https://a.4cdn.org/${boardname}/threads.json`); const res = await ifetch(`https://a.4cdn.org/${boardname}/threads.json`);
const pages = await res.json() as Page[]; const pages = await res.json() as Page[];
type Page = { threads: Thread[] } type Page = { threads: Thread[] }
type Thread = { no: number; posts: Post[] }; type Thread = { no: number; posts: Post[] };
type BasePost = { no: number, resto: number, tim: number }; type BasePost = { no: number, resto: number, tim: number };
type PostWithFile = BasePost & { tim: number, ext: string, md5: string, filename: string }; type PostWithFile = BasePost & { tim: number, ext: string, md5: string, filename: string };
type PostWithoutFile = BasePost & Record<string, unknown>; type PostWithoutFile = BasePost & Record<string, unknown>;
type Post = (PostWithoutFile | PostWithFile); type Post = (PostWithoutFile | PostWithFile);
fireNotification("info", "Fetching all threads..."); fireNotification("info", "Fetching all threads...");
const threads = (await Promise.all(pages const threads = (await Promise.all(pages
.reduce((a: Thread[], b: Page) => [...a, ...b.threads], []) .reduce((a: Thread[], b: Page) => [...a, ...b.threads], [])
.map(e => e.no) .map(e => e.no)
.map(async id => { .map(async id => {
try { try {
const res = await ifetch(`https://a.4cdn.org/${boardname}/thread/${id}.json`); const res = await ifetch(`https://a.4cdn.org/${boardname}/thread/${id}.json`);
return await res.json() as Thread; return await res.json() as Thread;
} catch { } catch {
return undefined; 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;
} }
} while (found !== false && !chunk.done); }))).filter(e => e).map(e => e as Thread);
await iter.next(true); const filenames = threads
return found === true; .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]);
const range = ~~(filenames.length / n) + 1;
const hasEmbed: typeof filenames = []; console.log(filenames);
const total = filenames.length; fireNotification("info", "Analyzing images...");
let processed = 0; const n = 1;
//console.log(posts);
const int = setInterval(() => { const processFile = (src: string, fn: string, hex: string) => {
fireNotification("info", `Processed [${processed} / ${total}] files`); return Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
}, 5000); if (proc.skip) {
const md5 = Buffer.from(hex, 'base64');
await Promise.all([...new Array(n + 1)].map(async (e, i) => { return await proc.has_embed(md5, fn);
const postsslice = filenames.slice(i * range, (i + 1) * range); }
for (const post of postsslice) { // TODO: Move this outside the loop?
try { const iter = streamRemote(src);
const res = await processFile(post[1], post[3], post[2]); if (!iter)
processed++; return false;
if (res.some(e => e)) { let cumul = Buffer.alloc(0);
hasEmbed.push(post); let found: boolean | undefined;
// dont report results from archive, only live threads let chunk: ReadableStreamDefaultReadResult<Buffer> = { done: true };
if (['boards.4chan.org', 'boards.4channel.org'].includes(location.host)) { do {
pendingPosts.push({ id: post[4], op: post[0] }); const { value, done } = await iter.next(typeof found === "boolean");
signalNewEmbeds(); // let it run async 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"); clearInterval(int);
await addFromSource(ndom.body, "https://based.coom.tech/fuuka.js");
await addFromSource(ndom.body, "https://based.coom.tech/lazyload.js"); const counters: Record<number, number> = {};
*/ for (const k of hasEmbed)
return [ndom.documentElement.innerHTML, evalWhenReady] as [string, string[]]; 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; let gmo: MutationObserver;
@ -564,6 +743,13 @@ const startup = async (is4chanX = true) => {
await bodyInit; 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')) { if (!is4chanX && location.host.startsWith('boards.4chan')) {
const notificationHost = document.createElement('span'); const notificationHost = document.createElement('span');
new NotificationsHandler({ new NotificationsHandler({
@ -793,7 +979,7 @@ function processAttachments(post: HTMLDivElement, ress: [EmbeddedFile, boolean][
target: textInsertCursor, target: textInsertCursor,
props: { props: {
files: ress.map(e => e[0]).filter(e => 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'; import { GM_fetch, GM_head, headerStringToObject } from './requests';
const lqueue = {} as any; export const lqueue = {} as any;
const localLoad = <T>(key: string, def: T) => const localLoad = <T>(key: string, def: T) =>
('__pee__' + key) in localStorage ('__pee__' + key) in localStorage
@ -10,9 +10,19 @@ const localLoad = <T>(key: string, def: T) =>
const localSet = (key: string, value: any) => const localSet = (key: string, value: any) =>
localStorage.setItem('__pee__' + key, JSON.stringify(value)); localStorage.setItem('__pee__' + key, JSON.stringify(value));
const { port1, port2 } = new MessageChannel(); export let port1: MessagePort;
let port2: MessagePort;
console.log(execution_mode, isBackground); 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 // It has to be a content script
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.style.display = 'none'; iframe.style.display = 'none';
@ -21,7 +31,7 @@ if (execution_mode != 'userscript' && !isBackground) {
iframe.onload = _; iframe.onload = _;
}); });
iframe.src = `${chrome.runtime.getURL('')}options.html`; 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); document.documentElement.appendChild(iframe);
iframeloaded.then(() => { iframeloaded.then(() => {
iframe.contentWindow?.postMessage('', '*', [port2]); 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; let gid = 0;
const visit = (e: any, cb: (e: any) => true | undefined) => { const visit = (e: any, cb: (e: any) => true | undefined) => {
@ -42,11 +70,11 @@ const visit = (e: any, cb: (e: any) => true | undefined) => {
cb(e); cb(e);
}; };
const sendCmd = <V>(cmd: any, tr?: Transferable[]) => { export const sendCmd = <V>(cmd: any, tr?: Transferable[]) => {
const prom = new Promise<V>(_ => { const prom = new Promise<V>(_ => {
const id = gid++; const id = gid++;
lqueue[id] = (e: any) => { lqueue[id] = (e: any) => {
_(e.res); _(e);
delete lqueue[id]; delete lqueue[id];
}; };
port1.postMessage({ id, ...cmd }, tr || []); 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) => { 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; return f;
// It has to be the background script // It has to be the background script
return (...args: U) => { return (...args: U) => {
@ -386,7 +414,6 @@ export async function* streamRemote(url: string, chunkSize = 72 * 1024, fetchRes
while (ptr != size) { while (ptr != size) {
//console.log('doing a fetch of ', url, ptr, ptr + fetchSize - 1); //console.log('doing a fetch of ', url, ptr, ptr + fetchSize - 1);
let obj: Record<string, string>; let obj: Record<string, string>;
const fres = await ifetch(url, { headers: { range: `bytes=${ptr}-${ptr + fetchSize - 1}` } }); const fres = await ifetch(url, { headers: { range: `bytes=${ptr}-${ptr + fetchSize - 1}` } });
if (execution_mode == "userscript") { if (execution_mode == "userscript") {
obj = headerStringToObject((fres as any as Tampermonkey.Response<any>).responseHeaders); obj = headerStringToObject((fres as any as Tampermonkey.Response<any>).responseHeaders);

31
src/pngv3.ts

@ -1,5 +1,5 @@
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import type { EmbeddedFile, ImageProcessor } from "./main"; import type { WorkerEmbeddedFile, ImageProcessor } from "./processor.worker";
import { PNGDecoder, PNGEncoder } from "./png"; import { PNGDecoder, PNGEncoder } from "./png";
import { decodeCoom3Payload } from "./utils"; import { decodeCoom3Payload } from "./utils";
import { settings } from "./stores"; import { settings } from "./stores";
@ -141,10 +141,10 @@ const extractFromRawDeflate = (b: Buffer) => {
return false; // possibly incorrect? return false; // possibly incorrect?
}; };
const extract = async (png: Buffer) => { const extract = async (png: Buffer, doextract = true) => {
const reader = BufferReadStream(png).getReader(); const reader = BufferReadStream(png).getReader();
const sneed = new PNGDecoder(reader, false); const sneed = new PNGDecoder(reader, false);
const ret: EmbeddedFile[] = []; const ret: WorkerEmbeddedFile[] = [];
let w: Buffer | undefined; let w: Buffer | undefined;
if (!csettings) if (!csettings)
throw new Error("Settings uninit"); throw new Error("Settings uninit");
@ -159,22 +159,28 @@ const extract = async (png: Buffer) => {
case 'tEXt': case 'tEXt':
buff = chunk; buff = chunk;
if (buff.slice(4, 4 + CUM3.length).equals(CUM3)) { if (buff.slice(4, 4 + CUM3.length).equals(CUM3)) {
if (!doextract)
return true;
const k = await decodeCoom3Payload(buff.slice(4 + CUM3.length)); 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 (buff.slice(4, 4 + CUM4.length).equals(CUM4)) {
if (!doextract)
return true;
const passed = buff.slice(4 + CUM4.length); const passed = buff.slice(4 + CUM4.length);
xor(passed, password); xor(passed, password);
const k = await decodeCoom3Payload(passed); 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 (buff.slice(4, 4 + CUM5.length).equals(CUM5)) {
if (!doextract)
return true;
const passed = buff.slice(4 + CUM5.length); const passed = buff.slice(4 + CUM5.length);
const decoded = Buffer.from(passed.toString(), 'base64').toString().split(' ').map(e => { const decoded = Buffer.from(passed.toString(), 'base64').toString().split(' ').map(e => {
return `https://${rprefs[e[0]]}/${e.slice(1)}`; return `https://${rprefs[e[0]]}/${e.slice(1)}`;
}).join(' '); }).join(' ');
const k = await decodeCoom3Payload(Buffer.from(decoded)); 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 // 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 // should also check if the id has a len of 6-8 or ends in .pee
return `https://${rprefs[e[0]]}/${e.slice(1)}`; return `https://${rprefs[e[0]]}/${e.slice(1)}`;
}).join(' '); }).join(' ');
if (!doextract)
return true;
const k = await decodeCoom3Payload(Buffer.from(decoded)); 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) { } 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 // should also check if the id has a len of 6-8 or ends in .pee
return `https://${rprefs[e[0]]}/${e.slice(1)}`; return `https://${rprefs[e[0]]}/${e.slice(1)}`;
}).join(' '); }).join(' ');
return decodeCoom3Payload(Buffer.from(dec)); if (doextract)
return decodeCoom3Payload(Buffer.from(dec));
return true;
} }
} catch (e) { } catch (e) {
console.error(e); if (e != "Uhh")
console.error(e);
} finally { } finally {
reader.releaseLock(); reader.releaseLock();
} }
@ -359,7 +370,7 @@ const inject = async (container: File, links: string[]) => {
}; };
const has_embed = async (png: Buffer) => { const has_embed = async (png: Buffer) => {
const r = await extract(png); const r = await extract(png, false);
return !!r; 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 { Buffer } from "buffer";
import thumbnail from "./assets/hasembed.png"; import thumbnail from "./assets/hasembed.png";
import { settings } from "./stores"; import { settings } from "./stores";
@ -16,7 +17,7 @@ settings.subscribe(b => {
}); });
const getExt = (fn: string) => { 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 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(/\[.*=(.*)\]/); const isExt = fn!.match(/\[.*=(.*)\]/);
let ext; let ext;
@ -61,15 +62,9 @@ const extract = async (b: Buffer, fn?: string) => {
return [{ return [{
filename: ext, filename: ext,
data: csettings.hotlink ? rsource! : async (lsn) => { data: csettings.hotlink ? rsource! : { url: rsource! },
try {
return Buffer.from(await (await ifetch(rsource, undefined, lsn)).arrayBuffer());
} catch (e) {
//404
}
},
thumbnail: Buffer.from(thumbnail) thumbnail: Buffer.from(thumbnail)
} as EmbeddedFile]; } as WorkerEmbeddedFile];
}; };
const has_embed = async (b: Buffer, fn?: string) => { const has_embed = async (b: Buffer, fn?: string) => {

215
src/processor.worker.ts

@ -1,16 +1,199 @@
//export default ((() => {/* */ }) as () => Worker); /// <reference lib="ES2021" />
//const exports = {}; /// <reference lib="webworker" />
//import * as platform from './platform'; import * as platform from './platform';
// import pngv3 from "./pngv3";
//console.log("Worker started"); //import webm from "./webm";
// //import gif from "./gif";
//const deserializeMessage = (m: any) => { import jpg from "./jpg";
// import thirdeye from "./thirdeye";
//} import pomf from "./pomf";
//
//(async () => { console.log("Worker started");
// onmessage = (msg: MessageEvent<any>) => {
// const des = deserializeMessage(msg); 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? // Todo: use GM get/setValue instead?
export const localLoad = async <T>(key: string, def: T) => { export const localLoad = async <T>(key: string, def: T) => {
if (execution_mode == "worker") {
return def;
}
const isinls = ('__pee__' + key) in localStorage; const isinls = ('__pee__' + key) in localStorage;
let ret: T; let ret: T;
if (isinls) { if (isinls) {
@ -31,6 +34,9 @@ export const localLoad = async <T>(key: string, def: T) => {
}; };
const localSet = (key: string, value: any) => { const localSet = (key: string, value: any) => {
if (execution_mode == "worker") {
return;
}
if (execution_mode != "userscript") if (execution_mode != "userscript")
chrome.storage.local.set({ [key]: JSON.stringify(value) }); chrome.storage.local.set({ [key]: JSON.stringify(value) });
else 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 { localLoad, settings } from "./stores";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import { decode } from 'jpeg-js'; import { decode } from 'jpeg-js';
@ -173,12 +174,8 @@ const extract = async (b: Buffer, fn?: string) => {
}, },
filename: fn!.substring(0, 33) + result[0].ext, filename: fn!.substring(0, 33) + result[0].ext,
thumbnail: csettings.hotlink ? (prev || full) : Buffer.from(await (await ifetch(prev || full)).arrayBuffer()), thumbnail: csettings.hotlink ? (prev || full) : Buffer.from(await (await ifetch(prev || full)).arrayBuffer()),
data: csettings.hotlink ? (full || prev) : (async (lsn) => { data: csettings.hotlink ? (full || prev) : { url: full || prev }
if (!cachedFile) } as WorkerEmbeddedFile];
cachedFile = (await (await ifetch(full || prev, undefined, lsn)).arrayBuffer());
return Buffer.from(cachedFile);
})
} as EmbeddedFile];
}; };
const phash = (b: Buffer) => { const phash = (b: Buffer) => {

30
src/utils.ts

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

2
src/webm.ts

@ -1,6 +1,6 @@
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import * as ebml from "ts-ebml"; import * as ebml from "ts-ebml";
import type { ImageProcessor } from "./main"; import type { ImageProcessor } from "./processor.worker";
import { decodeCoom3Payload, uploadFiles } from "./utils"; import { decodeCoom3Payload, uploadFiles } from "./utils";
// unused, but will in case 4chan does file sig checks // 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, catalogControlHost: () => document.getElementById("index-options") as HTMLDivElement,
getImageLink: async function *(post: HTMLElement) { getImageLink: async function *(post: HTMLElement) {
if (location.host == "arch.b4k.co") { //get hecked if (location.host == "arch.b4k.co") { //get hecked
const pid = post.id.match(/\d+/)![0]; return;
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;
} }
yield post.querySelector('a[rel]')?.getAttribute('href') || ''; yield post.querySelector('a[rel]')?.getAttribute('href') || '';
}, },

Loading…
Cancel
Save