Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb68883781 | ||
|
|
542b9f9dba | ||
|
|
99c9b106c1 | ||
|
|
cc229b54be | ||
|
|
b55c47d5f1 | ||
|
|
32b0994076 | ||
|
|
ead1e34689 | ||
|
|
9a0ae70c35 | ||
|
|
b7965bf71d | ||
|
|
e9169cbcf4 | ||
|
|
2790a97ea2 | ||
|
|
8d44086516 | ||
|
|
760e7c234a | ||
|
|
71545bb08e | ||
|
|
8cc0f3aa64 | ||
|
|
83bc5190a7 | ||
|
|
f6bfb1aa4d | ||
|
|
3753d103e4 | ||
|
|
c4c0119f66 | ||
|
|
4e966102ee | ||
|
|
2581d9b746 | ||
|
|
acc4bda140 | ||
|
|
7b06d78cd4 | ||
|
|
dbec1398e0 | ||
|
|
ca0b79b6c8 | ||
|
|
7effbbc900 | ||
|
|
bf6c96a3b1 | ||
|
|
5a4896b5b4 |
6
.devcontainer/devcontainer.json
Normal file
6
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/universal:2",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-community/npm-features/typescript:1": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
scripts/
|
||||||
|
node_modules/
|
||||||
|
package/
|
||||||
18
README.md
18
README.md
@@ -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
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
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
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
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
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
BIN
icons/virus_white_64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Slop Farmer",
|
"name": "Slop Farmer",
|
||||||
"version": "0.1",
|
"version": "0.4.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,36 @@
|
|||||||
"*://*.duckduckgo.com/*"
|
"*://*.duckduckgo.com/*"
|
||||||
],
|
],
|
||||||
|
|
||||||
"page_action": {
|
"content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; worker-src 'self' blob:",
|
||||||
"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_icon": "icons/virus_black_32.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_white_16.png",
|
||||||
|
"light": "icons/virus_black_16.png",
|
||||||
|
"size": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dark": "icons/virus_white_32.png",
|
||||||
|
"light": "icons/virus_black_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"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
7
package.json
Normal file
7
package.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@types/firefox-webext-browser": "^143.0.0",
|
||||||
|
"altcha": "^2.2.4",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,17 +3,49 @@
|
|||||||
<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>
|
||||||
<title>Slop Farmer</title>
|
<title>Slop Farmer</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Hello, world!</h1>
|
<nav>
|
||||||
<form id="login-form">
|
<button id="signup-select" type="button">sign up</button>
|
||||||
<label for="email" id="username">user:</label>
|
<button id="login-select" type="button">log in</button>
|
||||||
<input type="text" name="username" required />
|
</nav>
|
||||||
<label for="password" id="password">password:</label>
|
|
||||||
<input type="password" name="password" required />
|
<div id="onboarding" style="visibility: visible;">
|
||||||
<button id="login-button">login</button>
|
<h1>Welcome, Slop Farmer!</h1>
|
||||||
</form>
|
<p>tired of ai-generated slop articles in your search results? Sign up or log in to start reporting slop articles
|
||||||
|
and have reported slop flagged in your searches!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="signup" style="visibility: collapse;">
|
||||||
|
<form id="signup-form">
|
||||||
|
<label for="email">email</label>
|
||||||
|
<input type="email" name="email" required />
|
||||||
|
<label for="password">password</label>
|
||||||
|
<input type="password" name="password" required />
|
||||||
|
<altcha-widget challengeurl="https://api.slopfarmer.jack-case.pro/altcha-challenge"></altcha-widget>
|
||||||
|
<button id="signup-button">sign up</button>
|
||||||
|
</form>
|
||||||
|
<h2></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="login" style="visibility: collapse;">
|
||||||
|
<h1>Log in to enable slop checking and reporting</h1>
|
||||||
|
<form id="login-form">
|
||||||
|
<label for="email" id="username">username</label>
|
||||||
|
<input type="text" name="username" required />
|
||||||
|
<label for="password" id="password">password</label>
|
||||||
|
<input type="password" name="password" required />
|
||||||
|
<button id="login-button">login</button>
|
||||||
|
</form>
|
||||||
|
<h2 style="visibility: collapse;" id="login-status">You're logged in.</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="report" style="visibility: collapse;">
|
||||||
|
<button id="report-button">Report this page</button>
|
||||||
|
<h2></h2>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
126
src/browser-action.ts
Normal file
126
src/browser-action.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
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>
|
||||||
|
|
||||||
|
constructor(logged_in: boolean, page_sections: Map<string, HTMLElement>, visible_section: string, page_elements: Map<string, HTMLElement>) {
|
||||||
|
this.logged_in = logged_in
|
||||||
|
this.page_sections = page_sections
|
||||||
|
this.visible_section = visible_section
|
||||||
|
this.set_visible_section(logged_in ? "report" : "signup")
|
||||||
|
this.page_elements = page_elements
|
||||||
|
}
|
||||||
|
|
||||||
|
set_visible_section(section_id: string) {
|
||||||
|
this.visible_section = section_id
|
||||||
|
this.page_sections.forEach((element, id) => {
|
||||||
|
element.style.visibility = id === section_id ? "visible" : "collapse"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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 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-select") as HTMLButtonElement
|
||||||
|
const signup_status = signup_section.querySelector("h2")
|
||||||
|
const login_button = document.getElementById("login-select") as HTMLButtonElement
|
||||||
|
const report_button = document.getElementById("report-button") as HTMLButtonElement
|
||||||
|
const report_status = report_section.querySelector("h2")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
const logged_in = await check_login()
|
||||||
|
|
||||||
|
popup_state = new PopupState(logged_in, page_sections, "signup", page_elements)
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
initialize_popup()
|
||||||
|
})
|
||||||
6
src/common.ts
Normal file
6
src/common.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
class SearchLink {
|
class SearchLink {
|
||||||
constructor(link_node) {
|
|
||||||
|
node: Element
|
||||||
|
target: string
|
||||||
|
url: URL
|
||||||
|
checked: boolean
|
||||||
|
result: any
|
||||||
|
|
||||||
|
constructor(link_node: Element) {
|
||||||
this.node = link_node
|
this.node = link_node
|
||||||
this.target = link_node.getAttribute("href")
|
this.target = link_node.getAttribute("href")
|
||||||
this.url = new URL(link_node.getAttribute("href"))
|
this.url = new URL(link_node.getAttribute("href"))
|
||||||
@@ -10,7 +17,7 @@ class SearchLink {
|
|||||||
|
|
||||||
class ResultLinks extends Map {
|
class ResultLinks extends Map {
|
||||||
// map domains to paths and their associated nodes
|
// map domains to paths and their associated nodes
|
||||||
set(domain, path, search_link) {
|
setLink(domain: string, path: string, search_link: SearchLink) {
|
||||||
if(!super.get(domain)) {
|
if(!super.get(domain)) {
|
||||||
const nested_map = new Map()
|
const nested_map = new Map()
|
||||||
nested_map.set(path, search_link)
|
nested_map.set(path, search_link)
|
||||||
@@ -20,30 +27,35 @@ class ResultLinks extends Map {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setNode(link_node) {
|
setNode(link_node: Element) {
|
||||||
const search_link = new SearchLink(link_node)
|
const search_link = new SearchLink(link_node)
|
||||||
this.set(search_link.url.hostname, search_link.url.pathname, search_link)
|
this.setLink(search_link.url.hostname, search_link.url.pathname, search_link)
|
||||||
}
|
}
|
||||||
|
|
||||||
get(domain, path="/") {
|
get(domain: string, path: string = "/") {
|
||||||
return super.get(domain).get(path)
|
return super.get(domain).get(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
getDomain(domain) {
|
getDomain(domain: string) {
|
||||||
return super.get(domain)
|
return super.get(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
getUrl(url) {
|
getUrl(url: string) {
|
||||||
const urlobj = new URL(url)
|
const urlobj = new URL(url)
|
||||||
return this.get(urlobj.hostname, urlobj.pathname)
|
return this.get(urlobj.hostname, urlobj.pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchLinks() {
|
getSearchLinks() {
|
||||||
// return an iterator over the nested SearchLink objects
|
// return an iterator over the nested SearchLink objects
|
||||||
const domain_value_iterator = super.values()
|
const domain_value_iterator = super.values() as Iterator<Map<string, SearchLink>>
|
||||||
const search_link_iterator = domain_value_iterator.flatMap((domain_map) => {
|
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 domain_map.values()
|
||||||
})
|
})
|
||||||
|
|
||||||
return search_link_iterator
|
return search_link_iterator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,29 +64,37 @@ class ResultLinks extends Map {
|
|||||||
const ddg_result_selector = "a[data-testid=\"result-title-a\""
|
const ddg_result_selector = "a[data-testid=\"result-title-a\""
|
||||||
const ddg_result_list_selector = "ol.react-results--main"
|
const ddg_result_list_selector = "ol.react-results--main"
|
||||||
|
|
||||||
let result_list_node
|
let result_list_node: Element
|
||||||
let result_list_observer
|
let result_list_observer
|
||||||
const page_links = new ResultLinks()
|
const page_links = new ResultLinks()
|
||||||
|
|
||||||
|
|
||||||
function check_links(search_links) {
|
function check_links(search_links: SearchLink[]) {
|
||||||
// send a message to background script with a list of URLs to check
|
// send a message to background script with a list of URLs to check
|
||||||
const urls = search_links.map((search_link) => {
|
const urls = search_links.map((search_link: SearchLink) => {
|
||||||
search_link.checked = true
|
search_link.checked = true
|
||||||
return search_link.target
|
return search_link.target
|
||||||
})
|
})
|
||||||
browser.runtime.sendMessage({type: "check", urls: urls.toArray()})
|
|
||||||
|
browser.runtime.sendMessage({type: "check", urls: urls})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function message_listener(message) {
|
async function backend_message_listener(message: any) {
|
||||||
// handle slop reports returned from the background script
|
// handle slop reports returned from the background script
|
||||||
if(message.type === "check_result") {
|
switch(message.type) {
|
||||||
console.log(message.url, message.result)
|
case ("check_result"):
|
||||||
const link = page_links.get(message.url)
|
if (message.domain) {
|
||||||
if ( message.result.slop_domain ) {
|
const paths = page_links.getDomain(message.domain)
|
||||||
link.node.setAttribute("style", "color: red;")
|
paths.forEach((search_link: SearchLink) => {
|
||||||
}
|
search_link.node.setAttribute("style", "color: red;")
|
||||||
link.result = message.result
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +105,8 @@ function get_initial_links() {
|
|||||||
page_links.setNode(node)
|
page_links.setNode(node)
|
||||||
})
|
})
|
||||||
const link_targets = page_links.getSearchLinks()
|
const link_targets = page_links.getSearchLinks()
|
||||||
check_links(link_targets)
|
// @ts-ignore
|
||||||
|
check_links(link_targets.toArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_links() {
|
function update_links() {
|
||||||
@@ -94,11 +115,12 @@ function update_links() {
|
|||||||
links.forEach((node) => {
|
links.forEach((node) => {
|
||||||
page_links.setNode(node)
|
page_links.setNode(node)
|
||||||
})
|
})
|
||||||
const link_iter = page_links.getSearchLinks().filter((search_link) => {
|
// @ts-ignore
|
||||||
|
const link_iter = page_links.getSearchLinks().filter((search_link: SearchLink) => {
|
||||||
return !(search_link.checked)
|
return !(search_link.checked)
|
||||||
})
|
})
|
||||||
|
|
||||||
check_links(link_iter)
|
check_links(link_iter.toArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
function setup_result_observer() {
|
function setup_result_observer() {
|
||||||
@@ -109,10 +131,10 @@ function setup_result_observer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function wait_for_results() {
|
async function wait_for_results() {
|
||||||
results = new Promise(async (resolve) => {
|
let results: Promise<Element> = new Promise(async (resolve) => {
|
||||||
let node = document.querySelector(ddg_result_list_selector)
|
let node = document.querySelector(ddg_result_list_selector)
|
||||||
while (!node) {
|
while (!node) {
|
||||||
await new Promise((resolve) => {setTimeout(()=>{resolve()}, 100)})
|
await new Promise<void>((resolve) => {setTimeout(()=>{resolve()}, 100)})
|
||||||
node = document.querySelector(ddg_result_list_selector)
|
node = document.querySelector(ddg_result_list_selector)
|
||||||
}
|
}
|
||||||
resolve(node)
|
resolve(node)
|
||||||
@@ -132,7 +154,7 @@ async function onload_handler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// listen for messages from the background script
|
// listen for messages from the background script
|
||||||
browser.runtime.onMessage.addListener(message_listener)
|
browser.runtime.onMessage.addListener(backend_message_listener)
|
||||||
|
|
||||||
// initialize state on document load
|
// initialize state on document load
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
251
src/report-slop.ts
Normal file
251
src/report-slop.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
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 "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)
|
||||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal 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
32
yarn.lock
Normal 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==
|
||||||
Reference in New Issue
Block a user