2022-08-06 12:08:17 +00:00
/// <reference lib="ES2021" />
/// <reference lib="dom" />
2021-12-22 20:05:17 +00:00
import { Buffer } from "buffer" ;
2022-08-08 11:53:50 +00:00
import { appState , settings , initial_settings , localLoad } from "./stores" ;
2022-04-17 18:45:40 +00:00
import { debounce } from './debounce' ;
2022-01-06 05:59:52 +00:00
import globalCss from './global.css' ;
2022-01-02 14:37:19 +00:00
2022-02-13 17:55:41 +00:00
import pngv3 from "./pngv3" ;
2022-08-06 12:08:17 +00:00
//import webm from "./webm";
//import gif from "./gif";
2022-02-13 17:55:41 +00:00
import jpg from "./jpg" ;
2022-01-05 01:14:23 +00:00
import thirdeye from "./thirdeye" ;
2022-01-12 05:31:58 +00:00
import pomf from "./pomf" ;
2022-01-03 22:29:28 +00:00
2022-01-16 15:47:13 +00:00
import App from "./Components/App.svelte" ;
import ScrollHighlighter from "./Components/ScrollHighlighter.svelte" ;
import PostOptions from "./Components/PostOptions.svelte" ;
import SettingsButton from './Components/SettingsButton.svelte' ;
import Embeddings from './Components/Embeddings.svelte' ;
import EyeButton from './Components/EyeButton.svelte' ;
import NotificationsHandler from './Components/NotificationsHandler.svelte' ;
2022-04-17 18:45:40 +00:00
2022-07-23 13:30:46 +00:00
import { fireNotification , getEmbedsFromCache , getSelectedFile } from "./utils" ;
2022-01-16 13:34:14 +00:00
import { getQueryProcessor , QueryProcessor } from "./websites" ;
2022-08-08 16:50:26 +00:00
import { ifetch , Platform , sendCmd , lqueue , supportedAltDomain , supportedMainDomain , genPort , initMainIPC } from "./platform" ;
2022-04-12 23:56:18 +00:00
import TextEmbeddingsSvelte from "./Components/TextEmbeddings.svelte" ;
2022-04-14 19:52:21 +00:00
import { HydrusClient } from "./hydrus" ;
2022-04-17 18:45:40 +00:00
import { registerPlugin } from 'linkifyjs' ;
2022-06-08 22:35:05 +00:00
import ViewCountSvelte from "./Components/ViewCount.svelte" ;
2022-08-06 12:08:17 +00:00
import type { ImageProcessor , WorkerEmbeddedFile } from './processor.worker' ;
import ProcessWorkerAny from './processor.worker' ;
import { headerStringToObject } from "./requests" ;
const ProcessWorker = ProcessWorkerAny as ( ) = > Worker ;
2022-01-02 06:23:08 +00:00
2022-07-18 13:57:11 +00:00
if ( ! supportedMainDomain ( location . host ) && ! supportedAltDomain ( location . host ) )
throw "PEE not supported here, skipping" ;
2022-01-16 13:34:14 +00:00
let qp : QueryProcessor ;
2022-01-05 01:14:23 +00:00
2022-07-24 17:22:39 +00:00
export let csettings : Parameters < typeof settings [ ' set ' ] > [ 0 ] ;
2022-08-06 12:08:17 +00:00
const processors : ImageProcessor [ ] =
[ thirdeye , pomf , pngv3 , jpg ] ; //, webm, gif
2022-01-05 01:14:23 +00:00
2022-01-07 06:45:30 +00:00
let cappState : Parameters < typeof appState [ ' set ' ] > [ 0 ] ;
2022-04-14 19:52:21 +00:00
settings . subscribe ( async b = > {
2022-07-24 17:22:39 +00:00
if ( ! b ) return ;
2022-08-07 04:56:12 +00:00
csettings = b ;
2022-04-14 19:52:21 +00:00
if ( b . hyd ) {
// transition from disable to enabled
if ( b . ak ) {
const hydCli = new HydrusClient ( b . ak ) ;
console . log ( b . ak ) ;
let herror : string | undefined ;
try {
const valid = await hydCli . verify ( ) ;
if ( ! valid )
2022-04-17 18:45:40 +00:00
herror = "Hydrus appears to not be running or the key is wrong." ;
2022-04-14 19:52:21 +00:00
appState . set ( { . . . cappState , akValid : valid , client : hydCli , herror } ) ;
} catch {
herror = "Hydrus appears to not be running" ;
appState . set ( { . . . cappState , akValid : false , client : null , herror } ) ;
}
}
}
2022-08-06 12:08:17 +00:00
//processors = [...(!csettings.te ? [thirdeye] : []),
// pngv3, pomf, jpg, webm, gif
//];
2022-04-14 19:52:21 +00:00
2022-01-05 01:14:23 +00:00
} ) ;
2021-12-25 01:50:40 +00:00
2022-01-07 06:45:30 +00:00
appState . subscribe ( v = > {
cappState = v ;
} ) ;
2022-01-05 01:14:23 +00:00
type EmbeddedFileWithPreview = {
2022-01-08 22:08:20 +00:00
page ? : { title : string , url : string } ; // can be a booru page
2022-01-07 04:43:28 +00:00
source? : string ; // can be like a twitter post this was posted in originally
2022-01-29 20:01:45 +00:00
thumbnail : string | Buffer ;
2022-01-05 01:14:23 +00:00
filename : string ;
2022-08-06 12:08:17 +00:00
data : EmbeddedFileWithoutPreview [ 'data' ] | ( ( lisn? : EventTarget ) = > Promise < Uint8Array > ) ;
2022-01-05 01:14:23 +00:00
} ;
2021-12-22 20:05:17 +00:00
2022-01-05 01:14:23 +00:00
type EmbeddedFileWithoutPreview = {
2022-01-07 04:43:28 +00:00
page : undefined ;
source : undefined ;
2022-01-29 20:01:45 +00:00
thumbnail? : string ;
2022-01-05 01:14:23 +00:00
filename : string ;
2022-01-12 08:09:30 +00:00
data : string | Buffer ;
2022-01-05 01:14:23 +00:00
} ;
export type EmbeddedFile = EmbeddedFileWithPreview | EmbeddedFileWithoutPreview ;
2022-08-06 12:08:17 +00:00
/ *
2022-06-10 03:44:58 +00:00
const processImage = async ( srcs : AsyncGenerator < string , void , void > , fn : string , hex : string , prevurl : string ) = > {
2022-06-08 22:35:05 +00:00
const ret = await Promise . all ( processors . filter ( e = > e . match ( fn ) ) . map ( async proc = > {
2022-01-09 06:39:02 +00:00
if ( proc . skip ) {
// skip file downloading, file is referenced from the filename
// basically does things like filtering out blacklisted tags
const md5 = Buffer . from ( hex , 'base64' ) ;
2022-01-26 20:45:15 +00:00
if ( await proc . has_embed ( md5 , fn , prevurl ) === true ) {
2022-01-12 06:58:46 +00:00
return [ await proc . extract ( md5 , fn ) , true ] as [ EmbeddedFile [ ] , boolean ] ;
2022-06-08 22:35:05 +00:00
}
return ;
2022-01-04 15:36:43 +00:00
}
2022-04-30 00:03:49 +00:00
let succ = false ;
let cumul : Buffer ;
2022-01-09 06:39:02 +00:00
do {
2022-04-30 00:03:49 +00:00
try {
const n = await srcs . next ( ) ;
if ( n . done )
2022-08-06 12:08:17 +00:00
return ; // no more links to try
2022-04-30 00:03:49 +00:00
const iter = 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 ! ] ) ;
2022-07-19 15:29:49 +00:00
const v = await proc . has_embed ( cumul ) ;
if ( typeof v == "string" ) {
return [ await proc . extract ( cumul , v ) , false ] as [ EmbeddedFile [ ] , boolean ] ;
}
found = v ;
2022-04-30 00:03:49 +00:00
}
2022-08-06 12:08:17 +00:00
} while ( found !== false && ! chunk . done ) ;
2022-04-30 00:03:49 +00:00
succ = true ;
await iter . next ( true ) ;
2022-07-19 15:29:49 +00:00
if ( found !== true ) {
2022-04-30 00:03:49 +00:00
//console.log(`Gave up on ${src} after downloading ${cumul.byteLength} bytes...`);
return ;
}
return [ await proc . extract ( cumul ) , false ] as [ EmbeddedFile [ ] , boolean ] ;
} catch {
// ignore error and retry with another link
2022-04-28 10:35:22 +00:00
}
2022-04-30 00:03:49 +00:00
} while ( ! succ ) ;
2022-01-09 06:39:02 +00:00
} ) ) ;
2022-06-08 22:35:05 +00:00
return ret . filter ( e = > e ) . map ( e = > e ! ) ;
2022-08-06 12:08:17 +00:00
} ; * /
2021-12-22 20:05:17 +00:00
2022-01-02 13:12:19 +00:00
const textToElement = < T = HTMLElement > ( s : string ) = >
document . createRange ( ) . createContextualFragment ( s ) . children [ 0 ] as any as T ;
2022-06-08 22:35:05 +00:00
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 ( ) ;
} ;
} ;
2022-04-11 21:31:32 +00:00
2022-06-08 22:35:05 +00:00
let pendingPosts : { id : number , op : number } [ ] = [ ] ;
2022-08-06 12:08:17 +00:00
let pendingNoPosts : { id : number , op : number } [ ] = [ ] ;
2022-06-08 22:35:05 +00:00
// should be equivalent to buildCumFun(signalNewEmbeds, 5000, {trailing: true})
2022-04-17 18:45:40 +00:00
const signalNewEmbeds = debounce ( async ( ) = > {
2022-04-11 21:31:32 +00:00
// ensure user explicitely enabled telemetry
2022-07-24 17:22:39 +00:00
if ( ! csettings )
return ;
2022-04-11 21:31:32 +00:00
if ( ! csettings . tm )
return ;
try {
const boardname = location . pathname . match ( /\/([^/]*)\// ) ! [ 1 ] ;
// restructure to minimize redundancy
const reshaped = Object . fromEntries ( [ . . . new Set ( pendingPosts . map ( e = > e . op ) ) ] . map ( e = > [ e , pendingPosts . filter ( p = > p . op == e ) . map ( e = > e . id ) ] ) ) ;
2022-08-06 14:40:41 +00:00
const reshaped2 = Object . fromEntries ( [ . . . new Set ( pendingNoPosts . map ( e = > e . op ) ) ] . map ( e = > [ e , pendingNoPosts . filter ( p = > p . op == e ) . map ( e = > e . id ) ] ) ) ;
2022-08-06 12:08:17 +00:00
//console.log(reshaped);
2022-04-12 23:56:18 +00:00
2022-05-02 19:07:24 +00:00
const res = await ifetch ( "https://shoujo.coom.tech/listing/" + boardname , {
2022-04-11 21:31:32 +00:00
method : "POST" ,
2022-08-06 12:08:17 +00:00
body : JSON.stringify ( {
emb : reshaped ,
noemb : reshaped2
} ) ,
2022-04-11 21:31:32 +00:00
headers : {
'content-type' : 'application/json'
}
} ) ;
await res . json ( ) ;
pendingPosts = [ ] ;
2022-08-06 12:08:17 +00:00
pendingNoPosts = [ ] ;
2022-04-11 21:31:32 +00:00
} catch ( e ) {
// silently fail
console . error ( e ) ;
}
} , 5000 , { trailing : true } ) ;
2022-06-08 22:35:05 +00:00
const shouldUseCache = ( ) = > {
if ( cappState . isCatalog )
return false ;
2022-07-24 17:22:39 +00:00
if ( ! csettings )
return false ;
2022-06-08 22:35:05 +00:00
return typeof csettings . cache == "boolean"
? csettings . cache
: location . hostname . includes ( 'b4k' ) ;
} ;
2022-08-06 12:08:17 +00:00
let cp : CommandProcessor ;
2022-08-07 04:56:12 +00:00
class BackgroundEmulator {
async bgCorsFetch ( c : MessagePort ,
pendingFetches : Map < MessagePort , { [ id in number ] : { fetchFully : boolean } } > ,
id : number ,
input : string ,
init? : RequestInit ) {
try {
const res = await ifetch ( input , init ) ;
// 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 ) ;
c . postMessage ( {
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 ( [ 'GET' , 'POST' ] . includes ( init ? . method || 'GET' ) ) {
const data = await res . arrayBuffer ( ) ;
c . postMessage ( {
id ,
pushData : {
data
}
} , [ data ] ) ;
}
// let's hope these are delivered in order :%)
c . postMessage ( {
id ,
pushData : {
}
} , [ ] ) ;
} catch ( e ) {
c . postMessage ( {
id ,
ok : false ,
setRes : true ,
headers : { } ,
responseHeaders : '' ,
redirected : false ,
status : 400 ,
bodyUsed : false ,
statusText : 'shit broke' ,
} ) ;
}
}
constructor ( private port : MessagePort ) {
const pendingFetches = new Map < MessagePort , { [ id in number ] : { fetchFully : boolean } } > ( ) ;
port . onmessage = async ( obj : MessageEvent ) = > {
const { id , name , args , sid , fid , url } = obj . data as any ;
if ( name == "keepAlive" ) {
console . log ( 'im alive, tho?' ) ;
return ;
}
if ( name == "abortCorsFetch" ) {
//chrome.runtime.sendMessage({ name, sid });
return ;
}
if ( name == "corsFetch" ) {
// this handles the reply
( this . bgCorsFetch as any ) ( port , pendingFetches , id , . . . args ) ;
return ;
}
if ( name == "revoke" ) {
URL . revokeObjectURL ( url ! ) ;
port . postMessage ( {
id , ok : 1
} ) ;
return ;
}
if ( name == "fullyRead" ) {
const obj = pendingFetches . get ( port ) ! ;
if ( obj && fid ! in obj )
obj [ fid ! ] . fetchFully = true ;
port . postMessage ( {
id , ok : 1
} ) ;
return ;
}
const res = await ( Platform as any ) [ name ] ( . . . args ) ;
port . postMessage ( {
id , res
} ) ;
} ;
}
}
2022-08-06 12:08:17 +00:00
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 '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 ;
}
2022-08-07 04:56:12 +00:00
2022-08-06 12:08:17 +00:00
} ;
2022-08-07 04:56:12 +00:00
if ( execution_mode != "userscript" ) {
const ipcport = genPort ( ) ;
this . processor . postMessage ( { type : 'ipc' , port : ipcport } , [ ipcport ] ) ;
} else {
const nmc = new MessageChannel ( ) ;
const port1 = nmc . port1 ;
const port2 = nmc . port2 ;
new BackgroundEmulator ( port2 ) ;
const ipcport = port1 ;
this . processor . postMessage ( { type : 'ipc' , port : ipcport } , [ ipcport ] ) ;
}
2022-08-06 12:08:17 +00:00
}
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 ;
2022-08-15 07:10:59 +00:00
ret = { . . . wef } as any ;
2022-08-06 12:08:17 +00:00
// handles bigger files where data is represented as a {url, header} object
if ( typeof wef . data == "object" ) {
2022-08-08 16:50:26 +00:00
if ( 'url' in wef . data ) {
2022-08-06 12:08:17 +00:00
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 ( ) ) ;
}
} ;
}
}
2022-08-08 16:50:26 +00:00
if ( typeof wef . data != "string" && ! ( 'url' in wef . data ) ) {
2022-08-06 12:08:17 +00:00
ret . data = Buffer . from ( wef . data ) ;
}
2022-08-08 16:50:26 +00:00
if ( wef . thumbnail && typeof wef . thumbnail != "string" ) {
2022-08-06 12:08:17 +00:00
ret . thumbnail = Buffer . from ( wef . thumbnail ) ;
}
return ret ! ;
} ;
2022-08-08 11:53:50 +00:00
const reportEmbed = ( post : HTMLDivElement , op : number ) = > {
if ( ! csettings )
2022-01-26 20:45:15 +00:00
return ;
2022-08-08 11:53:50 +00:00
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
pendingPosts . push ( { id : + ( post . id . match ( /([0-9]+)/ ) ! [ 1 ] ) , op } ) ;
signalNewEmbeds ( ) ; // let it run async
2022-06-10 03:44:58 +00:00
}
}
2022-08-08 11:53:50 +00:00
}
} ;
2022-06-10 03:44:58 +00:00
2022-08-08 11:53:50 +00:00
const reportNoEmbed = ( post : HTMLDivElement , op : number ) = > {
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
2022-08-06 12:08:17 +00:00
}
}
2022-08-08 11:53:50 +00:00
}
} ;
const processed = new Set < string > ( ) ;
2022-08-06 12:08:17 +00:00
2022-08-08 11:53:50 +00:00
const processPost = async ( post : HTMLDivElement ) = > {
let inc = true ;
2022-06-26 19:03:31 +00:00
try {
2022-08-08 11:53:50 +00:00
if ( processed . has ( post . id ) ) {
inc = false ;
return ;
}
const origlink = qp . getImageLink ( post ) ;
if ( ! origlink )
return ;
const thumbLink = qp . getThumbnailLink ( post ) ;
if ( ! thumbLink )
return ;
let res2 : [ WorkerEmbeddedFile [ ] , boolean ] [ ] | undefined = undefined ;
let op : number ;
if ( cappState . isCatalog )
op = + post . id . slice ( 2 ) ;
else
op = + location . pathname . match ( /\/thread\/(.*)/ ) ! [ 1 ] ;
2022-06-26 19:03:31 +00:00
if ( shouldUseCache ( ) ) {
res2 = await getEmbedsFromCache ( qp . getCurrentBoard ( ) , + qp . getCurrentThread ( ) ! , post . id ) ;
}
if ( ! res2 ) {
2022-08-06 12:08:17 +00:00
res2 = [ ] ;
const tmp = await cp . processImage ( origlink , qp . getFilename ( post ) , qp . getMD5 ( post ) , thumbLink ) ;
res2 . push ( . . . tmp ) ;
2022-06-26 19:03:31 +00:00
res2 = res2 ? . filter ( e = > e ) ;
}
2022-08-08 11:53:50 +00:00
if ( ! res2 || res2 . length == 0 )
return reportNoEmbed ( post , op ) ;
reportEmbed ( post , op ) ;
post . querySelector ( '.post' ) ? . classList . add ( "embedfound" ) ;
processAttachments ( post , res2 ? . flatMap ( e = > e ! [ 0 ] . map ( k = > [ convertToLocalEmbed ( k ) , e ! [ 1 ] ] as [ EmbeddedFile , boolean ] ) ) ) ;
2022-06-26 19:03:31 +00:00
} catch ( e ) {
console . error ( e ) ;
return ;
2022-08-08 11:53:50 +00:00
} finally {
processed . add ( post . id ) ;
if ( inc )
appState . update ( v = > { return v . processed ++ , v ; } ) ;
2022-06-08 22:35:05 +00:00
}
2022-01-01 18:52:50 +00:00
} ;
2021-12-22 20:05:17 +00:00
2022-01-13 07:38:29 +00:00
const versionCheck = async ( ) = > {
2022-05-10 03:41:26 +00:00
const txt = ( await ( await ifetch ( "https://raw.githubusercontent.com/coomdev/pngextraembedder/main/main.meta.js" ) ) . text ( ) ) ;
2022-05-02 19:07:24 +00:00
const [ lmajor , lminor ] = txt . split ( '\n' )
. filter ( e = > e . includes ( "// @version" ) ) [ 0 ] . match ( /.*version\s+(.*)/ ) ! [ 1 ] . split ( '.' )
. map ( e = > + e ) ;
2022-01-29 20:01:45 +00:00
const [ major , minor ] = BUILD_VERSION ;
2022-01-13 07:38:29 +00:00
if ( major < lmajor || ( major == lmajor && minor < lminor ) ) {
fireNotification ( "info" , ` Last PEE version is ${ lmajor } . ${ lminor } , you're on ${ major } . ${ minor } ` ) ;
}
} ;
2022-01-19 12:53:40 +00:00
// Not using the clipboard API because it needs focus
function copyTextToClipboard ( text : string ) {
const copyFrom = document . createElement ( "textarea" ) ;
copyFrom . textContent = text ;
document . body . appendChild ( copyFrom ) ;
copyFrom . select ( ) ;
document . execCommand ( 'copy' ) ;
copyFrom . blur ( ) ;
document . body . removeChild ( copyFrom ) ;
2022-01-21 08:18:20 +00:00
navigator . clipboard . writeText ( text ) ;
2022-01-19 12:53:40 +00:00
}
2022-01-13 20:14:29 +00:00
const scrapeBoard = async ( self : HTMLButtonElement ) = > {
2022-08-06 12:08:17 +00:00
/ * i f ( ! c s e t t i n g s )
return false ;
if ( csettings . tm ) {
fireNotification ( "success" , "Scrapping board with telemetry on! Thank you for your service, selfless stranger ;_;7" ) ;
}
self . disabled = true ;
self . textContent = "Searching..." ;
const boardname = location . pathname . match ( /\/([^/]*)\// ) ! [ 1 ] ;
const res = await ifetch ( ` https://a.4cdn.org/ ${ boardname } /threads.json ` ) ;
const pages = await res . json ( ) as Page [ ] ;
type Page = { threads : Thread [ ] }
type Thread = { no : number ; posts : Post [ ] } ;
type BasePost = { no : number , resto : number , tim : number } ;
type PostWithFile = BasePost & { tim : number , ext : string , md5 : string , filename : string } ;
type PostWithoutFile = BasePost & Record < string , unknown > ;
type Post = ( PostWithoutFile | PostWithFile ) ;
fireNotification ( "info" , "Fetching all threads..." ) ;
const threads = ( await Promise . all ( pages
. reduce ( ( a : Thread [ ] , b : Page ) = > [ . . . a , . . . b . threads ] , [ ] )
. map ( e = > e . no )
. map ( async id = > {
try {
const res = await ifetch ( ` https://a.4cdn.org/ ${ boardname } /thread/ ${ id } .json ` ) ;
return await res . json ( ) as Thread ;
} catch {
return undefined ;
2022-04-28 10:35:22 +00:00
}
2022-08-06 12:08:17 +00:00
} ) ) ) . filter ( e = > e ) . map ( e = > e as Thread ) ;
const filenames = threads
. reduce ( ( a , b ) = > [ . . . a , . . . b . posts . filter ( p = > p . ext )
. map ( p = > p as PostWithFile ) ] , [ ] as PostWithFile [ ] ) . filter ( p = > p . ext != '.webm' && p . ext != '.gif' )
. map ( p = > [ p . resto || p . no , ` https://i.4cdn.org/ ${ boardname } / ${ p . tim } ${ p . ext } ` , p . md5 , p . filename + p . ext , p . no ] as [ number , string , string , string , number ] ) ;
console . log ( filenames ) ;
fireNotification ( "info" , "Analyzing images..." ) ;
const n = 1 ;
//console.log(posts);
const processFile = ( src : string , fn : string , hex : string ) = > {
return Promise . all ( processors . filter ( e = > e . match ( fn ) ) . map ( async proc = > {
if ( proc . skip ) {
const md5 = Buffer . from ( hex , 'base64' ) ;
return await proc . has_embed ( md5 , fn ) ;
}
// TODO: Move this outside the loop?
const iter = streamRemote ( src ) ;
if ( ! iter )
return false ;
let cumul = Buffer . alloc ( 0 ) ;
let found : boolean | undefined ;
let chunk : ReadableStreamDefaultReadResult < Buffer > = { done : true } ;
do {
const { value , done } = await iter . next ( typeof found === "boolean" ) ;
if ( done ) {
chunk = { done : true } as ReadableStreamDefaultReadDoneResult ;
} else {
chunk = { done : false , value } as ReadableStreamDefaultReadValueResult < Buffer > ;
cumul = Buffer . concat ( [ cumul , value ! ] ) ;
const v = await proc . has_embed ( cumul ) ;
if ( typeof v == "string" ) {
return true ;
}
found = v ;
}
} while ( found !== false && ! chunk . done ) ;
await iter . next ( true ) ;
return found === true ;
} ) ) ;
} ;
const range = ~ ~ ( filenames . length / n ) + 1 ;
const hasEmbed : typeof filenames = [ ] ;
const total = filenames . length ;
let processed = 0 ;
const int = setInterval ( ( ) = > {
fireNotification ( "info" , ` Processed [ ${ processed } / ${ total } ] files ` ) ;
} , 5000 ) ;
await Promise . all ( [ . . . new Array ( n + 1 ) ] . map ( async ( e , i ) = > {
const postsslice = filenames . slice ( i * range , ( i + 1 ) * range ) ;
for ( const post of postsslice ) {
try {
const res = await processFile ( post [ 1 ] , post [ 3 ] , post [ 2 ] ) ;
processed ++ ;
if ( res . some ( e = > e ) ) {
hasEmbed . push ( post ) ;
// dont report results from archive, only live threads
if ( [ 'boards.4chan.org' , 'boards.4channel.org' ] . includes ( location . host ) ) {
pendingPosts . push ( { id : post [ 4 ] , op : post [ 0 ] } ) ;
signalNewEmbeds ( ) ; // let it run async
}
2022-04-11 21:31:32 +00:00
}
2022-08-06 12:08:17 +00:00
} catch ( e ) {
console . log ( e ) ;
2022-01-13 20:14:29 +00:00
}
}
2022-08-06 12:08:17 +00:00
} ) ) ;
2022-05-02 19:07:24 +00:00
2022-08-06 12:08:17 +00:00
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 ) ;
} ; * /
2022-04-29 01:36:46 +00:00
} ;
let gmo : MutationObserver ;
const earlystartup = async ( ) = > {
2022-07-18 13:57:11 +00:00
if ( [ 'arch.b4k.co' , 'desuarchive.org' ] . includes ( location . host ) && execution_mode == "userscript" ) {
2022-05-02 19:07:24 +00:00
if ( ! GM_getValue ( "warning_seen2" , false ) ) {
2022-07-18 13:57:11 +00:00
alert ( ` Due to b4k and desuarchive policies being mean, PEE will get you banned, so the userscript version is disabled here \ n ` + "Use the WebExtension version of PEE if you want to use b4k!" ) ; // "Cool new features will be coming to it, too", then MV3 happened.
2022-05-02 19:07:24 +00:00
GM_setValue ( "warning_seen2" , true ) ;
2022-04-29 01:36:46 +00:00
return false ;
}
}
2022-04-29 02:33:13 +00:00
return true ;
2022-04-29 01:36:46 +00:00
} ;
2022-06-12 05:27:54 +00:00
let init = false ;
2022-04-29 01:36:46 +00:00
const startup = async ( is4chanX = true ) = > {
2022-06-12 05:27:54 +00:00
if ( init )
return ;
init = true ;
2022-09-14 15:59:00 +00:00
2022-09-01 08:50:16 +00:00
window . addEventListener ( "securitypolicyviolation" , ( e ) = > {
if ( e . blockedURI . startsWith ( "blob" ) ) {
if ( supportedMainDomain ( location . host ) ) {
fireNotification ( "error" , "CSP is preventing PEE from running, add blob: to the allowed JS whitelist in 4chanX in Advanced Settings" ) ;
} else if ( supportedAltDomain ( location . host ) ) {
fireNotification ( "error" , "CSP is preventing PEE from running, use a CSP bypass extension like PEE-companion" ) ;
}
}
} ) ;
2022-09-14 15:59:00 +00:00
let meta = document . querySelector ( 'meta[name="referrer"]' ) as HTMLMetaElement ;
2022-04-29 01:36:46 +00:00
const customStyles = document . createElement ( 'style' ) ;
2022-05-02 19:07:24 +00:00
2022-04-29 01:36:46 +00:00
customStyles . appendChild ( document . createTextNode ( globalCss ) ) ;
document . documentElement . insertBefore ( customStyles , null ) ;
2022-02-13 17:55:41 +00:00
2022-06-08 22:35:05 +00:00
if ( ! navigator . userAgent . includes ( 'Firefox' ) && meta )
2022-02-13 17:55:41 +00:00
meta . setAttribute ( 'content' , 'no-referrer' ) ;
2022-09-14 15:59:00 +00:00
meta = document . createElement ( 'meta' ) ;
meta . setAttribute ( "http-equiv" , "Content-Security-Policy" ) ;
meta . setAttribute ( "content" , "worker-src 'self' blob:" ) ;
document . head . append ( meta ) ;
2022-01-15 12:33:29 +00:00
appState . set ( { . . . cappState , is4chanX } ) ;
2022-01-16 13:34:14 +00:00
const lqp = getQueryProcessor ( is4chanX ) ;
if ( ! lqp )
return ;
else
qp = lqp ;
2021-12-22 20:05:17 +00:00
2022-08-08 16:50:26 +00:00
if ( execution_mode != 'userscript' )
await initMainIPC ( ) ;
2022-08-08 11:53:50 +00:00
const nset = await localLoad ( 'settingsv2' , initial_settings ) ;
settings . set ( nset ) ;
2022-07-24 17:22:39 +00:00
if ( ! csettings )
return false ;
2022-01-13 07:38:29 +00:00
if ( csettings . vercheck )
versionCheck ( ) ;
2022-01-16 13:34:14 +00:00
2022-04-17 18:45:40 +00:00
const postQuote = ( { scanner , parser , utils } : any ) = > {
const { CLOSEANGLEBRACKET , NUM } = scanner . tokens ;
const START_STATE = parser . start ;
const pref = qp . getPostIdPrefix ( ) ;
const endQuote = utils . createTokenClass ( 'postQuote' , {
isLink : true ,
toHref() {
return ` # ${ pref } ${ this . toString ( ) . substr ( 2 ) } ` ;
}
} ) ;
// A post quote (>>123456789) is made of
const MEMEARROW1 = START_STATE . tt ( CLOSEANGLEBRACKET ) ; // One meme arrow followed by
const MEMEARROW2 = MEMEARROW1 . tt ( CLOSEANGLEBRACKET ) ; // another meme arrow, terminated by
const POSTNUM_STATE = MEMEARROW2 . tt ( NUM , endQuote ) ; // a number
} ;
registerPlugin ( 'quote' , postQuote ) ;
2022-01-18 09:03:32 +00:00
if ( ! is4chanX && location . host . startsWith ( 'boards.4chan' ) ) {
2022-05-03 00:55:49 +00:00
const QRObs = new MutationObserver ( rec = > {
rec . forEach ( m = > {
m . addedNodes . forEach ( no = > {
if ( ( no as HTMLElement ) . id != "quickReply" ) {
return ;
}
document . dispatchEvent ( new CustomEvent ( "QRDialogCreation" , {
detail : no
} ) ) ;
} ) ;
} ) ;
} ) ;
// only need immediate children of body
QRObs . observe ( document . body , { childList : true } ) ;
2022-01-16 13:34:14 +00:00
document . addEventListener ( "QRGetFile" , ( e ) = > {
const qr = document . getElementById ( 'qrFile' ) as HTMLInputElement | null ;
document . dispatchEvent ( new CustomEvent ( "QRFile" , { detail : ( qr ? . files || [ ] ) [ 0 ] } ) ) ;
} ) ;
document . addEventListener ( "QRSetFile" , ( ( e : CustomEvent < { file : Blob , name : string } > ) = > {
const qr = document . getElementById ( 'qrFile' ) as HTMLInputElement | null ;
if ( ! qr ) return ;
const dt = new DataTransfer ( ) ;
dt . items . add ( new File ( [ e . detail . file ] , e . detail . name ) ) ;
qr . files = dt . files ;
} ) as any ) ;
}
2022-01-09 19:41:04 +00:00
//await Promise.all([...document.querySelectorAll('.postContainer')].filter(e => e.textContent?.includes("191 KB")).map(e => processPost(e as any)));
2021-12-23 01:19:08 +00:00
2022-01-04 15:36:43 +00:00
// keep this to handle posts getting inlined
2022-07-23 13:30:46 +00:00
if ( ! cappState . isCatalog ) {
const mo = new MutationObserver ( reco = > {
for ( const rec of reco )
if ( rec . type == "childList" )
rec . addedNodes . forEach ( e = > {
if ( ! ( e instanceof HTMLElement ) )
return ;
2022-07-24 17:22:39 +00:00
if ( ! csettings )
return false ;
2022-07-23 13:30:46 +00:00
if ( cappState . isCatalog && csettings . notcata )
return ;
// apparently querySelector cannot select the root element if it matches
let el = qp . postsWithFiles ( e ) ;
if ( ! el && e . classList . contains ( 'postContainer' ) )
el = [ e ] ;
2022-08-08 11:53:50 +00:00
if ( el ) {
2022-08-15 07:10:59 +00:00
for ( const e of el ) {
if ( processed . has ( e . id ) )
continue ;
appState . update ( v = > {
v . processing += 1 ;
return v ;
} ) ;
processPost ( e as HTMLDivElement ) ;
}
2022-08-08 11:53:50 +00:00
}
2022-07-23 13:30:46 +00:00
} ) ;
} ) ;
2022-01-02 13:12:19 +00:00
2022-07-23 13:30:46 +00:00
document . querySelectorAll ( '.board' ) . forEach ( e = > {
mo . observe ( e ! , { childList : true , subtree : true } ) ;
} ) ;
}
2022-07-24 17:22:39 +00:00
2022-05-02 19:07:24 +00:00
if ( ! document . body ) {
let bodyRes : any ;
const bodyInit = new Promise ( r = > bodyRes = r ) ;
const mo2 = new MutationObserver ( r = > {
if ( document . body ) {
mo2 . disconnect ( ) ;
bodyRes ( ) ;
}
} ) ;
mo2 . observe ( document . documentElement , { childList : true , subtree : true } ) ;
await bodyInit ;
}
2022-09-14 16:21:09 +00:00
document . addEventListener ( 'QRDialogCreation' , < any > ( ( e : CustomEvent < HTMLElement > ) = > {
const a = document . createElement ( 'span' ) ;
const po = new PostOptions ( {
target : a ,
props : { processors , textinput : ( e . detail || e . target ) . querySelector ( 'textarea' ) ! }
} ) ;
let prevFile : File ;
let target ;
const somethingChanged = async ( m : any ) = > {
// file possibly changed
const currentFile = await getSelectedFile ( ) ;
if ( prevFile != currentFile ) {
prevFile = currentFile ;
document . dispatchEvent ( new CustomEvent ( "PEEFile" , { detail : prevFile } ) ) ;
}
} ;
const obs = new MutationObserver ( somethingChanged ) ;
if ( ! cappState . is4chanX ) {
target = e . detail ;
a . style . display = "inline-block" ;
target . querySelector ( "input[type=submit]" ) ? . insertAdjacentElement ( "beforebegin" , a ) ;
const filesinp = target . querySelector ( '#qrFile' ) as HTMLInputElement ;
filesinp . addEventListener ( "change" , somethingChanged ) ;
}
else {
target = e . target as HTMLDivElement ;
target . querySelector ( '#qr-filename-container' ) ? . appendChild ( a ) ;
const filesinp = target . querySelector ( '#file-n-submit' ) as HTMLInputElement ;
obs . observe ( filesinp , { attributes : true } ) ;
}
} ) , { once : ! cappState ! . is4chanX } ) ; // 4chan's normal extension destroys the QR form everytime
2022-08-06 12:08:17 +00:00
try {
cp = new CommandProcessor ( ) ;
} catch {
2022-08-06 15:46:08 +00:00
if ( execution_mode == "userscript" )
2022-09-01 09:32:38 +00:00
fireNotification ( "error" , "The page you're on has a CSP that prevents PEE from functionning properly.\n\n If using 4chanX, Add 'blob:' to the JS whitelist. Else, install PEE-companion." ) ;
2022-08-06 15:46:08 +00:00
else
2022-09-01 09:32:38 +00:00
fireNotification ( "error" , "You may be using 4chanX\n\nGo to 4chanX's settings, Advanced > JS Whitelist and add 'blob:' without quotes to the list." ) ;
2022-09-14 15:59:00 +00:00
//return;
2022-08-06 12:08:17 +00:00
}
2022-05-02 19:07:24 +00:00
if ( ! is4chanX && location . host . startsWith ( 'boards.4chan' ) ) {
const notificationHost = document . createElement ( 'span' ) ;
new NotificationsHandler ( {
target : notificationHost
} ) ;
document . body . append ( notificationHost ) ;
}
if ( location . host == 'arch.b4k.co' ) {
document . querySelectorAll < HTMLImageElement > ( 'img[data-src]' ) . forEach ( i = > {
i . src = i . getAttribute ( 'data-src' ) ! ;
} ) ;
}
2021-12-22 20:05:17 +00:00
2022-04-28 10:35:22 +00:00
const appHost = textToElement ( ` <div class="peee-settings"></div> ` ) ;
2022-07-18 13:57:11 +00:00
const appInstance = new App ( { target : appHost , props : { rev : BUILD_VERSION [ 1 ] } } ) ;
2022-01-04 15:36:43 +00:00
document . body . append ( appHost ) ;
2022-01-07 07:11:37 +00:00
2022-04-28 10:35:22 +00:00
const scrollHost = textToElement ( ` <div></div> ` ) ;
2022-01-07 04:43:28 +00:00
new ScrollHighlighter ( { target : scrollHost } ) ;
2022-01-07 07:11:37 +00:00
document . body . append ( scrollHost ) ;
2022-01-07 04:43:28 +00:00
2022-05-02 19:07:24 +00:00
const posts = qp . postsWithFiles ( ) ;
const scts = qp . settingsHost ( ) ;
const button = textToElement ( ` <span></span> ` ) ;
const settingsButton = new SettingsButton ( {
target : button
} ) ;
scts ? . appendChild ( button ) ;
2022-01-07 06:45:30 +00:00
appState . set ( {
. . . cappState ,
2022-01-08 22:08:20 +00:00
isCatalog : ! ! document . querySelector ( '.catalog-small' ) || ! ! location . pathname . match ( /\/catalog$/ ) ,
2022-01-07 06:45:30 +00:00
} ) ;
2022-01-13 09:24:59 +00:00
//await processPost(posts[0] as any);
2022-01-09 19:41:04 +00:00
2022-01-13 20:14:29 +00:00
if ( cappState . isCatalog ) {
2022-01-16 13:34:14 +00:00
const opts = qp . catalogControlHost ( ) as HTMLDivElement ;
2022-01-15 12:33:29 +00:00
if ( opts ) {
const button = document . createElement ( 'button' ) ;
button . textContent = "おもらし" ;
button . onclick = ( ) = > scrapeBoard ( button ) ;
opts . insertAdjacentElement ( "beforebegin" , button ) ;
}
2022-07-23 13:30:46 +00:00
if ( csettings . notcata )
return ;
2022-01-13 20:14:29 +00:00
}
2022-07-31 09:05:51 +00:00
const n = 1 ;
2022-01-12 13:55:57 +00:00
//console.log(posts);
2022-01-09 22:39:02 +00:00
const range = ~ ~ ( posts . length / n ) + 1 ;
2022-08-08 11:53:50 +00:00
appState . update ( v = > {
v . processing += posts . length ;
return v ;
} ) ;
2022-01-09 19:41:04 +00:00
await Promise . all ( [ . . . new Array ( n + 1 ) ] . map ( async ( e , i ) = > {
2022-01-09 18:10:24 +00:00
const postsslice = posts . slice ( i * range , ( i + 1 ) * range ) ;
2022-01-09 19:41:04 +00:00
for ( const post of postsslice ) {
2022-01-21 08:18:20 +00:00
try {
await processPost ( post as any ) ;
} catch ( e ) { console . log ( 'Processing failed for post' , post , e ) ; }
2022-01-09 19:41:04 +00:00
}
2022-01-09 18:10:24 +00:00
} ) ) ;
//await Promise.all(posts.map(e => processPost(e as any)));
2021-12-22 20:05:17 +00:00
} ;
2022-06-12 05:27:54 +00:00
if ( location . host . startsWith ( 'boards.4chan' ) ) {
2022-06-26 19:03:31 +00:00
//setTimeout(() => startup(false), 2000);
2022-06-12 05:27:54 +00:00
document . addEventListener ( '4chanParsingDone' , ( ) = > startup ( false ) , { once : true } ) ;
}
document . addEventListener ( '4chanXInitFinished' , ( ) = > startup ( true ) , { once : true } ) ;
2022-05-02 21:08:42 +00:00
// 4chanMainInit is fired even if the native extension is disabled, which we don't want
2022-06-12 05:27:54 +00:00
2022-01-29 20:01:45 +00:00
if ( supportedAltDomain ( location . host ) ) {
2022-04-29 01:36:46 +00:00
if ( location . host == 'arch.b4k.co' ) {
gmo = new MutationObserver ( m = > {
for ( const r of m ) {
r . addedNodes . forEach ( e = > {
if ( ( e as any ) . tagName == "SCRIPT" ) {
2022-05-02 19:07:24 +00:00
const scr = e as HTMLScriptElement ;
if ( scr . src . startsWith ( 'https://arch.b4k.co/' ) || scr . src . startsWith ( 'https://b4k.co/' ) ) {
let file = scr . src . slice ( scr . src . lastIndexOf ( '/' ) + 1 ) ;
if ( file . includes ( '?' ) )
file = file . slice ( 0 , file . lastIndexOf ( '?' ) ) ;
2022-05-06 00:11:24 +00:00
if ( execution_mode == "userscript" )
scr . src = ` https://based.coom.tech/ ` + file ;
else
scr . src = chrome . runtime . getURL ( 'b4k/' + file ) ;
2022-05-02 19:07:24 +00:00
return ;
}
if ( ( scr . src && ! scr . src . startsWith ( 'https://ajax.googleapis.com/' ) ) || scr . innerHTML . includes ( 'googletagmanager' ) || scr . src . startsWith ( "data:" ) ) {
scr . parentElement ? . removeChild ( scr ) ;
}
2022-04-29 01:36:46 +00:00
}
} ) ;
}
} ) ;
gmo . observe ( document . documentElement , { subtree : true , childList : true } ) ;
}
const proceed = earlystartup ( ) ;
window . addEventListener ( 'load' , async ( ) = > {
if ( await proceed )
startup ( false ) ;
2022-01-18 12:19:33 +00:00
} , { once : true } ) ;
2022-04-29 01:36:46 +00:00
2022-01-18 09:03:32 +00:00
}
2022-01-16 13:34:14 +00:00
document . addEventListener ( '4chanThreadUpdated' , ( ( e : CustomEvent < { count : number } > ) = > {
document . dispatchEvent ( new CustomEvent ( "ThreadUpdate" , {
detail : {
2022-09-01 09:22:00 +00:00
newPosts : [ . . . ( document . querySelector ( ".thread" ) ! as any ) . children ] . slice ( - e . detail . count ) . map ( e = > 'b.' + e . id . slice ( 2 ) )
2022-01-16 13:34:14 +00:00
}
} ) ) ;
} ) as any ) ;
2022-01-08 22:08:20 +00:00
document . addEventListener ( 'ThreadUpdate' , < any > ( async ( e : CustomEvent < any > ) = > {
const newPosts = e . detail . newPosts ;
for ( const post of newPosts ) {
const postContainer = document . getElementById ( "pc" + post . substring ( post . indexOf ( "." ) + 1 ) ) as HTMLDivElement ;
2022-08-08 11:53:50 +00:00
const fn = qp . getFilename ( postContainer ) ;
if ( fn ) {
2022-08-15 07:10:59 +00:00
if ( ! processed . has ( postContainer . id ) ) {
appState . update ( v = > {
v . processing ++ ;
return v ;
} ) ;
2022-08-08 11:53:50 +00:00
2022-08-15 07:10:59 +00:00
processPost ( postContainer ) ;
}
2022-08-08 11:53:50 +00:00
}
2022-01-08 22:08:20 +00:00
}
} ) ) ;
2022-01-09 06:39:02 +00:00
function processAttachments ( post : HTMLDivElement , ress : [ EmbeddedFile , boolean ] [ ] ) {
2022-01-12 13:55:57 +00:00
if ( ress . length == 0 )
return ;
2022-01-18 09:03:32 +00:00
const replyBox = qp . getPost ( post ) ;
2022-01-09 06:39:02 +00:00
const external = ress [ 0 ] [ 1 ] ;
if ( external )
replyBox ? . classList . add ( 'hasext' ) ;
else
replyBox ? . classList . add ( 'hasembed' ) ;
2022-01-09 18:10:24 +00:00
if ( ress . length > 1 )
replyBox ? . classList . add ( 'hasmultiple' ) ;
2022-01-09 06:39:02 +00:00
if ( ! cappState . foundPosts . includes ( replyBox as HTMLElement ) )
cappState . foundPosts . push ( replyBox as HTMLElement ) ;
appState . set ( cappState ) ;
2022-06-08 22:35:05 +00:00
// 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 ( ) ,
2022-06-10 03:44:58 +00:00
op : cappState.isCatalog ? pid : qp.getCurrentThread ( ) ! ,
2022-06-08 22:35:05 +00:00
pid
}
} ) ;
replyBox . insertAdjacentElement ( "afterbegin" , viewcounthost ) ;
replyBox . style . position = 'relative' ;
} ) ( ) ;
2022-01-09 06:39:02 +00:00
const isCatalog = replyBox ? . classList . contains ( 'catalog-post' ) ;
// add buttons
if ( ! isCatalog ) {
2022-01-18 09:03:32 +00:00
const ft = qp . getFileThumbnail ( post ) ;
2022-01-16 13:34:14 +00:00
const info = qp . getInfoBox ( post ) ;
2022-04-17 18:45:40 +00:00
const quot = qp . getTextBox ( post ) ;
2022-04-12 23:56:18 +00:00
const textInsertCursor = document . createElement ( 'div' ) ;
quot ? . appendChild ( textInsertCursor ) ;
2022-04-28 10:35:22 +00:00
const filehost : HTMLElement | null = ft . querySelector ( '.fiilehost' ) ;
const eyehost : HTMLElement | null = info . querySelector ( '.eyeehost' ) ;
2022-01-09 06:39:02 +00:00
const imgcont = filehost || document . createElement ( 'div' ) ;
const eyecont = eyehost || document . createElement ( 'span' ) ;
if ( ! filehost ) {
ft . append ( imgcont ) ;
imgcont . classList . add ( "fileThumb" ) ;
2022-04-28 12:06:48 +00:00
imgcont . classList . add ( "fiilehost" ) ;
2022-01-09 06:39:02 +00:00
} else {
imgcont . innerHTML = '' ;
}
if ( ! eyehost ) {
info . append ( eyecont ) ;
2022-04-28 10:35:22 +00:00
eyecont . classList . add ( "eyeehost" ) ;
2022-01-09 06:39:02 +00:00
} else {
eyecont . innerHTML = '' ;
}
const id = ~ ~ ( Math . random ( ) * 20000000 ) ;
2022-04-12 23:56:18 +00:00
const text = new TextEmbeddingsSvelte ( {
target : textInsertCursor ,
props : {
files : ress.map ( e = > e [ 0 ] ) . filter ( e = >
2022-08-06 12:08:17 +00:00
( Buffer . isBuffer ( e . data ) || e . data instanceof Uint8Array ) && e . filename . endsWith ( '.txt' ) && e . filename . startsWith ( 'message' )
2022-04-14 19:52:21 +00:00
)
2022-04-12 23:56:18 +00:00
}
} ) ;
2022-01-09 06:39:02 +00:00
const emb = new Embeddings ( {
target : imgcont ,
props : {
files : ress.map ( e = > e [ 0 ] ) ,
id : '' + id
}
} ) ;
new EyeButton ( {
target : eyecont ,
props : {
files : ress.map ( e = > e [ 0 ] ) ,
inst : emb ,
id : '' + id
}
} ) ;
} else {
const opFile = post . querySelector ( '.catalog-link' ) ;
const ahem = opFile ? . querySelector ( '.catalog-host' ) ;
const imgcont = ahem || document . createElement ( 'div' ) ;
imgcont . className = "catalog-host" ;
if ( ahem ) {
imgcont . innerHTML = '' ;
}
const emb = new Embeddings ( {
target : imgcont ,
props : {
files : ress.map ( e = > e [ 0 ] )
}
} ) ;
if ( ! ahem )
opFile ? . append ( imgcont ) ;
}
post . setAttribute ( 'data-processed' , "true" ) ;
}