Skip to content

Instantly share code, notes, and snippets.

@saxamaphone69
Last active October 5, 2024 09:30
4chan searcher
// ==UserScript==
// @name 4chan search mega
// @namespace https://github.com/saxamaphone69/ss21
// @version 2.6
// @description Companion userscript
// @author saxamaphone69
// @match https://*.4chan.org/*
// @connect https://boards.4chan.org
// @connect https://find.4chan.org
// @grant GM.xmlHttpRequest
// @grant GM.addStyle
// @run-at document-start
// ==/UserScript==
(async () => {
"use strict";
const d = document;
function addStyle() {
const css = `
#shortcut-search {
all: unset;
margin-left: 3px;
}
dialog {
padding: 0;
border: 1px solid var(--xt-border);
background-color: var(--xt-background);
}
dialog::backdrop {
background-color: rgba(0,0,0,.75);
}
search {
max-width: 50vw;
white-space: normal;
}
.search_panel {
max-height: 50vh;
text-align: left;
overflow-y: auto;
}
.search-post {
display: flow-root;
background-color: #d6daf0;
border: 1px solid var(--xt-border);
border-radius: 4px;
padding: 4px;
.quote {
color: olive;
}
.name {
color: #117743;
font-weight: 700;
}
.subject {
color: #0f0c5d;
font-weight: 700;
}
.fileThumb {
float: left;
margin: 5px;
}
}
`;
GM.addStyle(css);
}
function makeRequest(url, method = 'GET', headers = {}, callback) {
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
// Set headers if provided
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header, headers[header]);
}
}
// Setup the callback
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
callback(null, xhr);
} else {
callback(new Error(`HTTP Status: ${xhr.status}`), null);
}
};
xhr.onerror = function () {
callback(new Error('Network Error'), null);
};
xhr.send();
}
function addSearchShortcut() {
let searchIcon = d.createElement('button');
searchIcon.id = 'shortcut-search';
searchIcon.classList.add('shortcut');
searchIcon.setAttribute("onclick", "document.querySelector('dialog').show();");
searchIcon.innerHTML = `
<a href="javascript:;" title="Search">
<span class="icon--alt-text">Search</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 512 512">
<path d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z" fill="currentColor" />
</svg>
</a>`;
let searchIconParent = d.getElementById('shortcuts');
let searchIconPlacement = d.getElementById('shortcut-watcher');
searchIconParent.insertBefore(searchIcon, searchIconPlacement);
}
function addSearchDialog() {
let searchDialog = d.createElement('dialog');
searchDialog.id = 'dialog-search';
//searchDialog.setAttribute("inert", "");
searchDialog.innerHTML = `
<form method="dialog">
<search>
<section class="search_wrap">
<input type="search" id="four-search" autocomplete="off"><button id="search-submit" type="submit" value="search">Search</button><button onclick="this.closest('dialog').close('cancel')">Close</button>
</section>
<section class="search_panel">
</section>
</search>
</form>`;
d.querySelector('#shortcut-search').appendChild(searchDialog);
}
function actuallySearch() {
let searchInput = d.querySelector('#four-search');
let searchButton = d.querySelector('#search-submit');
let searchPanel = d.querySelector('.search_panel');
searchButton.addEventListener('click', (e) => {
e.preventDefault();
grabSearch(searchInput.value);
});
function grabSearch(term) {
let url = `https://find.4chan.org/api?q=${term}`;
makeRequest(url, 'GET', { 'Accept': 'application/json' }, function (err, response) {
if (err) {
console.error('Error:', err);
searchPanel.innerHTML = `<div class="warning">Whoops! Seems there was an error:<br><span>${err}</span></div><div>Try <a href="${url}">going to your query in a new tab</a>, come back, and try again.`;
} else {
let searchResults = JSON.parse(response.responseText);
//console.log(searchResults, response.responseText);
function escapeQuotesInComField(jsonString) {
if (typeof jsonString !== 'string') {
throw new TypeError("Input must be a string");
}
// Match the com field and escape quotes within its value
return jsonString.replace(/"com": "(.*?)"/g, (match, p1) => {
const escapedCom = p1.replace(/"/g, '\\"');
return `"com": "${escapedCom}"`;
});
}
// Convert the response object to a JSON string
const jsonString = JSON.stringify(searchResults);
let escapedJsonString = escapeQuotesInComField(jsonString);
const data = JSON.parse(escapedJsonString);
let html = `<div>There was ${data.nhits} hits for "${data.query}"!</div>`;
// Loop over each thread in the threads array
data.threads.forEach(thread => {
// Create a variable to hold the modified posts
let modifiedPosts = '';
// Loop over each post in the thread's posts array
thread.posts.forEach(post => {
// Replace the search term with a marked version in the com field
const modifiedCom = post.com.replace(new RegExp(term, 'gi'), match => `<mark>${match}</mark>`);
// Use template literals to create HTML for each post
modifiedPosts += `
<article class="search-post">
${post.filename ? `<div class="file">
<div class="fileText">${post.filename}${post.ext}, ${post.w}x${post.h}</div>
<a class="fileThumb" href="//is2.4chan.org/${thread.board}/${post.tim}${post.ext}.png"><img data-md5="${post.md5}" src="//i.4cdn.org/${thread.board}/${post.tim}s.jpg" style="height: ${post.tn_h}px; width: ${post.tn_w}px;"></a>
</div>` : ``}
<div class="search-postInfo">${post.sub ? `<span class="subject">${post.sub}</span>` : ``}<span class="name">${post.name}</span><span class="dateTime">${post.now}</span><a href="/${thread.board}/thread/${thread.thread.substring(1)}/#p${post.no}" class="post-no">#${post.no}</a></div>
<blockquote class="post-com">${modifiedCom}</blockquote>
</article>
`;
});
// Use template literals to create HTML for each thread
html += `<div class="search-thread">
<div class="board"><a href="/${thread.board}/thread/${thread.thread.substring(1)}/">Posted on ${thread.board}</a></div>
<div class="posts">
${modifiedPosts}
</div>
</div>`;
});
searchPanel.innerHTML = html;
}
});
}
}
function init() {
addStyle();
addSearchShortcut();
addSearchDialog();
actuallySearch();
}
//d.addEventListener("DOMContentLoaded", init);
d.addEventListener("4chanXInitFinished", init);
/*
function ready(fn) {
if (document.readyState != 'loading'){
fn();
} else if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', fn);
} else {
document.attachEvent('onreadystatechange', function() {
if (document.readyState != 'loading') {
fn();
}
});
}
}
*/
//ready(init);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment