43 Commits
v0.1 ... v0.5.1

Author SHA1 Message Date
Jack Case
5ce34bec91 theme_icons works, it's just the opposite of what I thought. 2025-11-23 14:21:16 +00:00
Jack Case
1f04a6227f Merge pull request #8 from GandalfDG/feat_bulma_ui
Convert Browser Action Popup to use Bulma CSS
2025-11-23 09:17:50 -05:00
Jack Case
39d0b7ea23 Merge branch 'main' into feat_bulma_ui 2025-11-23 09:17:12 -05:00
Jack Case
0308aa85f4 remove default_icon key in the hope that theme_icons will work 2025-11-23 14:13:34 +00:00
Jack Case
d1a9cd04b3 add package.zip to .gitignore 2025-11-23 14:05:05 +00:00
Jack Case
d3531afa05 add a logout button and improve logged in ui state handling 2025-11-23 14:04:51 +00:00
Jack Case
f03a7ac85e Merge pull request #7 from GandalfDG/package-script
created script to zip the extension for uploading
2025-11-23 08:34:48 -05:00
Jack Case
8527a9252b created script to zip the extension for uploading
package-extension.sh
2025-11-23 13:32:05 +00:00
Jack Case
a29d43f226 bump minor version and remove a csp permission in manifest.json 2025-11-23 13:29:33 +00:00
Jack Case
b5aeee47ea created script to zip the extension for uploading
package-extension.sh
2025-11-23 13:27:42 +00:00
Jack Case
7faef08713 update manifest.json 2025-11-21 22:52:07 +00:00
Jack Case
e025f3995c signup/login tabs are reactive 2025-11-21 14:07:11 +00:00
Jack Case
39fb3320c9 working on popup styling with bulma 2025-11-19 23:07:12 +00:00
Jack Case
bb68883781 add package/ to .gitignore 2025-11-17 12:32:47 +00:00
Jack Case
542b9f9dba add a status notification to direct users to verify their email 2025-11-17 12:32:28 +00:00
Jack Case
99c9b106c1 final tweaks for 0.4 release 2025-11-17 00:35:17 +00:00
Jack Case
cc229b54be add light/dark icons 2025-11-16 23:39:12 +00:00
Jack Case
b55c47d5f1 basics of new popup are good to go 2025-11-16 23:25:27 +00:00
Jack Case
32b0994076 give report button an ID 2025-11-16 20:54:57 +00:00
Jack Case
ead1e34689 working on logged in popup flow 2025-11-16 14:21:46 +00:00
Jack Case
9a0ae70c35 allow signup and login in browseraction popup 2025-11-15 20:00:15 +00:00
Jack Case
b7965bf71d fixed some state-bugs 2025-11-14 01:24:39 +00:00
Jack Case
e9169cbcf4 working on state 2025-11-14 00:55:29 +00:00
Jack Case
2790a97ea2 working on popup state management 2025-11-13 01:11:47 +00:00
Jack Case
8d44086516 signup form submission from popup
server needs work to support properly
2025-11-08 20:37:52 +00:00
Jack Case
760e7c234a fix content script to check for messages from the background script properly 2025-11-08 19:09:15 +00:00
Jack Case
71545bb08e let's just not bother with imports for the content script 2025-11-06 16:47:40 +00:00
Jack Case
8cc0f3aa64 working on imports, maybe not worth it for the content script, it can't be an es module 2025-11-06 01:38:17 +00:00
Jack Case
83bc5190a7 wip refactoring 2025-11-05 21:33:32 +00:00
Jack Case
f6bfb1aa4d get rid of page action, start working on browser action popup 2025-11-05 20:03:41 +00:00
Jack Case
3753d103e4 add a boolean to prevent re-reporting links that are inserted into the indexeddb from a check response 2025-11-01 20:01:10 +00:00
Jack Case
c4c0119f66 resolved some more type issues 2025-10-30 22:33:16 +00:00
Jack Case
4e966102ee resolved typing issues in hide-slop.ts 2025-10-30 22:05:16 +00:00
Jack Case
2581d9b746 typescripting 2025-10-27 20:56:16 +00:00
Jack Case
acc4bda140 emit source maps for debugging 2025-10-27 20:23:56 +00:00
Jack Case
7b06d78cd4 more typing 2025-10-27 20:18:03 +00:00
Jack Case
dbec1398e0 good ol' node modules
added to define types for firefox web extension browser api
2025-10-27 20:08:02 +00:00
Jack Case
ca0b79b6c8 beginning annotation with typescript 2025-10-27 19:57:18 +00:00
Jack Case
7effbbc900 Update readme
added the current state of the project
2025-10-27 09:30:23 -04:00
Jack Case
bf6c96a3b1 report successful login and update pageaction icon on report 2025-10-26 14:41:03 -04:00
Jack Case
5a4896b5b4 much more stable link attribute setting 2025-10-26 10:41:19 -04:00
Jack Case
b1f4ad61ff ResultLinks data structure 2025-10-26 09:54:31 -04:00
Jack Case
0423ed8807 working on better defining the page links datastructure 2025-10-25 20:32:21 -04:00
23 changed files with 810 additions and 363 deletions

View File

@@ -0,0 +1,6 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
"ghcr.io/devcontainers-community/npm-features/typescript:1": {}
}
}

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
scripts/
node_modules/
package/
package.zip

View File

@@ -1,6 +1,18 @@
# Slop Farmer # Slop Farmer
a browser extension to crowdsource reports of AI slop articles and pages, and to flag or hide links to slop from search results. a browser extension to crowdsource reports of AI slop articles and pages, and to flag or hide links to slop from search results.
This is a work in progress. Next steps: This is a work in progress.
- a database server to collect reports of slop content
- page scripts for search engine result pages ## Features
- Report a page as slop, and its domain and path will be stored in your browser and pushed to the backend API
- Search on DuckDuckGo and your results will be checked against known slop domains and paths so anything known to be slop will appear as a red link
## TODOs
- Implement signup with email verification
- Enable link checking without signup using a proof-of-work check to limit bot access to API
- Enable voting on reported slop to get rid of false reports
- Improve user experience
- Port to chromium-based browsers
## Stretch Goals
- webapp to allow users to go through a list of pages and report them as slop or not to proactively gather more reports outside of normal browsing activity

BIN
icons/virus_black_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

BIN
icons/virus_black_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

BIN
icons/virus_black_64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
icons/virus_white_16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

BIN
icons/virus_white_32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

BIN
icons/virus_white_64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Slop Farmer", "name": "Slop Farmer",
"version": "0.1", "version": "0.5.1",
"author": "Jack Case", "author": "Jack Case",
"description": "Crowd-source AI slop pages and domains", "description": "Crowd-source AI slop pages and domains",
@@ -13,26 +13,46 @@
"*://*.duckduckgo.com/*" "*://*.duckduckgo.com/*"
], ],
"page_action": { "content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; worker-src 'self'",
"default_icon": "icons/virus-slash.png",
"default_title": "report slop",
"show_matches": ["<all_urls>"]
},
"browser_action": { "browser_action": {
"default_icon": "icons/virus-slash.png",
"default_title": "Slop Farmer", "default_title": "Slop Farmer",
"default_popup": "pages/action_popup.html" "default_popup": "pages/action_popup.html",
"default_area": "navbar",
"theme_icons": [
{
"dark": "icons/virus_black_16.png",
"light": "icons/virus_white_16.png",
"size": 16
},
{
"dark": "icons/virus_black_32.png",
"light": "icons/virus_white_32.png",
"size": 32
}
]
}, },
"background": { "background": {
"scripts": ["scripts/report-slop.js"] "scripts": ["scripts/report-slop.js"],
"type": "module"
}, },
"content_scripts": [ "content_scripts": [
{ {
"matches": ["*://*.duckduckgo.com/*q=*"], "matches": ["*://*.duckduckgo.com/*q=*"],
"js": ["scripts/hide-slop.js"] "js": ["scripts/content-script.js"]
} }
],
"browser_specific_settings": {
"gecko": {
"data_collection_permissions": {
"required": [
"authenticationInfo",
"personallyIdentifyingInfo"
] ]
} }
}
}
}

4
package-extension.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/bash
tsc;
zip -r package.zip icons/ pages/ scripts/ styles/ manifest.json;

7
package.json Normal file
View File

@@ -0,0 +1,7 @@
{
"dependencies": {
"@types/firefox-webext-browser": "^143.0.0",
"altcha": "^2.2.4",
"typescript": "^5.9.3"
}
}

View File

@@ -1,19 +1,86 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> --> <!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
<!-- <script async defer src="https://cdn.jsdelivr.net/gh/altcha-org/altcha/dist/altcha.min.js" type="module"></script> --> <!-- <script async defer src="https://cdn.jsdelivr.net/gh/altcha-org/altcha/dist/altcha.min.js" type="module"></script> -->
<script async src="/scripts/report-slop.js" type="module"></script> <script async src="/scripts/browser-action.js" type="module"></script>
<script async defer src="/scripts/altcha/dist/altcha.js" type="module"></script>
<link rel="stylesheet" href="/styles/bulma/bulma.min.css">
<title>Slop Farmer</title> <title>Slop Farmer</title>
</head> </head>
<body> <body>
<h1>Hello, world!</h1> <div id="onboarding" style="visibility: visible;" class="hero is-primary not-logged-in">
<form id="login-form"> <div class="hero-body">
<label for="email" id="username">user:</label> <h1 class="title">Welcome, Slop Farmer!</h1>
<input type="text" name="username" required /> <p>tired of ai-generated slop articles in your search results? Sign up or log in to start reporting slop
<label for="password" id="password">password:</label> articles and have reported slop flagged in your searches!</p>
<input type="password" name="password" required /> </div>
<button id="login-button">login</button> </div>
<button id="logout-button" class="button is-small logged-in">Logout</button>
<div class="section">
<nav class="tabs is-centered not-logged-in">
<ul>
<li id="signup-tab"><a>sign up</a></li>
<li id="login-tab"><a>log in</a></li>
</ul>
</nav>
<div id="signup" style="display:block" class="box">
<h1 class="title">Sign Up</h1>
<form id="signup-form">
<div class="field">
<label for="email" class="label">E-Mail</label>
<div class="control">
<input type="email" name="email" required class="input" />
</div>
</div>
<div class="field">
<label for="password" class="label">Password</label>
<div class="control">
<input type="password" name="password" required class="input" />
</div>
</div>
<div class="field">
<altcha-widget challengeurl="https://api.slopfarmer.jack-case.pro/altcha-challenge"></altcha-widget>
</div>
<div class="field">
<button id="signup-button" class="button is-primary">sign up</button>
</div>
</form> </form>
<h2></h2>
</div>
<div id="login" style="display:none" class="box">
<h1 class="title">Log In</h1>
<form id="login-form">
<div class="field">
<label for="email" id="username" class="label">E-Mail</label>
<div class="control">
<input type="text" name="username" required class="input" />
</div>
</div>
<div class="field">
<label for="password" id="password" class="label">Password</label>
<div class="control">
<input type="password" name="password" required class="input" />
</div>
</div>
<div class="field">
<button id="login-button" class="button is-primary">log in</button>
</div>
</form>
<h2 style="visibility: collapse;" id="login-status">You're logged in.</h2>
</div>
<div id="report" style="display: none" class="block">
<button id="report-button" class="button is-primary">Report this page</button>
<h2></h2>
</div>
</div>
</body> </body>

View File

@@ -1,99 +0,0 @@
const ddg_result_selector = "a[data-testid=\"result-title-a\""
const ddg_result_list_selector = "ol.react-results--main"
let result_list_node
let result_list_observer
const page_links = new Map()
class SearchLink {
constructor(link_node) {
this.node = link_node
this.target = link_node.getAttribute("href")
this.checked = false
this.result = undefined
}
}
function check_links(links) {
// send a message to background script with a list of URLs to check
browser.runtime.sendMessage({type: "check", urls: links})
links.forEach((link) => {page_links.get(link).checked = true})
}
async function message_listener(message) {
// handle slop reports returned from the background script
if(message.type === "check_result") {
console.log(message.url, message.result)
const link = page_links.get(message.url)
if ( message.result.slop_domain ) {
link.node.setAttribute("style", "color: red;")
}
link.result = message.result
}
}
function get_initial_links() {
// get links from initial page load
const links = document.querySelectorAll(ddg_result_selector)
links.forEach((node) => {
const link = new SearchLink(node)
page_links.set(link.target, link)
})
const link_targets = page_links.keys().toArray()
check_links(link_targets)
}
function update_links() {
// the result list has updated, add new links and check them
const links = document.querySelectorAll(ddg_result_selector)
links.forEach((node) => {
const link = new SearchLink(node)
if (page_links.has(link.target)) return
page_links.set(link.target, link)
})
const link_arr = page_links.keys().filter((key) => {
return !(page_links.get(key).checked)
}).toArray()
check_links(link_arr)
}
function setup_result_observer() {
// observe changes in the result list to respond to newly loaded results
const config = { childList: true }
result_list_observer = new MutationObserver(update_links)
result_list_observer.observe(result_list_node, config)
}
async function wait_for_results() {
results = new Promise(async (resolve) => {
let node = document.querySelector(ddg_result_list_selector)
while (!node) {
await new Promise((resolve) => {setTimeout(()=>{resolve()}, 100)})
node = document.querySelector(ddg_result_list_selector)
}
resolve(node)
})
return results
}
async function onload_handler() {
// get results ol node to observe
result_list_node = await wait_for_results()
get_initial_links()
setup_result_observer()
}
// listen for messages from the background script
browser.runtime.onMessage.addListener(message_listener)
// initialize state on document load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", onload_handler)
} else {
wait_for_results().then(onload_handler)
}

View File

@@ -1,240 +0,0 @@
const API_URL = "https://api.slopfarmer.jack-case.pro"
let access_token
const login_form = document.getElementById("login-form")
if(login_form) {
login_form.addEventListener("submit", (event) => {event.preventDefault(); submit_login_form()})
}
function setup_storage_db() {
/* create indexeddb object store to retain objects in the form of
* {"domain": "domain.tld",
* "paths": [
* "page/1",
* "page/2"
* ]
* }
*/
let db
const db_request = window.indexedDB.open("SlopDB", 1)
db_request.onerror = (event) => {
// handle error
console.log(event)
}
db_request.onsuccess = (event) => {
// create objectstore
console.log(event)
db = event.target.result
}
db_request.onupgradeneeded = (event) => {
console.log(event)
db = event.target.result
const slop_store = db.createObjectStore("slop", {keyPath: "domain"})
}
}
function on_install_handler() {
setup_storage_db()
}
async function get_slop_store(readwrite) {
const slop_store_promise = new Promise((resolve, reject) => {
const db_request = window.indexedDB.open("SlopDB", 1)
db_request.onsuccess = (event) => {
const db = event.target.result
const transaction = db.transaction(["slop"], readwrite ? "readwrite" : undefined)
const slop_store = transaction.objectStore("slop")
resolve(slop_store)
}
db_request.onerror = (event) => {
reject(event)
}
})
return await slop_store_promise
}
async function insert_slop(domain, path) {
let db
const db_request = window.indexedDB.open("SlopDB", 1)
db_request.onsuccess = (event) => {
db = event.target.result
const transaction = db.transaction(["slop"], "readwrite")
const slop_store = transaction.objectStore("slop")
// is this domain already stored?
const request = slop_store.get(domain)
request.onsuccess = (event) => {
let result = request.result
if (result) {
// domain exists, add this path
result.paths.add(path)
}
else {
// create a new domain object
const paths_set = new Set()
paths_set.add(path)
result = {domain: domain, paths: paths_set}
}
// persist to indexeddb
const store_request = slop_store.put(result)
store_request.onsuccess = (event) => {
console.log(domain, path, "stored")
}
}
}
const report_url = new URL("/report", API_URL)
const request = new Request(report_url,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Bearer": get_access_token()
},
body: JSON.stringify({slop_urls: [new URL(path, "http://"+domain).toString()]})
})
fetch(request)
}
async function check_local_slop(url) {
const slop_url = new URL(url)
const slop_store = await get_slop_store(false)
const known_slop = new Promise((resolve, reject) => {
const request = slop_store.get(slop_url.hostname)
request.onsuccess = (event) => {
resolve(request.result)
}
request.onerror = (event) => {
reject(event)
}
})
const slop_object = await known_slop
let result = {slop_domain: false, slop_path: false}
if (slop_object) {
// domain was found
result.slop_domain = true
if (slop_object.paths.has(slop_url.pathname)) {
// specific page was found
result.slop_path = true
}
}
return result
}
async function check_remote_slop(urls) {
const check_url = new URL("/check", API_URL)
const request = new Request(check_url, {method: "POST", headers: { "Content-Type": "application/json", "Bearer": get_access_token() }, body: JSON.stringify({slop_urls: urls})})
const response = await fetch(request)
let domain_objects = await response.json()
domain_objects.forEach((domain) => {insert_slop(domain.domain_name, "/")})
return domain_objects
}
async function on_button_clicked_handler(tab) {
// insert the current tab's page into slop storage
const tab_url = new URL(tab.url)
const domain = tab_url.hostname
const path = tab_url.pathname
insert_slop(domain, path)
}
async function update_page_action_icon(details) {
if(details.frameId != 0) {
return
}
const is_slop = await check_local_slop(details.url)
if(is_slop.slop_path) {
browser.pageAction.setIcon({
path: "icons/virus_red.png",
tabId: details.tabId
})
}
else if(is_slop.slop_domain) {
browser.pageAction.setIcon({
path: "icons/virus_yellow.png",
tabId: details.tabId
})
}
else {
browser.pageAction.setIcon({
path: "icons/virus-slash.png",
tabId: details.tabId
})
}
console.log(is_slop)
}
async function message_listener(message, sender) {
const tabid = sender.tab.id
if (message.type === "check") {
let check_promises = new Array()
let not_found_local = new Array()
message.urls.forEach((url) => {
check_promises.push(check_local_slop(url).then(async (result) => {
if (result.slop_domain) {
browser.tabs.sendMessage(tabid, { type: "check_result", url: url, result: result })
}
else {
not_found_local.push(url)
}
}))
})
await Promise.all(check_promises)
let remote_slop = await check_remote_slop(not_found_local)
remote_slop.forEach((result) => {
browser.tabs.sendMessage(tabid, { type: "check_result", url: result.url, result: result })
})
}
}
function get_access_token() {
access_token = localStorage.getItem("accessToken")
if (!access_token) {
// get an access token from the API
}
return access_token
}
async function submit_login_form() {
const login_url = new URL("/login", API_URL)
const request = new Request(login_url,
{
method: "POST",
body: new FormData(login_form)
})
const response = await fetch(request)
if(response.ok) {
const body = await response.json()
const token = body.access_token
localStorage.setItem("accessToken", token)
}
}
if(!login_form) {
browser.runtime.onInstalled.addListener(on_install_handler)
browser.runtime.onStartup.addListener(get_access_token)
browser.pageAction.onClicked.addListener(on_button_clicked_handler)
browser.webNavigation.onCommitted.addListener(update_page_action_icon)
browser.runtime.onMessage.addListener(message_listener)
}

170
src/browser-action.ts Normal file
View File

@@ -0,0 +1,170 @@
import { getTextOfJSDocComment } from "../node_modules/typescript/lib/typescript.js"
import { API_URL, send_message_to_background } from "./common.js"
let popup_state: PopupState = null
class PopupState {
logged_in: boolean
visible_section: string
page_sections: Map<string, HTMLElement>
page_elements: Map<string, HTMLElement>
visible_logged_in: Array<HTMLElement>
visible_logged_out: Array<HTMLElement>
constructor(logged_in: boolean, page_sections: Map<string, HTMLElement>, visible_section: string, page_elements: Map<string, HTMLElement>, visible_logged_in: Array<HTMLElement>, visible_logged_out: Array<HTMLElement>) {
this.logged_in = logged_in
this.page_sections = page_sections
this.visible_section = visible_section
this.page_elements = page_elements
this.visible_logged_in = visible_logged_in
this.visible_logged_out = visible_logged_out
}
update_login_visibility() {
this.visible_logged_in.forEach((element) => {
element.style.display = this.logged_in ? "block" : "none"
})
this.visible_logged_out.forEach((element) => {
element.style.display = this.logged_in ? "none" : "block"
})
}
set_visible_section(section_id: string) {
this.visible_section = section_id
switch (section_id) {
case "signup":
this.page_elements.get("signup_button").setAttribute("class", "is-active")
this.page_elements.get("login_button").setAttribute("class", "")
break
case "login":
this.page_elements.get("login_button").setAttribute("class", "is-active")
this.page_elements.get("signup_button").setAttribute("class", "")
break
}
this.page_sections.forEach((element, id) => {
element.style.display = id === section_id ? "block" : "none"
})
this.update_login_visibility()
}
}
async function submit_login_form() {
const login_url = new URL("/login", API_URL)
const request = new Request(login_url,
{
method: "POST",
body: new FormData(popup_state.page_elements.get("login_form") as HTMLFormElement)
})
const response = await fetch(request)
if (response.ok) {
const body = await response.json()
const token = body.access_token
await send_message_to_background({type: "login", token: token})
const status_el = document.getElementById("login-status")
status_el.setAttribute("style", "visibility: visible;")
popup_state.logged_in = true
popup_state.set_visible_section("report")
}
else {
//bad login, update the form
}
}
async function submit_signup_form() {
const signup_url = new URL("/signup", API_URL)
const request = new Request(signup_url, {
method: "POST",
body: new FormData(popup_state.page_elements.get("signup_form") as HTMLFormElement)
})
const response = await fetch(request)
console.log(response)
if (response.ok) {
popup_state.page_elements.get("signup_status").textContent = "check your email for a verification link from slopfarmer@jack-case.pro. It may be in your spam folder."
}
}
async function logout() {
const response = await send_message_to_background({type: "logout"})
popup_state.logged_in = false
popup_state.set_visible_section("login")
}
async function check_login(): Promise<boolean> {
const response = await send_message_to_background({type: "islogged"})
return response.logged_in
}
async function initialize_popup() {
const login_form = document.getElementById("login-form") as HTMLFormElement
const login_status = document.getElementById("login-status")
const signup_form = document.getElementById("signup-form") as HTMLFormElement
const signup_section = document.getElementById("signup")
const login_section = document.getElementById("login")
const report_section = document.getElementById("report")
const signup_button = document.getElementById("signup-tab")
const signup_status = signup_section.querySelector("h2")
const login_button = document.getElementById("login-tab")
const report_button = document.getElementById("report-button") as HTMLButtonElement
const report_status = report_section.querySelector("h2")
const logout_button = document.getElementById("logout-button")
const page_sections = new Map()
page_sections.set("signup", signup_section)
page_sections.set("login", login_section)
page_sections.set("report", report_section)
const page_elements = new Map()
page_elements.set("login_form", login_form as HTMLElement)
page_elements.set("login_status", login_status)
page_elements.set("signup_form", signup_form as HTMLElement)
page_elements.set("signup_status", signup_status)
page_elements.set("report_button", report_button)
page_elements.set("report_status", report_status)
page_elements.set("signup_button", signup_button)
page_elements.set("login_button", login_button)
const logged_in_items = Array.from(document.querySelectorAll(".logged-in")) as Array<HTMLElement>
const logged_out_items = Array.from(document.querySelectorAll(".not-logged-in")) as Array<HTMLElement>
const logged_in = await check_login()
popup_state = new PopupState(logged_in, page_sections, "signup", page_elements, logged_in_items, logged_out_items)
popup_state.set_visible_section(logged_in ? "report" : "signup")
login_form.addEventListener("submit", (event) => { event.preventDefault(); submit_login_form() })
signup_form.addEventListener("submit", (event) => { event.preventDefault(); submit_signup_form() })
signup_button.addEventListener("click", (event) => {popup_state.set_visible_section("signup")})
login_button.addEventListener("click", (event) => {popup_state.set_visible_section("login")})
report_button.addEventListener("click", async (event) => {
const result = await send_message_to_background({type: "report"})
popup_state.page_elements.get("report_status").textContent = "report accepted"
setTimeout(() => { window.close() }, 1000)
})
logout_button.addEventListener("click", async (event) => {
logout()
})
}
addEventListener("DOMContentLoaded", (event) => {
initialize_popup()
})

6
src/common.ts Normal file
View File

@@ -0,0 +1,6 @@
export const API_URL: string = "https://api.slopfarmer.jack-case.pro"
export async function send_message_to_background(message: any): Promise<any> {
const response = browser.runtime.sendMessage(message)
return response
}

164
src/content-script.ts Normal file
View File

@@ -0,0 +1,164 @@
class SearchLink {
node: Element
target: string
url: URL
checked: boolean
result: any
constructor(link_node: Element) {
this.node = link_node
this.target = link_node.getAttribute("href")
this.url = new URL(link_node.getAttribute("href"))
this.checked = false
this.result = undefined
}
}
class ResultLinks extends Map {
// map domains to paths and their associated nodes
setLink(domain: string, path: string, search_link: SearchLink) {
if(!super.get(domain)) {
const nested_map = new Map()
nested_map.set(path, search_link)
super.set(domain, nested_map)
} else {
super.get(domain).set(path, search_link)
}
}
setNode(link_node: Element) {
const search_link = new SearchLink(link_node)
this.setLink(search_link.url.hostname, search_link.url.pathname, search_link)
}
get(domain: string, path: string = "/") {
return super.get(domain).get(path)
}
getDomain(domain: string) {
return super.get(domain)
}
getUrl(url: string) {
const urlobj = new URL(url)
return this.get(urlobj.hostname, urlobj.pathname)
}
getSearchLinks() {
// return an iterator over the nested SearchLink objects
const domain_value_iterator = super.values() as Iterator<Map<string, SearchLink>>
let search_link_iterator: Iterator<SearchLink>
// didn't realize flatMap was brand new this year
// @ts-ignore
search_link_iterator = domain_value_iterator.flatMap((domain_map: Map<string, SearchLink>) => {
return domain_map.values()
})
return search_link_iterator
}
}
const ddg_result_selector = "a[data-testid=\"result-title-a\""
const ddg_result_list_selector = "ol.react-results--main"
let result_list_node: Element
let result_list_observer
const page_links = new ResultLinks()
function check_links(search_links: SearchLink[]) {
// send a message to background script with a list of URLs to check
const urls = search_links.map((search_link: SearchLink) => {
search_link.checked = true
return search_link.target
})
browser.runtime.sendMessage({type: "check", urls: urls})
}
async function backend_message_listener(message: any) {
// handle slop reports returned from the background script
switch(message.type) {
case ("check_result"):
if (message.domain) {
const paths = page_links.getDomain(message.domain)
paths.forEach((search_link: SearchLink) => {
search_link.node.setAttribute("style", "color: red;")
search_link.result = message.result
})
} else if (message.url) {
const link = page_links.getUrl(message.url)
link.node.setAttribute("style", "color: red;")
link.result = message.result
}
break
}
}
function get_initial_links() {
// get links from initial page load
const links = document.querySelectorAll(ddg_result_selector)
links.forEach((node) => {
page_links.setNode(node)
})
const link_targets = page_links.getSearchLinks()
// @ts-ignore
check_links(link_targets.toArray())
}
function update_links() {
// the result list has updated, add new links and check them
const links = document.querySelectorAll(ddg_result_selector)
links.forEach((node) => {
page_links.setNode(node)
})
// @ts-ignore
const link_iter = page_links.getSearchLinks().filter((search_link: SearchLink) => {
return !(search_link.checked)
})
check_links(link_iter.toArray())
}
function setup_result_observer() {
// observe changes in the result list to respond to newly loaded results
const config = { childList: true }
result_list_observer = new MutationObserver(update_links)
result_list_observer.observe(result_list_node, config)
}
async function wait_for_results() {
let results: Promise<Element> = new Promise(async (resolve) => {
let node = document.querySelector(ddg_result_list_selector)
while (!node) {
await new Promise<void>((resolve) => {setTimeout(()=>{resolve()}, 100)})
node = document.querySelector(ddg_result_list_selector)
}
resolve(node)
})
return results
}
async function onload_handler() {
// get results ol node to observe
result_list_node = await wait_for_results()
get_initial_links()
setup_result_observer()
}
// listen for messages from the background script
browser.runtime.onMessage.addListener(backend_message_listener)
// initialize state on document load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", onload_handler)
} else {
wait_for_results().then(onload_handler)
}

256
src/report-slop.ts Normal file
View File

@@ -0,0 +1,256 @@
import { API_URL } from "./common.js"
let access_token: string
function setup_storage_db() {
/* create indexeddb object store to retain objects in the form of
* {"domain": "domain.tld",
* "paths": [
* "page/1",
* "page/2"
* ]
* }
*/
let db
const db_request = window.indexedDB.open("SlopDB", 1)
db_request.onerror = (event) => {
// handle error
console.log(event)
}
db_request.onsuccess = (event) => {
// create objectstore
console.log(event)
//@ts-ignore
db = event.target.result
}
db_request.onupgradeneeded = (event) => {
console.log(event)
//@ts-ignore
db = event.target.result
const slop_store = db.createObjectStore("slop", { keyPath: "domain" })
}
}
function on_install_handler() {
setup_storage_db()
}
async function get_slop_store(readwrite: boolean) {
const slop_store_promise: Promise<IDBObjectStore> = new Promise((resolve, reject) => {
const db_request = window.indexedDB.open("SlopDB", 1)
db_request.onsuccess = (event) => {
//@ts-ignore
const db = event.target.result
const transaction = db.transaction(["slop"], readwrite ? "readwrite" : undefined)
const slop_store = transaction.objectStore("slop")
resolve(slop_store)
}
db_request.onerror = (event) => {
reject(event)
}
})
return await slop_store_promise
}
async function insert_slop(domain: string, path: string, report: boolean = true) {
let db
const db_request = window.indexedDB.open("SlopDB", 1)
db_request.onsuccess = (event) => {
//@ts-ignore
db = event.target.result
const transaction = db.transaction(["slop"], "readwrite")
const slop_store = transaction.objectStore("slop")
// is this domain already stored?
const request = slop_store.get(domain)
request.onsuccess = () => {
let result = request.result
if (result) {
// domain exists, add this path
result.paths.add(path)
}
else {
// create a new domain object
const paths_set = new Set()
paths_set.add(path)
result = { domain: domain, paths: paths_set }
}
// persist to indexeddb
const store_request = slop_store.put(result)
store_request.onsuccess = () => {
console.log(domain, path, "stored")
}
}
}
if (report) {
const report_url = new URL("/report", API_URL)
const request = new Request(report_url,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Bearer": get_access_token()
},
body: JSON.stringify({ slop_urls: [new URL(path, "http://" + domain).toString()] })
})
fetch(request)
}
}
async function check_local_slop(url: string) {
const slop_url = new URL(url)
const slop_store = await get_slop_store(false)
const known_slop: Promise<any> = new Promise((resolve, reject) => {
const request = slop_store.get(slop_url.hostname)
request.onsuccess = (event) => {
resolve(request.result)
}
request.onerror = (event) => {
reject(event)
}
})
const slop_object = await known_slop
let result = { slop_domain: false, slop_path: false }
if (slop_object) {
// domain was found
result.slop_domain = true
if (slop_object.paths.has(slop_url.pathname)) {
// specific page was found
result.slop_path = true
}
}
return result
}
async function check_remote_slop(urls: string[]) {
const check_url = new URL("/check", API_URL)
const request = new Request(check_url, { method: "POST", headers: { "Content-Type": "application/json", "Bearer": get_access_token() }, body: JSON.stringify({ slop_urls: urls }) })
const response = await fetch(request)
let domain_objects = await response.json()
domain_objects.forEach((domain: any) => { insert_slop(domain.domain_name, "/", false) })
return domain_objects
}
async function on_button_clicked_handler(tab: any) {
// insert the current tab's page into slop storage
const tab_url = new URL(tab.url)
const domain = tab_url.hostname
const path = tab_url.pathname
await insert_slop(domain, path)
// @ts-ignore
update_page_action_icon({ frameId: 0, tabId: tab.id, url: tab.url })
}
async function update_page_action_icon(details: browser.webNavigation._OnCommittedDetails) {
if (details.frameId != 0) {
return
}
const is_slop = await check_local_slop(details.url)
if (is_slop.slop_path) {
browser.pageAction.setIcon({
path: "icons/virus_red.png",
tabId: details.tabId
})
}
else if (is_slop.slop_domain) {
browser.pageAction.setIcon({
path: "icons/virus_yellow.png",
tabId: details.tabId
})
}
else {
browser.pageAction.setIcon({
path: "icons/virus-slash.png",
tabId: details.tabId
})
}
console.log(is_slop)
}
function message_listener(message: any, sender: any, send_response: Function): Promise<any> {
const sender_tab = sender.tab ? sender.tab : undefined
const tabid = sender_tab ? sender_tab.id : undefined
switch (message.type) {
case "check":
let check_promises = new Array()
let not_found_local = new Array()
message.urls.forEach((url: string) => {
check_promises.push(check_local_slop(url).then(async (result) => {
if (result.slop_domain) {
browser.tabs.sendMessage(tabid, { type: "check_result", url: url, result: result })
}
else {
not_found_local.push(url)
}
}))
})
const result = Promise.all(check_promises).then(() => {
let remote_slop = check_remote_slop(not_found_local)
remote_slop.then((remote_results) => {
remote_results.forEach((result: any) => {
browser.tabs.sendMessage(tabid, { type: "check_result", domain: result.domain_name, result: result })
})
})
})
return result
break
case "login":
localStorage.setItem("accessToken", message.token)
return new Promise((resolve, reject) => { resolve(true) })
break
case "islogged":
const token = get_access_token()
const response = { logged_in: token != null ? true : false }
return new Promise((resolve, reject) => { resolve(response) })
break
case "logout":
localStorage.removeItem("accessToken")
return new Promise((resolve, reject) => { resolve(true) })
break
case "report":
return new Promise((resolve, reject) => {
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
const tab_url = new URL(tabs[0].url)
insert_slop(tab_url.hostname, tab_url.pathname, true).then(() => resolve(true))
})
})
break
}
}
function get_access_token() {
access_token = localStorage.getItem("accessToken")
if (!access_token) {
// get an access token from the API
}
return access_token
}
browser.runtime.onInstalled.addListener(on_install_handler)
browser.runtime.onStartup.addListener(get_access_token)
browser.webNavigation.onCommitted.addListener(update_page_action_icon)
browser.runtime.onMessage.addListener(message_listener)

21
styles/bulma/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2023 Jeremy Thomas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

3
styles/bulma/bulma.min.css vendored Normal file

File diff suppressed because one or more lines are too long

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"include": ["src/**/*"],
"compilerOptions": {
"outDir": "scripts",
"noImplicitAny": true,
"lib": ["ES7", "DOM"],
"sourceMap": true,
"target": "esnext",
"baseUrl": "src/",
"module": "esnext",
"noFallthroughCasesInSwitch": true
}
}

32
yarn.lock Normal file
View File

@@ -0,0 +1,32 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@altcha/crypto@^0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@altcha/crypto/-/crypto-0.0.1.tgz#0e2f254559fb350c80ff56d29b8e3ab2e6bbea95"
integrity sha512-qZMdnoD3lAyvfSUMNtC2adRi666Pxdcw9zqfMU5qBOaJWqpN9K+eqQGWqeiKDMqL0SF+EytNG4kR/Pr/99GJ6g==
"@rollup/rollup-linux-x64-gnu@4.18.0":
version "4.18.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942"
integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==
"@types/firefox-webext-browser@^143.0.0":
version "143.0.0"
resolved "https://registry.yarnpkg.com/@types/firefox-webext-browser/-/firefox-webext-browser-143.0.0.tgz#29413c9f393d4c4b5622d4a74182ff0219e98620"
integrity sha512-865dYKMOP0CllFyHmgXV4IQgVL51OSQQCwSoihQ17EwugePKFSAZRc0EI+y7Ly4q7j5KyURlA7LgRpFieO4JOw==
altcha@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/altcha/-/altcha-2.2.4.tgz#e89d9e6fbdf3a754e40a458f0f353b6dd495a5b2"
integrity sha512-UrU2izh1pISqzd7TCAJiJB2N+r7roqA348Qxt1gJlW5k9pJpbDDmMcDaxfuet9h/WFE6Snrritu/WusmERarrg==
dependencies:
"@altcha/crypto" "^0.0.1"
optionalDependencies:
"@rollup/rollup-linux-x64-gnu" "4.18.0"
typescript@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==