Browse Source

Add view count and server-cache support

pull/46/head
coomdev 2 years ago
parent
commit
64bb4227d0
  1. 42
      build-chrome.js
  2. 1
      build-ff.js
  3. BIN
      chrome/_metadata/generated_indexed_rulesets/_ruleset1
  4. 3
      chrome/b4k-csp.json
  5. 0
      chrome/bg.js
  6. 11
      chrome/manifest.json
  7. 8
      firefox/manifest.json
  8. 2
      firefox_update.json
  9. 2
      main.meta.js
  10. 779
      main.user.js
  11. 106
      package-lock.json
  12. 1
      package.json
  13. 23
      src/Components/App.svelte
  14. 57
      src/Components/ViewCount.svelte
  15. 4
      src/debounce.ts
  16. 19
      src/filehosts.ts
  17. 4
      src/global.css
  18. 100
      src/main.ts
  19. 32
      src/platform.ts
  20. 2
      src/stores.ts
  21. 96
      src/utils.ts
  22. 15
      src/websites/index.ts

42
build-chrome.js

@ -54,6 +54,7 @@ const domains = [
"https://*.donmai.us/*",
"https://*.lolibooru.moe/*",
"https://*.allthefallen.moe/*",
"https://desu-usergeneratedcontent.xyz/*"
];
const manif = {
@ -94,48 +95,45 @@ const manif = {
const manif3 = {
"manifest_version": 3,
manifest_version: 3,
// "update_url": "https://github.com/coomdev/pngextraembedder/raw/branch/%E4%B8%AD%E5%87%BA%E3%81%97/chrome_update.xml",
"name": "PngExtraEmbedder",
"description": "Discover embedded files on 4chan and archives!",
"version": "0." + rev,
"icons": {
"64": "1449696017588.png"
name: "PngExtraEmbedder",
description: "Discover embedded files on 4chan and archives!",
version: "0." + rev,
icons: {
64: "1449696017588.png"
},
"permissions": [
permissions: [
//"notifications",
//"clipboardWrite",
//"activeTab",
"declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback"
//"contextMenus",
],
host_permissions: domains,
//"host_permissions":["<all_urls>"],
"web_accessible_resources": [{
"resources": ["*.html", "*.js"],
"matches": ["<all_urls>"]
web_accessible_resources: [{
resources: ["*.html", "*.js"],
matches: ["<all_urls>"]
}],
"content_scripts": [
content_scripts: [
{
"matches": domains,
"css": [],
"run_at": "document_start",
"js": ["dist/main.js"],
matches: domains,
css: [],
run_at: "document_start",
js: ["dist/main.js"],
}
],
"declarative_net_request": {
"rule_resources": [
declarative_net_request: {
rule_resources: [
{
id: 'rule1',
enabled: true,
path: 'b4k-csp.json'
}
]
},
//"background": {
// hope I won't need that polyfill...
//"service_worker": "dist/background.js"
// }
}
};
(async () => {

1
build-ff.js

@ -48,6 +48,7 @@ const domains = [
"https://*.donmai.us/*",
"https://*.lolibooru.moe/*",
"https://*.allthefallen.moe/*",
"https://desu-usergeneratedcontent.xyz/*"
];
const manif = {

BIN
chrome/_metadata/generated_indexed_rulesets/_ruleset1

Binary file not shown.

3
chrome/b4k-csp.json

@ -11,8 +11,7 @@
]
},
"condition": {
"urlFilter": "abc",
"initiatorDomains": [
"requestDomains": [
"arch.b4k.co"
],
"resourceTypes": [

0
chrome/bg.js

11
chrome/manifest.json

@ -2,12 +2,13 @@
"manifest_version": 3,
"name": "PngExtraEmbedder",
"description": "Discover embedded files on 4chan and archives!",
"version": "0.228",
"version": "0.230",
"icons": {
"64": "1449696017588.png"
},
"permissions": [
"declarativeNetRequestWithHostAccess"
"declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback"
],
"host_permissions": [
"https://*.coom.tech/*",
@ -44,7 +45,8 @@
"https://*.rule34.xxx/*",
"https://*.donmai.us/*",
"https://*.lolibooru.moe/*",
"https://*.allthefallen.moe/*"
"https://*.allthefallen.moe/*",
"https://desu-usergeneratedcontent.xyz/*"
],
"web_accessible_resources": [
{
@ -94,7 +96,8 @@
"https://*.rule34.xxx/*",
"https://*.donmai.us/*",
"https://*.lolibooru.moe/*",
"https://*.allthefallen.moe/*"
"https://*.allthefallen.moe/*",
"https://desu-usergeneratedcontent.xyz/*"
],
"css": [],
"run_at": "document_start",

8
firefox/manifest.json

@ -7,7 +7,7 @@
},
"name": "PngExtraEmbedder",
"description": "Discover embedded files on 4chan and archives!",
"version": "0.228",
"version": "0.230",
"icons": {
"64": "1449696017588.png"
},
@ -50,7 +50,8 @@
"https://*.rule34.xxx/*",
"https://*.donmai.us/*",
"https://*.lolibooru.moe/*",
"https://*.allthefallen.moe/*"
"https://*.allthefallen.moe/*",
"https://desu-usergeneratedcontent.xyz/*"
],
"content_scripts": [
{
@ -89,7 +90,8 @@
"https://*.rule34.xxx/*",
"https://*.donmai.us/*",
"https://*.lolibooru.moe/*",
"https://*.allthefallen.moe/*"
"https://*.allthefallen.moe/*",
"https://desu-usergeneratedcontent.xyz/*"
],
"css": [],
"run_at": "document_start",

2
firefox_update.json

@ -1 +1 @@
{"addons":{"{34ac4994-07f2-44d2-8599-682516a6c6a6}":{"updates":[{"version":"0.228","update_link":"https://github.com/coomdev/pngextraembedder/raw/branch/%E4%B8%AD%E5%87%BA%E3%81%97/pee-firefox.zip"}]}}}
{"addons":{"{34ac4994-07f2-44d2-8599-682516a6c6a6}":{"updates":[{"version":"0.230","update_link":"https://github.com/coomdev/pngextraembedder/raw/branch/%E4%B8%AD%E5%87%BA%E3%81%97/pee-firefox.zip"}]}}}

2
main.meta.js

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

779
main.user.js

File diff suppressed because it is too large

106
package-lock.json

@ -21,6 +21,7 @@
"lodash": "^4.17.21",
"png-js": "^1.0.0",
"readable-stream": "^3.6.0",
"socks-proxy-agent": "^7.0.0",
"ts-ebml": "^2.0.2"
},
"devDependencies": {
@ -5757,6 +5758,11 @@
"url": "https://github.com/sindresorhus/invert-kv?sponsor=1"
}
},
"node_modules/ip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -8815,6 +8821,62 @@
"node": ">=8"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz",
"integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==",
"dependencies": {
"ip": "^1.1.5",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.13.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks-proxy-agent": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
"integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
"dependencies": {
"agent-base": "^6.0.2",
"debug": "^4.3.3",
"socks": "^2.6.2"
},
"engines": {
"node": ">= 10"
}
},
"node_modules/socks-proxy-agent/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socks-proxy-agent/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/sonic-boom": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz",
@ -14882,6 +14944,11 @@
"integrity": "sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==",
"dev": true
},
"ip": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz",
"integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg=="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -17255,6 +17322,45 @@
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="
},
"socks": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz",
"integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==",
"requires": {
"ip": "^1.1.5",
"smart-buffer": "^4.2.0"
}
},
"socks-proxy-agent": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz",
"integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==",
"requires": {
"agent-base": "^6.0.2",
"debug": "^4.3.3",
"socks": "^2.6.2"
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"sonic-boom": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.8.0.tgz",

1
package.json

@ -27,6 +27,7 @@
"lodash": "^4.17.21",
"png-js": "^1.0.0",
"readable-stream": "^3.6.0",
"socks-proxy-agent": "^7.0.0",
"ts-ebml": "^2.0.2"
},
"devDependencies": {

23
src/Components/App.svelte

@ -12,7 +12,8 @@
import { settings, appState } from "../stores";
import { filehosts } from "../filehosts";
import HydrusSearch from "./HydrusSearch.svelte";
import { ifetch } from "../platform";
import { ifetch } from "../platform";
import { writable } from "svelte/store";
let newbooru: Partial<Omit<Booru, "quirks"> & { view: string }> = {};
let dial: Dialog;
@ -67,6 +68,18 @@ import { ifetch } from "../platform";
onDestroy(() => {
document.removeEventListener("penis", penisEvent);
});
let cached = writable<boolean>(false);
settings.subscribe((val) => {
cached.set(
typeof val.cache == "boolean" ? val.cache : location.host.includes("b4k")
);
});
cached.subscribe((v) => {
$settings.cache = v;
});
</script>
<div class="backpanel" class:enabled={visible} class:disabled={!visible}>
@ -84,6 +97,14 @@ import { ifetch } from "../platform";
{/if}
</TabList>
<TabPanel>
<label>
<input type="checkbox" bind:checked={$cached} />
Try to load embeds from server cache
</label>
<label>
<input type="checkbox" bind:checked={$settings.dvc} />
Display view counts
</label>
<label>
<input type="checkbox" bind:checked={$settings.vercheck} />
Check for new versions at startup.

57
src/Components/ViewCount.svelte

@ -0,0 +1,57 @@
<script lang="ts">
import { settings } from "../stores";
import {
getThreadDataCache,
refreshThreadDataCache,
threadDataCache,
} from "../utils";
export let board: string;
export let op: number;
let loading = false;
export let pid: number;
const snooze = (n: number) => new Promise<void>((_) => setTimeout(_, n));
const execRefresh = async (trueRefresh: boolean) => {
loading = true;
let p: Promise<void>[] = [];
if (trueRefresh) {
p.push(snooze(250)); // meant for the user to see that the thing is being reloaded
p.push(refreshThreadDataCache(board, op));
} else
p.push(
(async () => {
await getThreadDataCache(board, op);
})()
);
await Promise.all(p);
loading = false;
};
settings.subscribe((newsetting) => {
if (newsetting.dvc) execRefresh(false);
});
</script>
{#if $settings.dvc}
<span title="click to refresh" on:click={() => execRefresh(true)} class="tag">
{#if loading}
...
{:else if ($threadDataCache || {})[pid]}
{($threadDataCache || {})[pid].cnt} views
{:else}
smth wrong...
{/if}
</span>
{/if}
<style scoped>
.tag {
padding: 5px;
border: 1px solid;
border-radius: 55px;
cursor: pointer;
display: inline-flex;
}
</style>

4
src/debounce.ts

@ -1,6 +1,6 @@
const nativeMax = Math.max;
const nativeMin = Math.min;
export function debounce(func: (...args: any[]) => any, wait: number, options: any) {
export function debounce<U extends any[], V, T extends (...args: U) => V>(func: T, wait: number, options: any): T {
let lastArgs : any,
lastThis: any,
maxWait: number | undefined,
@ -123,5 +123,5 @@ export function debounce(func: (...args: any[]) => any, wait: number, options: a
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
return debounced as any;
}

19
src/filehosts.ts

@ -44,6 +44,21 @@ export const catbox = (domain: string, serving: string) => ({
}
});
export const pomf = (domain: string, serving: string) => ({
domain,
serving,
async uploadFile(inj: Blob) {
const resp = await ifetch(`https://${domain}/upload.php`, {
method: 'POST',
body: parseForm({
'files[]': inj
})
});
const rfm = (await resp.json()).url;
return `https://a.pomf.cat/${rfm}`;
}
});
export type API = {
domain: string;
serving: string;
@ -52,7 +67,7 @@ export type API = {
export const filehosts: API[] = [
catbox('catbox.moe', 'files.catbox.moe'),
lolisafe('zz.ht', 'z.zz.fo'),
lolisafe('imouto.kawaii.su'),
catbox('pomf.moe', 'a.pomf.cat'),
lolisafe('take-me-to.space'),
lolisafe('zz.ht', 'z.zz.fo'),
];

4
src/global.css

@ -104,3 +104,7 @@ div.hasmultiple .catalog-host img {
.theme_default .post_wrapper > .thread_image_box > a {
margin-right: 20px;
}
div.post {
overflow: auto;
}

100
src/main.ts

@ -18,12 +18,13 @@ import Embeddings from './Components/Embeddings.svelte';
import EyeButton from './Components/EyeButton.svelte';
import NotificationsHandler from './Components/NotificationsHandler.svelte';
import { fireNotification, getSelectedFile } from "./utils";
import { decodeCoom3Payload, fireNotification, getEmbedsFromCache, getSelectedFile } from "./utils";
import { getQueryProcessor, QueryProcessor } from "./websites";
import { ifetch, Platform, streamRemote, supportedAltDomain } from "./platform";
import TextEmbeddingsSvelte from "./Components/TextEmbeddings.svelte";
import { HydrusClient } from "./hydrus";
import { registerPlugin } from 'linkifyjs';
import ViewCountSvelte from "./Components/ViewCount.svelte";
export interface ImageProcessor {
skip?: true;
@ -86,8 +87,8 @@ type EmbeddedFileWithoutPreview = {
export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview;
const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string, hex: string, prevurl: string, onfound: () => void): Promise<([EmbeddedFile[], boolean] | undefined)[]> => {
return Promise.all(processors.filter(e => e.match(fn)).map(async proc => {
const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string, hex: string, prevurl: string, onfound: () => void) => {
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
@ -95,7 +96,8 @@ const processImage = async (srcs: AsyncGenerator<string, void, void>, fn: string
if (await proc.has_embed(md5, fn, prevurl) === true) {
onfound();
return [await proc.extract(md5, fn), true] as [EmbeddedFile[], boolean];
} return;
}
return;
}
let succ = false;
let cumul: Buffer;
@ -133,13 +135,28 @@ 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;
let pendingPosts: { id: number, op: number }[] = [];
type ParametersExceptFirst<F> =
F extends (arg0: any, ...rest: infer R) => any ? R : never;
const buildCumFun = <T extends any[], U>(f: (args: T[]) => void, ...r: ParametersExceptFirst<typeof debounce>): (args: T) => void => {
let cumul: T[] = [];
const debounced = debounce(() => {
f(cumul);
cumul = [];
}, ...r);
return (newarg: T) => {
cumul.push(newarg);
debounced();
};
};
let pendingPosts: { id: number, op: number }[] = [];
// should be equivalent to buildCumFun(signalNewEmbeds, 5000, {trailing: true})
const signalNewEmbeds = debounce(async () => {
// ensure user explicitely enabled telemetry
if (!csettings.tm)
@ -165,6 +182,14 @@ const signalNewEmbeds = debounce(async () => {
}
}, 5000, { trailing: true });
const shouldUseCache = () => {
if (cappState.isCatalog)
return false;
return typeof csettings.cache == "boolean"
? csettings.cache
: location.hostname.includes('b4k');
};
const processPost = async (post: HTMLDivElement) => {
const origlink = qp.getImageLink(post);
if (!origlink)
@ -172,22 +197,29 @@ const processPost = async (post: HTMLDivElement) => {
const thumbLink = qp.getThumbnailLink(post);
if (!thumbLink)
return;
let res2 = await processImage(origlink, qp.getFilename(post), qp.getMD5(post), thumbLink,
() => {
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
let res2: [EmbeddedFile[], boolean][] | undefined = undefined;
if (shouldUseCache()) {
res2 = await getEmbedsFromCache(qp.getCurrentBoard(), +qp.getCurrentThread()!, post.id);
}
if (!res2) {
res2 = await processImage(origlink, qp.getFilename(post), qp.getMD5(post), thumbLink,
() => {
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
}
}
}
}
post.querySelector('.post')?.classList.add("embedfound");
});
res2 = res2?.filter(e => e);
post.querySelector('.post')?.classList.add("embedfound");
});
res2 = res2?.filter(e => e);
}
if (!res2 || res2.length == 0)
return;
processAttachments(post, res2?.flatMap(e => e![0].map(k => [k, e![1]] as [EmbeddedFile, boolean])));
@ -384,11 +416,8 @@ const startup = async (is4chanX = true) => {
customStyles.appendChild(document.createTextNode(globalCss));
document.documentElement.insertBefore(customStyles, null);
if (meta) {
meta.setAttribute('name', 'referrer');
if (!navigator.userAgent.includes('Firefox') && meta)
meta.setAttribute('content', 'no-referrer');
}
appState.set({ ...cappState, is4chanX });
const lqp = getQueryProcessor(is4chanX);
if (!lqp)
@ -648,6 +677,31 @@ function processAttachments(post: HTMLDivElement, ress: [EmbeddedFile, boolean][
cappState.foundPosts.push(replyBox as HTMLElement);
appState.set(cappState);
// attempt to load the view count, if it's found, attach it
(async () => {
const viewcounthost = document.createElement('div');
const pid = +post.id.slice(post.id.match(/\d/)!.index);
if (pid == qp.getCurrentThread()) {
viewcounthost.style.right = '0px';
viewcounthost.style.bottom = '0px';
viewcounthost.style.position = 'absolute';
} else {
viewcounthost.style.right = '0px';
viewcounthost.style.transform = 'translateX(calc(100% + 10px))';
viewcounthost.style.position = 'absolute';
}
new ViewCountSvelte({
target: viewcounthost,
props: {
board: qp.getCurrentBoard(),
op: cappState.isCatalog ? pid : qp.getCurrentThread(),
pid
}
});
replyBox.insertAdjacentElement("afterbegin", viewcounthost);
replyBox.style.position = 'relative';
})();
const isCatalog = replyBox?.classList.contains('catalog-post');
// add buttons
if (!isCatalog) {

32
src/platform.ts

@ -119,6 +119,34 @@ async function serialize(src: any): Promise<any> {
return ret;
}
function cleanupSerialized(src: any) {
if (typeof src != "object")
return src;
switch (src.cls) {
case 'FormData': {
for (const [key, items] of src.value) {
for (const item of items) {
cleanupSerialized(item);
}
}
break;
}
case 'File': {
URL.revokeObjectURL(src.value);
break;
}
case 'Blob': {
URL.revokeObjectURL(src.value);
break;
}
case 'Object': {
for (const prop in src.value) {
cleanupSerialized(src.value[prop]);
}
}
}
}
export const corsFetch = async (input: string, init?: RequestInit, lsn?: EventTarget) => {
const id = gid++;
@ -138,6 +166,10 @@ export const corsFetch = async (input: string, init?: RequestInit, lsn?: EventTa
start(controller) {
// something is finally ready to read
gcontroller = controller;
// at this point the background script already read all that it needed
// so we free up memory allocated for the request
if (execution_mode == "chrome_api" && init?.body)
cleanupSerialized(init.body);
// flush buffer
buffer.forEach(b => gcontroller?.enqueue(b));
buffer = [];

2
src/stores.ts

@ -28,11 +28,13 @@ export const initial_settings = localLoad('settingsv2', {
sh: false,
ep: false,
tm: false,
dvc: false,
expte: false,
mdist: -1,
phash: false,
hotlink: false,
vercheck: false,
cache: undefined as (boolean | undefined), // meaning defaults to false, except on b4k
fhost: 0,
maxe: 5,
conc: 8,

96
src/utils.ts

@ -6,6 +6,8 @@ import { filehosts } from "./filehosts";
import { getHeaders, ifetch, Platform } from "./platform";
import type { HydrusClient } from "./hydrus";
import { fileTypeFromBuffer } from "file-type";
import { writable } from "svelte/store";
import { init } from "svelte/internal";
export let csettings: Parameters<typeof settings['set']>[0];
@ -120,6 +122,89 @@ export const buildPeeFile = async (f: File) => {
return new Blob([ret]);
};
const getThreadInfo = async (board: string, op: number) => {
const res = await ((await fetch(`http://127.0.0.1:1488/data/${board}/${op}`)).json() as Promise<{
id: number;
cnt: number;
data: {
pee: string[]
} | {
third: any;
}
}[]>);
return Object.fromEntries(res.map(e => [e.id, e]));
};
export const threadDataCache = writable<undefined | {
[k in number]: {
id: number;
cnt: number;
mdist?: number;
data: {
pee: string[];
} | {
third: any;
};
}
}>();
let cthreadDataCache: Parameters<typeof threadDataCache['set']>[0];
threadDataCache.subscribe(newval => {
cthreadDataCache = newval;
});
export const refreshThreadDataCache = async (board: string, op: number) => {
threadDataCache.set(await getThreadInfo(board, op));
};
export const getThreadDataCache = async (board: string, op: number) => {
if (!cthreadDataCache)
await refreshThreadDataCache(board, op);
return threadDataCache;
};
export const getEmbedsFromCache = async (board: string, op: number, pid: string): Promise<[EmbeddedFile[], boolean][]> => {
await getThreadDataCache(board, op);
const target = +pid.slice(pid.match(/\d/)!.index);
const cachedData = cthreadDataCache![target];
if (!cachedData)
return [];
const ret: [EmbeddedFile[], boolean][] = [];
if ('pee' in cachedData.data) {
const files = await decodeCoom3Payload(Buffer.from(cachedData.data.pee.join(' ')));
ret.push([files, false]);
}
if ('third' in cachedData.data) {
if (csettings.phash) {
// if mdist is unknown (happens when no thumbnail was found, assume they are different)
if ((cachedData.mdist || Number.POSITIVE_INFINITY) < (csettings.mdist || 5))
return ret;
}
let cachedFile: ArrayBuffer;
const data = cachedData.data.third;
const prev = data.preview_url;
const full = data.full_url;
const fn = new URL(full).pathname.split('/').slice(-1)[0];
const end = [{
source: data.source,
page: {
title: 'PEE Cache',
url: data.page
},
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];
ret.push([end, true]);
}
return ret;
};
/*
header (must be < 2k): [1 byte bitfield](if hasfilename: null terminated string)(if has tags: [X null terminated string, tags are whitespace-separated])
(if has thumbnail: [thumbnail size X]
@ -145,7 +230,7 @@ export const decodeCoom3Payload = async (buff: Buffer) => {
const { domain, file } = m.groups!;
const headers = await getHeaders(pee);
const res = await ifetch(pee, {
headers: { range: 'bytes=0-2048', 'user-agent': '' },
headers: { range: 'bytes=0-16383', 'user-agent': '' },
mode: 'cors',
referrerPolicy: 'no-referrer',
});
@ -179,7 +264,10 @@ export const decodeCoom3Payload = async (buff: Buffer) => {
if (hasThumbnail) {
thumbsize = header.readInt32LE(ptr);
ptr += 4;
thumb = Buffer.from(await (await ifetch(pee, { headers: { 'user-agent': '', range: `bytes=${ptr}-${ptr + thumbsize}` } })).arrayBuffer());
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) =>
@ -199,7 +287,7 @@ export const decodeCoom3Payload = async (buff: Buffer) => {
// meanies trying to heck with bad links
console.warn(e);
}
}))).filter(e => e);
}))).filter(e => e).map(e => e!);
};
export const fireNotification = (type: 'success' | 'error' | 'info' | 'warning', content: string, lifetime = 3) => {
@ -285,7 +373,7 @@ export async function getFileFromHydrus(client: HydrusClient,
export function externalDispatch(name: string, data: any) {
let event: Event;
if (execution_mode == "ff_api") {
if (execution_mode == "ff_api") {
const clonedDetail = cloneInto(data, document.defaultView);
event = new CustomEvent(name, { detail: clonedDetail });
} else {

15
src/websites/index.ts

@ -13,6 +13,8 @@ export type QueryProcessor = {
getInfoBox: (post: HTMLElement) => HTMLElement;
getPostIdPrefix: () => string;
getTextBox: (post: HTMLElement) => HTMLElement;
getCurrentBoard: () => string;
getCurrentThread: () => number | undefined;
};
export const V4chan: QueryProcessor = {
@ -36,7 +38,9 @@ export const V4chan: QueryProcessor = {
getThumbnailLink: (post: HTMLElement) => post.querySelector("img[data-md5]")?.getAttribute("src") || '',
getInfoBox: post => post.querySelector("div.fileText")!,
getPostIdPrefix: () => 'p',
getTextBox: (post) => post.querySelector('blockquote')!
getTextBox: (post) => post.querySelector('blockquote')!,
getCurrentBoard: () => location.pathname.split('/')[1],
getCurrentThread: () => +location.pathname.split('/')[3]
};
export const X4chan: QueryProcessor = {
@ -57,7 +61,9 @@ export const X4chan: QueryProcessor = {
getThumbnailLink: (post: HTMLElement) => post.querySelector("img[data-md5]")?.getAttribute("src") || '',
getInfoBox: post => post.querySelector("span.file-info")!,
getPostIdPrefix: V4chan.getPostIdPrefix,
getTextBox: V4chan.getTextBox
getTextBox: V4chan.getTextBox,
getCurrentBoard: V4chan.getCurrentBoard,
getCurrentThread: V4chan.getCurrentThread,
};
export const FoolFuuka: QueryProcessor = {
@ -90,8 +96,9 @@ export const FoolFuuka: QueryProcessor = {
},
getInfoBox: post => post.querySelector("span.post_controls")!,
getPostIdPrefix: () => '',
getTextBox: post => post.querySelector('.text')!
getTextBox: post => post.querySelector('.text')!,
getCurrentBoard: V4chan.getCurrentBoard,
getCurrentThread: V4chan.getCurrentThread,
};
export const getQueryProcessor = (is4chanX: boolean) => {

Loading…
Cancel
Save