From 20a1598e265f45a361c6f10602336b97c6091c0d Mon Sep 17 00:00:00 2001 From: Rouge <14047458+rougetimelord@users.noreply.github.com> Date: Tue, 21 May 2024 21:37:24 -0700 Subject: [PATCH] 0.4.2 (#267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bump version string Need some diff to start an LLB PR * [fix] change condition for checking legacy if user used blue I did an oopsie whoopsie and flipped the logic in my brain!! * [doc] add all properties to the BlueBlockUser interface With the new integrations system I want to document everything that gets sent over so other extension devs can implement their own checks that use data that we don't. * [fix] refix skipping TweetTombstones Idk what happened here? Did I write code past my bed time? Maybe, no one can ever truly know * [fix] remove reference to chrome.runtime.restartAfterDelay * [chore] run prettier on src/background/index.ts * [feat] add endpoint for external extensions to automatically register as an integration External extensions can now send a request to Blue Blocker to be registered as a new integration. New integrations registered in this way are set to the disabled state by default, and the user of Blue Block still has to manually set the permissions of the new integration in order for the integration functionality to be enabled. This feature just makes it so that extension users do not have to find the extension ID of an external extension to register an integration. * Add the ability to block users based on their offensive usernames explain 'i' Reapply "case insensitive" This reverts commit ae0aa51c9a0b9fd4998d3b9af77269447cc4f6c5. Revert "case insensitive" This reverts commit 2272caa5de94123680001b5e983cb7698926ec57. case insensitive Update index.ts Update index.html Update shared.ts Stylizing changes, refactor element name, add status element [Fix[ Unclosed textarea tag that caused safelist and integrations buttons to not render Add a warning to the option explanation Change to a better console log string Use input number rules for textarea Should make the textarea look more cohesive with the rest of the UI [Fix] Typo Refactor disallowed words update func, remove uneeded error logging, strip whitespace before storing Update shared.ts remove unused function * switch firefox workflow to new action * quote zip url and source filename * simple check * Fixes severe false positives * Add space/end lookahead to the end of generated regex The array.join method does not add the separator to the end of the last array entry, but we need the lookahead on each entry to make sure we don't match on fragments inside of a word * adds the option to skip blue checkmark users * [fix] add registerAction definition Co-authored-by: dani <29378233+kheina@users.noreply.github.com> * [fix] use registerAction instead of bare string Co-authored-by: dani <29378233+kheina@users.noreply.github.com> * [fix] Simplify regex for disallowed words Thx Dani for saving me from being lost in the sauce <3 * Fix twitter x rename (#282) Closes #280 Co-authored-by: dani <29378233+DanielleMiu@users.noreply.github.com> * [fix] don't spread browser.runtime * [fix] replace consecutive spaces with a single space, not delete them * [fix] simplify skipBlueCheckmark check * [fix] trim whitespace first Co-authored-by: dani <29378233+kheina@users.noreply.github.com> * [fix] use new api url for tweetdeck requests * [fix] use chrome.runtime type definition annoying annoying annoying :( * [fix] escape regex characters * [fix] move RegExp escaping to utilities, don't escape the word list that sits at rest this will all make sense in a bit lol * [fix] precompile disallowed word regex, treat emojis differently, store both list and regexp * [fix] i forgor how escapes in strings work 💀 * compile config * [fix] Use better emoji regex * [fix] remove .list from config key selection lol oops * [fix] use null to signal an empty word list * [chore] run prettier * cleanup block for use and verified logic a lot, add feature use reason * added other reason codes * cleanup popup immensely * add some more logging and some fixes to the page functions * move ts-ignore * add block disallowed words option * prettier hell * move use blue features block outside verified block so it still catches people when skip blue is enabled * move block for use outside verified func * more accurate wording --------- Co-authored-by: MaxIsJoe <34368774+MaxIsJoe@users.noreply.github.com> Co-authored-by: dani <29378233+kheina@users.noreply.github.com> Co-authored-by: Eric Gallager --- .github/pull_request_template.md | 2 + .github/workflows/release.yml | 24 ++- .prettierignore | 1 + .prettierrc | 19 +- CHANGELOG.md | 72 ++++--- package-lock.json | 4 +- package.json | 2 +- readme.md | 4 +- src/background/db.ts | 267 +++++++++++++----------- src/background/index.ts | 331 +++++++++++++++++++++--------- src/constants.ts | 28 ++- src/content/index.ts | 109 +++++++--- src/content/startup.ts | 26 +-- src/global.d.ts | 79 ++++++- src/injected/inject.ts | 25 ++- src/manifest.ts | 30 +-- src/models/block_counter.ts | 7 +- src/models/block_queue.ts | 19 +- src/models/queue_consumer.ts | 34 ++- src/pages/history/index.html | 20 +- src/pages/history/index.ts | 272 +++++++++++++----------- src/pages/integrations/index.html | 12 +- src/pages/integrations/index.ts | 230 +++++++++++---------- src/pages/integrations/style.css | 16 +- src/pages/queue/index.html | 60 ++++-- src/pages/queue/index.ts | 211 ++++++++++--------- src/pages/safelist/index.html | 21 +- src/pages/safelist/index.ts | 221 ++++++++++++-------- src/pages/safelist/style.css | 2 +- src/pages/style.css | 35 ++-- src/parsers/instructions.ts | 29 +-- src/parsers/search.ts | 6 +- src/parsers/timeline.ts | 2 +- src/parsers/unblock.ts | 14 +- src/popup/index.html | 65 ++++-- src/popup/index.ts | 155 ++++++++++---- src/popup/style.css | 71 +++++-- src/shared.ts | 310 +++++++++++++++------------- src/utilities.ts | 129 ++++++++---- 39 files changed, 1835 insertions(+), 1129 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 75592e0..47d6666 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,6 +3,7 @@ This is a long-lived branch, intended to aggregate all changes planned for versi Pull requests should be made against this branch. Merges to this branch should be done via a merge commit. When ready, this branch should be merged to main via a squash commit. ## Changelog + ```txt Summary 1. document grouping follow 'SemVer2.0' protocol @@ -13,6 +14,7 @@ Summary ``` ## Deployment Checklist + 1. [ ] merge all pull requests to llb 2. [ ] ensure `src/manifest.ts` and `package.json` have the correct version number 3. [ ] use makefile to generate zips (`make chrome`, `make firefox`) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16752d6..5d04539 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,7 @@ jobs: runs-on: ubuntu-latest outputs: filename: ${{ steps.build.outputs.filename }} + sourcefile: ${{ steps.src.outputs.sourcefile }} steps: - uses: actions/checkout@v4 - name: Set up Node.js @@ -24,17 +25,24 @@ jobs: run: | make firefox echo "filename=blue-blocker-firefox-$(make version).zip" >> "$GITHUB_OUTPUT" + - name: downloading source code + id: src + run: | + export SOURCE_FILENAME="blue-blocker-source-v$(make version).zip" + curl --connect-timeout 5 --retry 10 --retry-delay 10 --retry-max-time 30 "${{ github.event.zipball_url }}" -o "$SOURCE_FILENAME" + echo "sourcefile=$SOURCE_FILENAME" >> "$GITHUB_OUTPUT" - name: publish firefox - uses: wdzeng/firefox-addon@v1.1.0-alpha.0 + uses: browser-actions/release-firefox-addon@v0.1.3 # https://github.com/marketplace/actions/release-firefox-addon#inputs with: - addon-guid: "{119be3f3-597c-4f6a-9caf-627ee431d374}" - xpi-path: "${{ steps.build.outputs.filename }}" - self-hosted: false - release-notes: "{\"en-US\": toJSON(${{ github.event.body }})}" # this should be the content of the release, make sure to include changelog - approval-notes: "source code for this version is available at https://github.com/kheina-com/Blue-Blocker/releases/tag/${{ github.event.tag_name }}\nrunning `npm run build` and then `make firefox` should build the addon package, then load and use twitter. you should start seeing users be blocked fairly quickly" + addon-id: "{119be3f3-597c-4f6a-9caf-627ee431d374}" + addon-path: "${{ steps.build.outputs.filename }}" + source-path: "${{ steps.src.outputs.sourcefile }}" + approval-note: "The extension can be built and tested locally using `make firefox`. Simply navigate to twitter.com and you should see users being queued and blocked fairly quickly." + release-note: "{\"en-US\": ${{ toJSON(toJSON(github.event.body)) }}}" # this should be the content of the release, make sure to include changelog. also yes the double use of tojson is intentional + compatibility-firefox-min: 48.0 license: MPL-2.0 - jwt-issuer: ${{ secrets.MOZILLA_ADDONS_JWT_ISSUER }} - jwt-secret: ${{ secrets.MOZILLA_ADDONS_JWT_SECRET }} + auth-api-issuer: ${{ secrets.MOZILLA_ADDONS_JWT_ISSUER }} + auth-api-secret: ${{ secrets.MOZILLA_ADDONS_JWT_SECRET }} Release-Chrome: runs-on: ubuntu-latest diff --git a/.prettierignore b/.prettierignore index daa3834..f08b07f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ # Ignore artifacts: +.github build coverage node_modules diff --git a/.prettierrc b/.prettierrc index 6516d40..f1ba3ab 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,10 +1,13 @@ { - "jsxSingleQuote": false, - "singleQuote": true, - "trailingComma": "all", - "endOfLine": "lf", - "printWidth": 100, - "semi": true, - "tabWidth": 4, - "useTabs": true + "jsxSingleQuote": false, + "singleQuote": true, + "trailingComma": "all", + "endOfLine": "lf", + "printWidth": 100, + "semi": true, + "tabWidth": 4, + "useTabs": true, + "arrowParens": "avoid", + "bracketSpacing": true, + "bracketSameLine": true } diff --git a/CHANGELOG.md b/CHANGELOG.md index 98828cf..4239ea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # CHANGELOG + ```txt Summary 1. document grouping follow 'SemVer2.0' protocol @@ -9,44 +10,49 @@ Summary ``` # v0.3.4 [2023.07.04] -- feat: show alert when user is logged out -- patch: write to history when block fails due to account deletion -- fix: retry after db failure -- fix: errors not surfacing during db transactions -- chore: add typedefs to all messages -- chore: display variance in popup + +- feat: show alert when user is logged out +- patch: write to history when block fails due to account deletion +- fix: retry after db failure +- fix: errors not surfacing during db transactions +- chore: add typedefs to all messages +- chore: display variance in popup # v0.3.3 [2023.07.01] -- feat: track block history, click blocked number in context menu to access (#63) -- feat: added safelist control buttons: import, export, clear -- feat: import block lists into queue via json files (#145) -- feat: added close button to all popups (#68) -- feat: auto-safelist unblocked users (#136) -- update: popups can now be placed in any corner of the screen (#68) -- fix: skip legacy verified error recovery -- chore: make toasts slightly slimmer -- chore: more and better header assignment -- chore: add entropy to block interval + +- feat: track block history, click blocked number in context menu to access (#63) +- feat: added safelist control buttons: import, export, clear +- feat: import block lists into queue via json files (#145) +- feat: added close button to all popups (#68) +- feat: auto-safelist unblocked users (#136) +- update: popups can now be placed in any corner of the screen (#68) +- fix: skip legacy verified error recovery +- chore: make toasts slightly slimmer +- chore: more and better header assignment +- chore: add entropy to block interval # v0.3.2 [2023.06.28] -- fix: chrome prior to version 109 -- fix: stop parsing twitter error responses when sent using status 200 -- fix: stop blocking automated accounts (those clearly labelled as such by twitter) -- fix: better error handling in blocking logic, wrap legacy verified logic to prevent deadlocks when db doesn't start -- chore: remove some unneeded debug logs + +- fix: chrome prior to version 109 +- fix: stop parsing twitter error responses when sent using status 200 +- fix: stop blocking automated accounts (those clearly labelled as such by twitter) +- fix: better error handling in blocking logic, wrap legacy verified logic to prevent deadlocks when db doesn't start +- chore: remove some unneeded debug logs # v0.3.1 [2023.06.27] -- feat: overhaul popup menu to have new quickmenu for quick changes and an advanced tab for full options list (#141) -- remove: management permissions requirement in manifest (#155) -- chore: update integration logic to send test message instead of using management api -- feat: block users promoting tweets (#5) + +- feat: overhaul popup menu to have new quickmenu for quick changes and an advanced tab for full options list (#141) +- remove: management permissions requirement in manifest (#155) +- chore: update integration logic to send test message instead of using management api +- feat: block users promoting tweets (#5) ## v0.3.0 [2023.06.26] -- chore: migrate to create-chrome-ext with typescript -- chore: also check profile shape to detect nft avatars (#130) -- feat: use travis brown's verified db to check legacy verifications (#134) -- feat: integration with soupcan (#139) -- fix: queue locking and overly fast blocking (#66) -- fix: critical point logic in queue and counter new generate ref ids per lock -- fix: ignore error responses from twitter (#142) -- perf: change user object in queue to be slimmer + +- chore: migrate to create-chrome-ext with typescript +- chore: also check profile shape to detect nft avatars (#130) +- feat: use travis brown's verified db to check legacy verifications (#134) +- feat: integration with soupcan (#139) +- fix: queue locking and overly fast blocking (#66) +- fix: critical point logic in queue and counter new generate ref ids per lock +- fix: ignore error responses from twitter (#142) +- perf: change user object in queue to be slimmer diff --git a/package-lock.json b/package-lock.json index dd5ff0e..74f95f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blue-blocker", - "version": "0.4.1", + "version": "0.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blue-blocker", - "version": "0.4.1", + "version": "0.4.2", "license": "MPL-2.0", "devDependencies": { "@crxjs/vite-plugin": "^1.0.14", diff --git a/package.json b/package.json index 71029af..75ec641 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blue-blocker", - "version": "0.4.1", + "version": "0.4.2", "author": "DanielleMiu", "description": "Blocks all Twitter Blue verified users on twitter.com", "type": "module", diff --git a/readme.md b/readme.md index 9640c23..7ab2722 100644 --- a/readme.md +++ b/readme.md @@ -31,7 +31,7 @@ npm run dev 1. run `npm run dev` or `npm run build` 2. Visit the [chrome extentions page](chrome://extensions/) - 1. (or enter `chrome://extensions/` in the Chrome url bar) + 1. (or enter `chrome://extensions/` in the Chrome url bar) 3. Enable `Developer mode` in the top right 4. Click `Load unpacked` in the top left and select `blue-blocker/build` folder @@ -40,7 +40,7 @@ npm run dev 1. Run `npm run build` 2. Run `make firefox` 3. Visit the [firefox addon debugging page](about:debugging#/runtime/this-firefox) - 1. (or enter `about:debugging#/runtime/this-firefox` in the Firefox url bar) + 1. (or enter `about:debugging#/runtime/this-firefox` in the Firefox url bar) 4. Click `Load Temporary Add-on` in the top right and select `manifest.json` in the `blue-blocker/build` folder ## License diff --git a/src/background/db.ts b/src/background/db.ts index 803f9bf..533d6da 100644 --- a/src/background/db.ts +++ b/src/background/db.ts @@ -1,5 +1,14 @@ -import { api, logstr, EventKey, LegacyVerifiedUrl, MessageEvent, ErrorEvent, HistoryStateBlocked, HistoryStateUnblocked } from "../constants"; -import { commafy, QueueId } from "../utilities"; +import { + api, + logstr, + EventKey, + LegacyVerifiedUrl, + MessageEvent, + ErrorEvent, + HistoryStateBlocked, + HistoryStateUnblocked, +} from '../constants'; +import { commafy, QueueId } from '../utilities'; const expectedVerifiedUsersCount = 407520; let legacyDb: IDBDatabase; @@ -7,41 +16,41 @@ let legacyDb: IDBDatabase; let legacyDbLoaded: boolean = false; interface LegacyVerifiedUser { - user_id: string, - handle: string, + user_id: string; + handle: string; } -const legacyDbName = "legacy-verified-users"; -const legacyDbStore = "verified_users"; +const legacyDbName = 'legacy-verified-users'; +const legacyDbStore = 'verified_users'; const legacyDbVersion = 1; // populates local storage with legacy verified users for the purpose of avoiding legacy verified export async function PopulateVerifiedDb() { const opts = await api.storage.sync.get({ skipVerified: true }); if (!opts.skipVerified) { - console.log(logstr, "skip verified false, not populating legacyDb", opts); + console.log(logstr, 'skip verified false, not populating legacyDb', opts); return; } const DBOpenRequest = indexedDB.open(legacyDbName, legacyDbVersion); DBOpenRequest.onerror = DBOpenRequest.onblocked = () => { - console.error(logstr, "failed to open legacy verified user database:", DBOpenRequest); + console.error(logstr, 'failed to open legacy verified user database:', DBOpenRequest); }; DBOpenRequest.onupgradeneeded = e => { - console.debug(logstr, "legacy db onupgradeneeded:", e); + console.debug(logstr, 'legacy db onupgradeneeded:', e); legacyDb = DBOpenRequest.result; if (legacyDb.objectStoreNames.contains(legacyDbStore)) { return; } - legacyDb.createObjectStore(legacyDbStore, { keyPath: "user_id" }); - console.log(logstr, "created legacy database."); + legacyDb.createObjectStore(legacyDbStore, { keyPath: 'user_id' }); + console.log(logstr, 'created legacy database.'); }; DBOpenRequest.onsuccess = async () => { - console.debug(logstr, "successfully connected to legacy db"); + console.debug(logstr, 'successfully connected to legacy db'); legacyDb = DBOpenRequest.result; if (legacyDbLoaded) { @@ -50,11 +59,11 @@ export async function PopulateVerifiedDb() { api.storage.sync.set({ skipVerified: true }); return; } - console.debug(logstr, "checking verified user database."); + console.debug(logstr, 'checking verified user database.'); try { await new Promise((resolve, reject) => { - const transaction = legacyDb.transaction([legacyDbStore], "readwrite"); + const transaction = legacyDb.transaction([legacyDbStore], 'readwrite'); const store = transaction.objectStore(legacyDbStore); const req = store.count(); @@ -62,30 +71,36 @@ export async function PopulateVerifiedDb() { req.onsuccess = () => { const count = req.result as number; if (count !== expectedVerifiedUsersCount) { - reject(`legacy verified users database (${commafy(count)}) did not contain the expected number of users (${commafy(expectedVerifiedUsersCount)})`); + reject( + `legacy verified users database (${commafy( + count, + )}) did not contain the expected number of users (${commafy( + expectedVerifiedUsersCount, + )})`, + ); } else { - console.log(logstr, "loaded", count, "legacy verified users"); + console.log(logstr, 'loaded', count, 'legacy verified users'); resolve(); } }; }); - } - catch (_e) { + } catch (_e) { // const e = _e as Error; await new Promise((resolve, reject) => { - const transaction = legacyDb.transaction([legacyDbStore], "readwrite"); + const transaction = legacyDb.transaction([legacyDbStore], 'readwrite'); const store = transaction.objectStore(legacyDbStore); const req = store.clear(); req.onerror = reject; req.onsuccess = () => { - console.debug(logstr, "cleared existing legacyDb store."); + console.debug(logstr, 'cleared existing legacyDb store.'); resolve(); }; }); (() => { - const message = "downloading legacy verified users database, this may take a few minutes."; + const message = + 'downloading legacy verified users database, this may take a few minutes.'; api.storage.local.set({ [EventKey]: { type: MessageEvent, @@ -96,21 +111,20 @@ export async function PopulateVerifiedDb() { })(); let count: number = 0; - const body = await fetch(LegacyVerifiedUrl) - .then(r => r.text()); + const body = await fetch(LegacyVerifiedUrl).then(r => r.text()); let intact: boolean = false; - const transaction = legacyDb.transaction([legacyDbStore], "readwrite"); + const transaction = legacyDb.transaction([legacyDbStore], 'readwrite'); const store = transaction.objectStore(legacyDbStore); - for (const line of body.split("\n")) { - if (line === "Twitter ID, Screen name, Followers") { - console.debug(logstr, "response csv good!"); + for (const line of body.split('\n')) { + if (line === 'Twitter ID, Screen name, Followers') { + console.debug(logstr, 'response csv good!'); intact = true; continue; - } - else if (!intact) { - const message = "legacy verified users database was mangled or otherwise unable to be parsed"; + } else if (!intact) { + const message = + 'legacy verified users database was mangled or otherwise unable to be parsed'; console.error(logstr, message); api.storage.local.set({ [EventKey]: { @@ -122,7 +136,7 @@ export async function PopulateVerifiedDb() { } await new Promise((resolve, reject) => { - const [user_id, handle, _] = line.split(","); + const [user_id, handle, _] = line.split(','); const item: LegacyVerifiedUser = { user_id, handle }; const req = store.add(item); @@ -130,7 +144,7 @@ export async function PopulateVerifiedDb() { req.onsuccess = () => { count++; if (count % 1000 === 0) { - console.debug(logstr, "stored 1,000 legacy verified users"); + console.debug(logstr, 'stored 1,000 legacy verified users'); } resolve(); }; @@ -138,7 +152,13 @@ export async function PopulateVerifiedDb() { } transaction.commit(); - console.debug(logstr, "committed", count, "users to legacy verified legacyDb:", transaction); + console.debug( + logstr, + 'committed', + count, + 'users to legacy verified legacyDb:', + transaction, + ); const message = `loaded ${commafy(count)} legacy verified users!`; console.log(logstr, message); @@ -150,20 +170,26 @@ export async function PopulateVerifiedDb() { }); if (count !== expectedVerifiedUsersCount) { - throw new Error(`legacy verified users database (${commafy(count)}) did not contain the expected number of users (${commafy(expectedVerifiedUsersCount)})`); + throw new Error( + `legacy verified users database (${commafy( + count, + )}) did not contain the expected number of users (${commafy( + expectedVerifiedUsersCount, + )})`, + ); } } api.storage.sync.set({ skipVerified: true }); legacyDbLoaded = true; }; - console.debug(logstr, "opening legacy verified user database:", DBOpenRequest); + console.debug(logstr, 'opening legacy verified user database:', DBOpenRequest); } export function CheckDbIsUserLegacyVerified(user_id: string, handle: string): Promise { // @ts-ignore // typescript is wrong here, this cannot return idb due to final throws return new Promise((resolve, reject) => { - const transaction = legacyDb.transaction([legacyDbStore], "readonly"); + const transaction = legacyDb.transaction([legacyDbStore], 'readonly'); transaction.onabort = transaction.onerror = reject; const store = transaction.objectStore(legacyDbStore); const req = store.get(user_id); @@ -176,20 +202,19 @@ export function CheckDbIsUserLegacyVerified(user_id: string, handle: string): Pr }).catch(e => { // if the db has already been loaded, we can safely reconnect if (legacyDbLoaded) { - return PopulateVerifiedDb() - .finally(() => { - throw e; // re-throw error + return PopulateVerifiedDb().finally(() => { + throw e; // re-throw error }); } - throw e; // re-throw error + throw e; // re-throw error }); } let db: IDBDatabase; -const dbName = "blue-blocker-db"; -export const historyDbStore = "blocked_users"; -export const queueDbStore = "block_queue"; +const dbName = 'blue-blocker-db'; +export const historyDbStore = 'blocked_users'; +export const queueDbStore = 'block_queue'; const dbVersion = 2; // used so we don't load the db twice let dbLoaded: boolean = false; @@ -200,27 +225,27 @@ export function ConnectDb(): Promise { // this logic should also be much easier because we don't need to populate anything (thank god) const DBOpenRequest = indexedDB.open(dbName, dbVersion); DBOpenRequest.onerror = DBOpenRequest.onblocked = () => { - console.error(logstr, "failed to connect database:", DBOpenRequest); + console.error(logstr, 'failed to connect database:', DBOpenRequest); return reject(); }; DBOpenRequest.onupgradeneeded = e => { - console.debug(logstr, "upgrading db:", e); + console.debug(logstr, 'upgrading db:', e); db = DBOpenRequest.result; if (!db.objectStoreNames.contains(historyDbStore)) { - const store = db.createObjectStore(historyDbStore, { keyPath: "user_id" }); - store.createIndex("user.name", "user.name", { unique: false }); - store.createIndex("user.screen_name", "user.screen_name", { unique: false }); - store.createIndex("time", "time", { unique: false }); - console.log(logstr, "created history database."); + const store = db.createObjectStore(historyDbStore, { keyPath: 'user_id' }); + store.createIndex('user.name', 'user.name', { unique: false }); + store.createIndex('user.screen_name', 'user.screen_name', { unique: false }); + store.createIndex('time', 'time', { unique: false }); + console.log(logstr, 'created history database.'); } if (!db.objectStoreNames.contains(queueDbStore)) { - const store = db.createObjectStore(queueDbStore, { keyPath: "user_id" }); - store.createIndex("user_id", "user_id", { unique: true }); - store.createIndex("queue", "queue", { unique: false }); - console.log(logstr, "created queue database."); + const store = db.createObjectStore(queueDbStore, { keyPath: 'user_id' }); + store.createIndex('user_id', 'user_id', { unique: true }); + store.createIndex('queue', 'queue', { unique: false }); + console.log(logstr, 'created queue database.'); } }; @@ -233,13 +258,13 @@ export function ConnectDb(): Promise { const items = await api.storage.local.get({ BlockQueue: [] }); if (items?.BlockQueue?.length !== undefined && items?.BlockQueue?.length > 0) { - const transaction = db.transaction([queueDbStore], "readwrite"); - transaction.onabort = transaction.onerror = reject; + const transaction = db.transaction([queueDbStore], 'readwrite'); + transaction.onabort = transaction.onerror = reject; const store = transaction.objectStore(queueDbStore); items.BlockQueue.forEach((item: BlockUser) => { // required for users enqueued before 0.3.0 - if (item.user.hasOwnProperty("legacy")) { + if (item.user.hasOwnProperty('legacy')) { // @ts-ignore item.user.name = item.user.legacy.name; // @ts-ignore @@ -263,10 +288,15 @@ export function ConnectDb(): Promise { api.storage.local.set({ BlockQueue: null }); transaction.commit(); - console.debug(logstr, "imported", items.BlockQueue.length, "users from local storage queue"); + console.debug( + logstr, + 'imported', + items.BlockQueue.length, + 'users from local storage queue', + ); } - console.log(logstr, "successfully connected to db"); + console.log(logstr, 'successfully connected to db'); return resolve(db); }; }); @@ -275,7 +305,7 @@ export function ConnectDb(): Promise { export function AddUserToHistory(user: BlockedUser): Promise { // @ts-ignore // typescript is wrong here, this cannot return idb due to final throw return new Promise((resolve, reject) => { - const transaction = db.transaction([historyDbStore], "readwrite"); + const transaction = db.transaction([historyDbStore], 'readwrite'); transaction.onabort = transaction.onerror = reject; transaction.oncomplete = () => resolve(); @@ -285,10 +315,9 @@ export function AddUserToHistory(user: BlockedUser): Promise { transaction.commit(); }).catch(e => // attempt to reconnect to the db - ConnectDb() - .finally(() => { - throw e; // re-throw error to retry - }) + ConnectDb().finally(() => { + throw e; // re-throw error to retry + }), ); } @@ -296,7 +325,7 @@ export function RemoveUserFromHistory(user_id: string): Promise { // @ts-ignore // typescript is wrong here, this cannot return idb due to final throw return new Promise(async (resolve, reject) => { try { - const transaction = db.transaction([historyDbStore], "readwrite"); + const transaction = db.transaction([historyDbStore], 'readwrite'); transaction.onabort = transaction.onerror = reject; transaction.oncomplete = () => resolve(); @@ -322,19 +351,18 @@ export function RemoveUserFromHistory(user_id: string): Promise { } }).catch(e => // attempt to reconnect to the db - ConnectDb() - .finally(() => { - throw e; // re-throw error to retry - }) + ConnectDb().finally(() => { + throw e; // re-throw error to retry + }), ); } interface QueueUser { - queue: number, - user_id: string, - user: { name: string, screen_name: string }, - reason: number, - external_reason?: string, + queue: number; + user_id: string; + user: { name: string; screen_name: string }; + reason: number; + external_reason?: string; } export function AddUserToQueue(blockUser: BlockUser): Promise { @@ -354,7 +382,7 @@ export function AddUserToQueue(blockUser: BlockUser): Promise { // @ts-ignore // typescript is wrong here, this cannot return idb due to final throw return new Promise((resolve, reject) => { - const transaction = db.transaction([queueDbStore], "readwrite"); + const transaction = db.transaction([queueDbStore], 'readwrite'); transaction.onabort = transaction.onerror = reject; const store = transaction.objectStore(queueDbStore); @@ -362,11 +390,10 @@ export function AddUserToQueue(blockUser: BlockUser): Promise { transaction.oncomplete = () => resolve(); transaction.commit(); }).catch(e => { - if (e?.target?.error?.name !== "ConstraintError") { + if (e?.target?.error?.name !== 'ConstraintError') { // attempt to reconnect to the db - return ConnectDb() - .finally(() => { - throw e; // re-throw error to retry + return ConnectDb().finally(() => { + throw e; // re-throw error to retry }); } }); @@ -375,10 +402,10 @@ export function AddUserToQueue(blockUser: BlockUser): Promise { export function PopUserFromQueue(): Promise { // @ts-ignore // typescript is wrong here, this cannot return idb due to final throw return new Promise(async (resolve, reject) => { - const transaction = db.transaction([queueDbStore], "readwrite"); + const transaction = db.transaction([queueDbStore], 'readwrite'); transaction.onabort = transaction.onerror = reject; const store = transaction.objectStore(queueDbStore); - const index = store.index("queue"); + const index = store.index('queue'); const result = await new Promise((res, rej) => { const req = index.get(IDBKeyRange.bound(Number.MIN_VALUE, Number.MAX_VALUE)); @@ -410,52 +437,52 @@ export function PopUserFromQueue(): Promise { transaction.oncomplete = () => resolve(user); }).catch(e => // attempt to reconnect to the db - ConnectDb() - .finally(() => { - throw e; // re-throw error to retry - }) + ConnectDb().finally(() => { + throw e; // re-throw error to retry + }), ); } export function WholeQueue(): Promise { - return ConnectDb().then(qdb => { - return new Promise((resolve, reject) => { - const transaction = qdb.transaction([queueDbStore], "readonly"); - transaction.onabort = transaction.onerror = reject; - const store = transaction.objectStore(queueDbStore); - const index = store.index("queue"); - const req = index.getAll(IDBKeyRange.bound(Number.MIN_VALUE, Number.MAX_VALUE), 10000); + return ConnectDb() + .then(qdb => { + return new Promise((resolve, reject) => { + const transaction = qdb.transaction([queueDbStore], 'readonly'); + transaction.onabort = transaction.onerror = reject; + const store = transaction.objectStore(queueDbStore); + const index = store.index('queue'); + const req = index.getAll( + IDBKeyRange.bound(Number.MIN_VALUE, Number.MAX_VALUE), + 10000, + ); - req.onerror = reject; - req.onsuccess = () => { - const users = req.result as BlockUser[]; - resolve(users); - }; - }); - }).catch(() => - api.storage.local.get({ BlockQueue: [] }).then(items => - items?.BlockQueue - ) - ); + req.onerror = reject; + req.onsuccess = () => { + const users = req.result as BlockUser[]; + resolve(users); + }; + }); + }) + .catch(() => api.storage.local.get({ BlockQueue: [] }).then(items => items?.BlockQueue)); } export function QueueLength(): Promise { - return ConnectDb().then(qdb => { - return new Promise((resolve, reject) => { - const transaction = qdb.transaction([queueDbStore], "readonly"); - transaction.onabort = transaction.onerror = reject; - const store = transaction.objectStore(queueDbStore); - const req = store.count(); + return ConnectDb() + .then(qdb => { + return new Promise((resolve, reject) => { + const transaction = qdb.transaction([queueDbStore], 'readonly'); + transaction.onabort = transaction.onerror = reject; + const store = transaction.objectStore(queueDbStore); + const req = store.count(); - req.onerror = reject; - req.onsuccess = () => { - const users = req.result as number; - resolve(users); - }; - }); - }).catch(() => - api.storage.local.get({ BlockQueue: [] }).then(items => - items?.BlockQueue?.length - ) - ); + req.onerror = reject; + req.onsuccess = () => { + const users = req.result as number; + resolve(users); + }; + }); + }) + .catch(() => + api.storage.local.get({ BlockQueue: [] }).then(items => items?.BlockQueue?.length), + ); } diff --git a/src/background/index.ts b/src/background/index.ts index 9e2561f..e9ee7e4 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,16 +1,42 @@ -import { api, logstr, AddToHistoryAction, ErrorStatus, IsVerifiedAction, ReasonExternal, RemoveFromHistoryAction, SoupcanExtensionId, SuccessStatus, DefaultOptions, AddToQueueAction, PopFromQueueAction, IntegrationStateSendAndReceive, IntegrationStateDisabled, IntegrationStateSendOnly } from '../constants'; +import { + api, + logstr, + AddToHistoryAction, + ErrorStatus, + IsVerifiedAction, + ReasonExternal, + RemoveFromHistoryAction, + SoupcanExtensionId, + SuccessStatus, + DefaultOptions, + AddToQueueAction, + PopFromQueueAction, + IntegrationStateSendAndReceive, + IntegrationStateDisabled, + IntegrationStateSendOnly, + EventKey, + MessageEvent, +} from '../constants'; import { abbreviate, RefId } from '../utilities'; -import { AddUserToHistory, AddUserToQueue, CheckDbIsUserLegacyVerified, ConnectDb, PopUserFromQueue, PopulateVerifiedDb, RemoveUserFromHistory } from './db'; +import { + AddUserToHistory, + AddUserToQueue, + CheckDbIsUserLegacyVerified, + ConnectDb, + PopUserFromQueue, + PopulateVerifiedDb, + RemoveUserFromHistory, +} from './db'; -api.action.setBadgeBackgroundColor({ color: "#666" }); -if (api.action.hasOwnProperty("setBadgeTextColor")) { +api.action.setBadgeBackgroundColor({ color: '#666' }); +if (api.action.hasOwnProperty('setBadgeTextColor')) { // setBadgeTextColor requires chrome 110+ - api.action.setBadgeTextColor({ color: "#fff" }); + api.action.setBadgeTextColor({ color: '#fff' }); } // TODO: change to message listener ? -api.storage.local.onChanged.addListener((items) => { - if (items.hasOwnProperty("BlockCounter")) { +api.storage.local.onChanged.addListener(items => { + if (items.hasOwnProperty('BlockCounter')) { api.action.setBadgeText({ text: abbreviate(items.BlockCounter.newValue), }); @@ -19,7 +45,11 @@ api.storage.local.onChanged.addListener((items) => { api.storage.sync.get(DefaultOptions).then(async items => { // set initial extension state - api.action.setIcon({ path: items.suspendedBlockCollection ? "/icon/icon-128-greyscale.png" : "/icon/icon-128.png" }); + api.action.setIcon({ + path: items.suspendedBlockCollection + ? '/icon/icon-128-greyscale.png' + : '/icon/icon-128.png', + }); if (items.skipVerified) { await PopulateVerifiedDb(); } @@ -38,102 +68,203 @@ api.storage.sync.onChanged.addListener(async items => { ConnectDb(); -api.runtime.onMessage.addListener((m, s, r) => { let response: MessageResponse; (async (message: RuntimeMessage, sender) => { - const refid = RefId(); - console.debug(logstr, refid, "recv:", message, sender); - // messages are ALWAYS expected to be: - // 1. objects - // 2. contain a string value stored under message.action. should be one defined above - // other message contents change based on the defined action - try { - switch (message?.action) { - case IsVerifiedAction: - const verifiedMessage = message.data as { user_id: string, handle: string }; - const isVerified = await CheckDbIsUserLegacyVerified(verifiedMessage.user_id, verifiedMessage.handle); - response = { status: SuccessStatus, result: isVerified } as SuccessResponse; - break; - - case AddToHistoryAction: - const historyMessage = message.data as BlockedUser; - await AddUserToHistory(historyMessage); - response = { status: SuccessStatus, result: null } as SuccessResponse; - break; - - case RemoveFromHistoryAction: - const removeMessage = message.data as { user_id: string }; - await RemoveUserFromHistory(removeMessage.user_id); - response = { status: SuccessStatus, result: null } as SuccessResponse; - break; - - case AddToQueueAction: - const addToQueueMessage = message.data as BlockUser; - await AddUserToQueue(addToQueueMessage); - response = { status: SuccessStatus, result: null } as SuccessResponse; - break; - - case PopFromQueueAction: - // no payload with this request - const user = await PopUserFromQueue(); - response = { status: SuccessStatus, result: user } as SuccessResponse; - break; - - default: - console.error(logstr, refid, "got a message that couldn't be handled from sender:", sender, message); - response = { status: ErrorStatus, message: "unknown action" } as ErrorResponse; +api.runtime.onMessage.addListener((m, s, r) => { + let response: MessageResponse; + (async (message: RuntimeMessage, sender) => { + const refid = RefId(); + console.debug(logstr, refid, 'recv:', message, sender); + // messages are ALWAYS expected to be: + // 1. objects + // 2. contain a string value stored under message.action. should be one defined above + // other message contents change based on the defined action + try { + switch (message?.action) { + case IsVerifiedAction: + const verifiedMessage = message.data as { user_id: string; handle: string }; + const isVerified = await CheckDbIsUserLegacyVerified( + verifiedMessage.user_id, + verifiedMessage.handle, + ); + response = { status: SuccessStatus, result: isVerified } as SuccessResponse; + break; + + case AddToHistoryAction: + const historyMessage = message.data as BlockedUser; + await AddUserToHistory(historyMessage); + response = { status: SuccessStatus, result: null } as SuccessResponse; + break; + + case RemoveFromHistoryAction: + const removeMessage = message.data as { user_id: string }; + await RemoveUserFromHistory(removeMessage.user_id); + response = { status: SuccessStatus, result: null } as SuccessResponse; + break; + + case AddToQueueAction: + const addToQueueMessage = message.data as BlockUser; + await AddUserToQueue(addToQueueMessage); + response = { status: SuccessStatus, result: null } as SuccessResponse; + break; + + case PopFromQueueAction: + // no payload with this request + const user = await PopUserFromQueue(); + response = { status: SuccessStatus, result: user } as SuccessResponse; + break; + + default: + console.error( + logstr, + refid, + "got a message that couldn't be handled from sender:", + sender, + message, + ); + response = { status: ErrorStatus, message: 'unknown action' } as ErrorResponse; + } + } catch (_e) { + const e = _e as Error; + console.error( + logstr, + refid, + 'unexpected error caught during', + message?.action, + 'action', + e, + ); + response = { + status: ErrorStatus, + message: e.message ?? 'unknown error', + } as ErrorResponse; } - } catch (_e) { - const e = _e as Error; - console.error(logstr, refid, "unexpected error caught during", message?.action, "action", e); - response = { status: ErrorStatus, message: e.message ?? "unknown error" } as ErrorResponse; - } - console.debug(logstr, refid, "respond:", response); -})(m, s).finally(() => r(response)); return true }); + console.debug(logstr, refid, 'respond:', response); + })(m, s).finally(() => r(response)); + return true; +}); ////////////////////////////////////////////////// EXTERNAL MESSAGE HANDLING ////////////////////////////////////////////////// -const [blockActionV1, blockAction] = ["BLOCK", "block_user"]; +const [blockActionV1, blockAction, registerAction] = ['BLOCK', 'block_user', 'register']; -api.runtime.onMessageExternal.addListener((m, s, r) => { let response: MessageResponse; (async (message, sender) => { - const refid = RefId(); - console.debug(logstr, refid, "ext recv:", message, sender); - const integrations = await api.storage.local.get({ soupcanIntegration: false, integrations: { } }) as { [id: string]: Integration }; - const senderId = sender.id ?? ""; - if (!integrations.hasOwnProperty(senderId)) { - response = { status: ErrorStatus, message: "extension not allowed" } as ErrorResponse; - return; - } - if (integrations[senderId].state === IntegrationStateDisabled || integrations[senderId].state === IntegrationStateSendOnly) { - response = { status: ErrorStatus, message: "extension disabled or not allowed to send messages" } as ErrorResponse; - return; - } +api.runtime.onMessageExternal.addListener((m, s, r) => { + let response: MessageResponse; + (async (message, sender) => { + const refid = RefId(); + console.debug(logstr, refid, 'ext recv:', message, sender); + const integrations = ( + await api.storage.local.get({ soupcanIntegration: false, integrations: {} }) + ).integrations as { [id: string]: Integration }; + const senderId = sender.id ?? ''; + if (!integrations.hasOwnProperty(senderId)) { + if (message?.action === registerAction) { + //External extension wants to register + const reg_request = message as RegisterRequest; + integrations[senderId] = { + name: reg_request.name, + state: IntegrationStateDisabled, + }; + api.storage.local.set({ + integrations, + [EventKey]: { + type: MessageEvent, + message: `

The extension ${ + reg_request.name + } would like to integrate with BlueBlocker.
Visit the integrations page to complete set up.

`, + options: { html: true }, + }, + }); + response = { + status: SuccessStatus, + result: 'integration registered', + } as SuccessResponse; + console.debug( + logstr, + refid, + `registered a new extention: ${reg_request.name} ${senderId}. ext resp:`, + response, + ); + return; + } + response = { status: ErrorStatus, message: 'extension not allowed' } as ErrorResponse; + return; + } + if ( + integrations[senderId].state === IntegrationStateDisabled || + integrations[senderId].state === IntegrationStateSendOnly + ) { + response = { + status: ErrorStatus, + message: 'extension disabled or not allowed to send messages', + } as ErrorResponse; + return; + } + + // messages are ALWAYS expected to be: + // 1. objects + // 2. contain a string value stored under message.action. should be one defined above + // other message contents change based on the defined action + try { + switch (message?.action) { + case blockActionV1: + const blockV1Message = message as { + user_id: string; + name: string; + screen_name: string; + reason: string; + }; + const userV1: BlockUser = { + user_id: blockV1Message.user_id, + user: { + name: blockV1Message.name, + screen_name: blockV1Message.screen_name, + }, + reason: ReasonExternal, + external_reason: blockV1Message.reason, + }; + await AddUserToQueue(userV1).catch(() => AddUserToQueue(userV1)); + response = { + status: SuccessStatus, + result: 'user queued for blocking', + } as SuccessResponse; + break; - // messages are ALWAYS expected to be: - // 1. objects - // 2. contain a string value stored under message.action. should be one defined above - // other message contents change based on the defined action - try { - switch (message?.action) { - case blockActionV1: - const blockV1Message = message as { user_id: string, name: string, screen_name: string, reason: string }; - const userV1: BlockUser = { user_id: blockV1Message.user_id, user: { name: blockV1Message.name, screen_name: blockV1Message.screen_name }, reason: ReasonExternal, external_reason: blockV1Message.reason }; - await AddUserToQueue(userV1).catch(() => AddUserToQueue(userV1)); - response = { status: SuccessStatus, result: "user queued for blocking" } as SuccessResponse; - break; - - case blockAction: - const blockMessage = message.data as BlockUser; - await AddUserToQueue(blockMessage).catch(() => AddUserToQueue(blockMessage)); - response = { status: SuccessStatus, result: "user queued for blocking" } as SuccessResponse; - break; - - default: - console.error(logstr, refid, "got a message that couldn't be handled from sender:", sender, message); - response = { status: ErrorStatus, message: "unknown action" } as ErrorResponse; + case blockAction: + const blockMessage = message.data as BlockUser; + await AddUserToQueue(blockMessage).catch(() => AddUserToQueue(blockMessage)); + response = { + status: SuccessStatus, + result: 'user queued for blocking', + } as SuccessResponse; + break; + + default: + console.error( + logstr, + refid, + "got a message that couldn't be handled from sender:", + sender, + message, + ); + response = { status: ErrorStatus, message: 'unknown action' } as ErrorResponse; + } + } catch (_e) { + const e = _e as Error; + console.error( + logstr, + refid, + 'unexpected error caught during', + message?.action, + 'action', + e, + ); + response = { + status: ErrorStatus, + message: e.message ?? 'unknown error', + } as ErrorResponse; } - } catch (_e) { - const e = _e as Error; - console.error(logstr, refid, "unexpected error caught during", message?.action, "action", e); - response = { status: ErrorStatus, message: e.message ?? "unknown error" } as ErrorResponse; - } - console.debug(logstr, refid, "ext respond:", response); -})(m, s).finally(() => r(response)); return true }); + console.debug(logstr, refid, 'ext respond:', response); + })(m, s).finally(() => r(response)); + return true; +}); diff --git a/src/constants.ts b/src/constants.ts index ce5c052..5b6155d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -6,11 +6,7 @@ let _api: { try { _api = { // @ts-ignore - runtime: { - ...browser.runtime, - OnInstalledReason: chrome.runtime.OnInstalledReason, - restartAfterDelay: chrome.runtime.restartAfterDelay, - }, + runtime: browser.runtime, storage: browser.storage, action: browser.browserAction, }; @@ -28,6 +24,7 @@ export const DefaultOptions: Config = { mute: false, blockFollowing: false, blockFollowers: false, + skipBlueCheckmark: false, skipVerified: true, skipAffiliated: true, skip1Mplus: true, @@ -36,6 +33,8 @@ export const DefaultOptions: Config = { skipFollowerCount: 1e6, soupcanIntegration: false, blockPromoted: false, + blockDisallowedWords: false, + disallowedWords: [], // this isn"t set, but is used // TODO: when migrating to firefox manifest v3, check to see if sets can be stored yet @@ -77,14 +76,20 @@ export const ReasonNftAvatar: number = 1; export const ReasonBusinessVerified: number = 2; export const ReasonTransphobia: number = 3; export const ReasonPromoted: number = 4; +export const ReasonDisallowedWordsOrEmojis: number = 5; +export const ReasonUsingBlueFeatures: number = 6; export const ReasonMap = { [ReasonBlueVerified]: 'Twitter Blue verified', [ReasonNftAvatar]: 'NFT avatar', [ReasonBusinessVerified]: 'Twitter Business verified', [ReasonTransphobia]: 'transphobia', [ReasonPromoted]: 'promoting tweets', + [ReasonDisallowedWordsOrEmojis]: 'disallowed words or emojis', + [ReasonUsingBlueFeatures]: 'using Twitter Blue features', }; +export const emojiRegExp = RegExp(/^[\p{Emoji_Presentation}\u200d]+$/, 'u'); + export const LegacyVerifiedUrl: string = 'https://gist.githubusercontent.com/travisbrown/b50d6745298cccd6b1f4697e4ec22103/raw/012009351630dc351e3a763b49bf24fa50ca3eb7/legacy-verified.csv'; export const Browser = @@ -95,11 +100,18 @@ export const SoupcanExtensionId = Browser === 'chrome' ? 'hcneafegcikghlbibfmlgadahjfckonj' : 'soupcan@beth.lgbt'; // internal message actions -export const [IsVerifiedAction, AddToHistoryAction, RemoveFromHistoryAction, AddToQueueAction, PopFromQueueAction] = [ +export const [ + IsVerifiedAction, + AddToHistoryAction, + RemoveFromHistoryAction, + AddToQueueAction, + PopFromQueueAction, +] = [ 'is_verified', 'add_user_to_history', 'remove_user_from_history', - 'add_user_to_queue', 'pop_user_from_queue' + 'add_user_to_queue', + 'pop_user_from_queue', ]; export const SuccessStatus: SuccessStatus = 'SUCCESS'; export const ErrorStatus: ErrorStatus = 'ERROR'; @@ -110,6 +122,6 @@ export const ErrorEvent = 'ErrorEvent'; export const MessageEvent = 'MessageEvent'; export const IntegrationStateDisabled = 0; -export const IntegrationStateReceiveOnly= 1; +export const IntegrationStateReceiveOnly = 1; export const IntegrationStateSendAndReceive = 2; export const IntegrationStateSendOnly = 3; diff --git a/src/content/index.ts b/src/content/index.ts index 5040b35..a127099 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -1,12 +1,49 @@ -import { SetHeaders } from "../shared"; -import { api, logstr, DefaultOptions, ErrorEvent, EventKey } from "../constants"; -import { HandleInstructionsResponse } from "../parsers/instructions"; -import { HandleForYou } from "../parsers/timeline"; -import { HandleTypeahead } from "../parsers/search"; -import { HandleUnblock } from "../parsers/unblock"; -import "./startup.ts"; +import { SetHeaders } from '../shared'; +import { api, logstr, DefaultOptions, emojiRegExp, ErrorEvent, EventKey } from '../constants'; +import { escapeRegExp } from '../utilities'; +import { HandleInstructionsResponse } from '../parsers/instructions'; +import { HandleForYou } from '../parsers/timeline'; +import { HandleTypeahead } from '../parsers/search'; +import { HandleUnblock } from '../parsers/unblock'; +import './startup.ts'; -document.addEventListener("blue-blocker-event", function (e: CustomEvent) { +function compileConfig(config: Config): CompiledConfig { + return { + suspendedBlockCollection: config.suspendedBlockCollection, + showBlockPopups: config.showBlockPopups, + toastsLocation: config.toastsLocation, + mute: config.mute, + blockFollowing: config.blockFollowing, + blockFollowers: config.blockFollowers, + skipBlueCheckmark: config.skipBlueCheckmark, + skipVerified: config.skipVerified, + skipAffiliated: config.skipAffiliated, + skip1Mplus: config.skip1Mplus, + blockInterval: config.blockInterval, + unblocked: config.unblocked, + popupTimer: config.popupTimer, + skipFollowerCount: config.skipFollowerCount, + soupcanIntegration: config.blockFollowers, + blockPromoted: config.blockPromoted, + blockForUse: config.blockForUse, + blockDisallowedWords: config.blockDisallowedWords, + disallowedWords: + config.disallowedWords.length === 0 + ? null + : new RegExp( + config.disallowedWords + .map(word => + word.match(emojiRegExp) + ? word + : `(?:^|\\s)${escapeRegExp(word)}(?:$|\\s)`, + ) + .join('|'), + 'i', + ), + } as CompiledConfig; +} + +document.addEventListener('blue-blocker-event', function (e: CustomEvent) { if (e.detail.status < 300) { SetHeaders(e.detail.request.headers); } else { @@ -21,32 +58,42 @@ document.addEventListener("blue-blocker-event", function (e: CustomEvent { const config = _config as Config; // @ts-ignore - toasts.classList = ""; + toasts.classList = ''; toasts.classList.add(config.toastsLocation); }); api.storage.sync.onChanged.addListener(items => { - if (items.hasOwnProperty("toastsLocation")) { + if (items.hasOwnProperty('toastsLocation')) { // @ts-ignore - toasts.classList = ""; + toasts.classList = ''; toasts.classList.add(items.toastsLocation.newValue); } }); diff --git a/src/global.d.ts b/src/global.d.ts index 720b40c..4d4d96a 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -7,6 +7,7 @@ interface Config { mute: boolean; blockFollowing: boolean; blockFollowers: boolean; + skipBlueCheckmark: boolean; skipVerified: boolean; skipAffiliated: boolean; skip1Mplus: boolean; @@ -17,10 +18,37 @@ interface Config { soupcanIntegration: boolean; blockPromoted: boolean; blockForUse: boolean; + blockDisallowedWords: boolean; + disallowedWords: string[]; +} + +interface CompiledConfig { + suspendedBlockCollection: boolean; + showBlockPopups: boolean; + toastsLocation: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + mute: boolean; + blockFollowing: boolean; + blockFollowers: boolean; + skipBlueCheckmark: boolean; + skipVerified: boolean; + skipAffiliated: boolean; + skip1Mplus: boolean; + blockInterval: number; + unblocked: { [k: string]: string? }; + popupTimer: number; + skipFollowerCount: number; + soupcanIntegration: boolean; + blockPromoted: boolean; + blockForUse: boolean; + blockDisallowedWords: boolean; + disallowedWords: RegExp | null; } interface BlueBlockerUser { + __typename: 'User'; is_blue_verified: boolean; + rest_id: string; + id: string; // TODO: verify affiliates_highlighted_label affiliates_highlighted_label?: { label?: { @@ -37,9 +65,43 @@ interface BlueBlockerUser { verified_type?: string; followers_count: number; muting?: boolean; + protected: boolean; + can_dm: boolean; + can_media_tag: boolean; + created_at: string; + default_profile: boolean; + default_profile_image: boolean; + description: string; + entities: { + description: { + urls: string[]; + }; + }; + fast_followers_count: number; + favourites_count: number; + friends_count: number; + has_custom_timelines: boolean; + is_translator: boolean; + listed_count: number; + location: string; + media_count: number; + normal_followers_count: number; + normal_followers_count: string[]; + possibly_sensitive: boolean; + profile_banner_url: string; + profile_banner_url_https: string; + profile_interstitial_type: string; + statuses_count: string; + translator_type: string; + want_retweets: boolean; + withheld_in_countries: string[]; }; - super_following: boolean; - rest_id: string; + tipjar_settings: { + /* TODO: figure out what gets put here */ + }; + super_following?: boolean; + has_graduated_access: boolean; + profile_image_shape: string; promoted_tweet?: boolean; used_blue?: boolean; } @@ -54,6 +116,11 @@ interface RuntimeMessage { data: any; } +interface RegisterRequest { + action: 'register'; + name: string; +} + interface MessageResponse { status: MessageStatus; } @@ -69,8 +136,8 @@ interface ErrorResponse { } interface ExternalBlockResponse { - block: boolean, - reason?: string, + block: boolean; + reason?: string; } interface BlueBlockerEvent { @@ -119,6 +186,6 @@ interface BlockedUser { } interface Integration { - name: string, - state: number, + name: string; + state: number; } diff --git a/src/injected/inject.ts b/src/injected/inject.ts index 7466bcc..d1b4d8b 100644 --- a/src/injected/inject.ts +++ b/src/injected/inject.ts @@ -2,7 +2,8 @@ (function (xhr) { // TODO: find a way to make this cleaner - const RequestRegex = /^https?:\/\/(?:\w+\.)?twitter.com\/[\w\/\.\-\_\=]+\/(HomeLatestTimeline|HomeTimeline|Followers|Following|SearchTimeline|UserTweets|UserCreatorSubscriptions|FollowersYouKnow|BlueVerifiedFollowers|SearchTimeline|timeline\/home\.json|TweetDetail|search\/typeahead\.json|search\/adaptive\.json|blocks\/destroy\.json|mutes\/users\/destroy\.json)(?:$|\?)/; + const RequestRegex = + /^https?:\/\/(?:\w+\.)?(?:twitter|x)\.com\/[\w\/\.\-\_\=]+\/(HomeLatestTimeline|HomeTimeline|Followers|Following|SearchTimeline|UserTweets|UserCreatorSubscriptions|FollowersYouKnow|BlueVerifiedFollowers|SearchTimeline|timeline\/home\.json|TweetDetail|search\/typeahead\.json|search\/adaptive\.json|blocks\/destroy\.json|mutes\/users\/destroy\.json)(?:$|\?)/; let XHR = XMLHttpRequest.prototype; let open = XHR.open; @@ -28,17 +29,19 @@ // determine if request is a timeline/tweet-returning request const parsedUrl = RequestRegex.exec(this._url); if (this._url && parsedUrl && parsedUrl.length > 0) { - document.dispatchEvent(new CustomEvent("blue-blocker-event", { - detail: { - parsedUrl, - url : this._url, - body: this.response, - request: { - headers: this._requestHeaders, + document.dispatchEvent( + new CustomEvent('blue-blocker-event', { + detail: { + parsedUrl, + url: this._url, + body: this.response, + request: { + headers: this._requestHeaders, + }, + status: this.status, }, - status: this.status, - }, - })); + }), + ); } }); // TODO: remove this ignore diff --git a/src/manifest.ts b/src/manifest.ts index 4111249..58ea039 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -1,36 +1,36 @@ -import { defineManifest } from "@crxjs/vite-plugin"; +import { defineManifest } from '@crxjs/vite-plugin'; export default defineManifest({ - name: "Blue Blocker", - description: "Blocks all Twitter Blue verified users on twitter.com", - version: "0.4.1", + name: 'Blue Blocker', + description: 'Blocks all Twitter Blue verified users on twitter.com', + version: '0.4.2', manifest_version: 3, icons: { - "128": "icon/icon-128.png", + '128': 'icon/icon-128.png', }, action: { - default_popup: "src/popup/index.html", - default_icon: "icon/icon.png", + default_popup: 'src/popup/index.html', + default_icon: 'icon/icon.png', }, background: { - service_worker: "src/background/index.ts", - type: "module", + service_worker: 'src/background/index.ts', + type: 'module', }, content_scripts: [ { - matches: ["*://*.twitter.com/*", "*://twitter.com/*"], - js: ["src/content/index.ts"], + matches: ['*://*.twitter.com/*', '*://twitter.com/*', '*://*.x.com/*', '*://x.com/*'], + js: ['src/content/index.ts'], }, ], - permissions: ["storage", "unlimitedStorage"], + permissions: ['storage', 'unlimitedStorage'], web_accessible_resources: [ { resources: [ // only files that are accessed from web pages need to be listed here. ie: injected files and assets - "src/injected/*", - "icon/*", + 'src/injected/*', + 'icon/*', ], - matches: ["*://*.twitter.com/*", "*://twitter.com/*"], + matches: ['*://*.twitter.com/*', '*://twitter.com/*', '*://*.x.com/*', '*://x.com/*'], }, ], }); diff --git a/src/models/block_counter.ts b/src/models/block_counter.ts index 4e171a5..0a58bfe 100644 --- a/src/models/block_counter.ts +++ b/src/models/block_counter.ts @@ -28,11 +28,12 @@ export class BlockCounter { await this.storage.set({ [criticalPointKey]: { refId, time: new Date().valueOf() + interval * 1.5 }, }); - await new Promise((r) => setTimeout(r, 10)); // wait a second to make sure any other sets have resolved - cpRefId = (await this.storage.get({ [criticalPointKey]: null }))[criticalPointKey]?.refId; + await new Promise(r => setTimeout(r, 10)); // wait a second to make sure any other sets have resolved + cpRefId = (await this.storage.get({ [criticalPointKey]: null }))[criticalPointKey] + ?.refId; } else { // sleep for a little bit to let the other tab(s) release the critical point - await new Promise((r) => setTimeout(r, sleep)); + await new Promise(r => setTimeout(r, sleep)); sleep = Math.min(sleep ** 2, interval); } } while (cpRefId !== refId); diff --git a/src/models/block_queue.ts b/src/models/block_queue.ts index 5adef37..9d56fb3 100644 --- a/src/models/block_queue.ts +++ b/src/models/block_queue.ts @@ -28,13 +28,14 @@ export class BlockQueue { await this.storage.set({ [criticalPointKey]: { refId, time: new Date().valueOf() + interval * 1.5 }, }); - await new Promise((r) => setTimeout(r, 10)); // wait a second to make sure any other sets have resolved - cpRefId = (await this.storage.get({ [criticalPointKey]: null }))[criticalPointKey]?.refId; + await new Promise(r => setTimeout(r, 10)); // wait a second to make sure any other sets have resolved + cpRefId = (await this.storage.get({ [criticalPointKey]: null }))[criticalPointKey] + ?.refId; } else { // rather than continually try to obtain the critical point, exit // if the consumer only queues the next run after successfully finishing the last, this can go back to only sleeping // console.debug(logstr, refId, "failed to obtain critical point, sleeping"); - await new Promise((r) => setTimeout(r, sleep)); + await new Promise(r => setTimeout(r, sleep)); sleep = Math.min(sleep ** 2, interval); } } while (cpRefId !== refId); @@ -51,9 +52,9 @@ export class BlockQueue { } async sync() { const refId = RefId(); - if (!await this.getCriticalPoint(refId)) { + if (!(await this.getCriticalPoint(refId))) { // we failed to obtain the critical point, so we can't continue - throw new Error("failed to obtain critical point"); + throw new Error('failed to obtain critical point'); } // sync simply adds the in-memory queue to the stored queue const oldQueue = (await this.storage.get({ BlockQueue: [] })).BlockQueue; @@ -76,9 +77,9 @@ export class BlockQueue { } async shift() { const refId = RefId(); - if (!await this.getCriticalPoint(refId)) { + if (!(await this.getCriticalPoint(refId))) { // we failed to obtain the critical point, so we can't continue - throw new Error("failed to obtain critical point"); + throw new Error('failed to obtain critical point'); } const items = await this.storage.get({ BlockQueue: [] }); const item = items.BlockQueue.shift(); @@ -90,9 +91,9 @@ export class BlockQueue { } async clear() { const refId = RefId(); - if (!await this.getCriticalPoint(refId)) { + if (!(await this.getCriticalPoint(refId))) { // we failed to obtain the critical point, so we can't continue - throw new Error("failed to obtain critical point"); + throw new Error('failed to obtain critical point'); } const items = await this.storage.get({ BlockQueue: [] }); if (!items.BlockQueue || items.BlockQueue.length === 0) { diff --git a/src/models/queue_consumer.ts b/src/models/queue_consumer.ts index e825a91..81985d8 100644 --- a/src/models/queue_consumer.ts +++ b/src/models/queue_consumer.ts @@ -7,7 +7,9 @@ const criticalPointKey = 'QueueConsumerCriticalPoint'; export class QueueConsumer { storage: typeof chrome.storage.local | typeof browser.storage.local; func: () => Promise; - interval: (storage: typeof chrome.storage.local | typeof browser.storage.local) => Promise; + interval: ( + storage: typeof chrome.storage.local | typeof browser.storage.local, + ) => Promise; private _timeout: number | null; private _interval: number; private _func_timeout: number | null; @@ -20,7 +22,9 @@ export class QueueConsumer { constructor( storage: typeof chrome.storage.local | typeof browser.storage.local, func: () => Promise, - interval_func: (storage: typeof chrome.storage.local | typeof browser.storage.local) => Promise, + interval_func: ( + storage: typeof chrome.storage.local | typeof browser.storage.local, + ) => Promise, ) { // idk this.storage = storage; @@ -29,7 +33,7 @@ export class QueueConsumer { this._timeout = null; this._interval = 100; this._func_timeout = null; - this._refId = RefId(); // consumer is assigned to a tab, so keep it in the class + this._refId = RefId(); // consumer is assigned to a tab, so keep it in the class } async getCriticalPoint(): Promise { // console.debug(logstr, this._refId, "attempting to obtain critical point"); @@ -50,8 +54,9 @@ export class QueueConsumer { time: new Date().valueOf() + (this._interval || 0) * 1.5, }, }); - await new Promise((r) => setTimeout(r, 10)); // wait a second to make sure any other sets have resolved - cpRefId = (await this.storage.get({ [criticalPointKey]: null }))[criticalPointKey]?.refId; + await new Promise(r => setTimeout(r, 10)); // wait a second to make sure any other sets have resolved + cpRefId = (await this.storage.get({ [criticalPointKey]: null }))[criticalPointKey] + ?.refId; } else { // console.debug(logstr, this._refId, "failed to obtain critical point"); return false; @@ -69,7 +74,7 @@ export class QueueConsumer { } } entropy(): number { - const variance = this._interval * 0.1; // 0.1 = 10% entropy + const variance = this._interval * 0.1; // 0.1 = 10% entropy return Math.random() * variance * 2 - variance; } async sync() { @@ -79,9 +84,18 @@ export class QueueConsumer { // if we just got it, func will be null and we can schedule it // if we already had it, it already finished or its waiting on queue if (this._func_timeout === null) { - this._func_timeout = setTimeout(() => this.func().then(() => { this._func_timeout = null; this.sync(); }).catch(() => this.stop()), this._interval + this.entropy()); + this._func_timeout = setTimeout( + () => + this.func() + .then(() => { + this._func_timeout = null; + this.sync(); + }) + .catch(() => this.stop()), + this._interval + this.entropy(), + ); } - this._timeout = null; // set timeout to null, just for the running check in start + this._timeout = null; // set timeout to null, just for the running check in start } else { // we couldn't get the critical point, so cancel func if its scheduled, then set timeout to check again if (this._func_timeout) { @@ -97,7 +111,7 @@ export class QueueConsumer { // we're already running return; } - console.debug(logstr, "queue consumer started"); + console.debug(logstr, 'queue consumer started'); this.sync(); } stop() { @@ -110,6 +124,6 @@ export class QueueConsumer { this._func_timeout = null; } this.releaseCriticalPoint(); - console.debug(logstr, "queue consumer stopped"); + console.debug(logstr, 'queue consumer stopped'); } } diff --git a/src/pages/history/index.html b/src/pages/history/index.html index 22c3a74..92e8d54 100644 --- a/src/pages/history/index.html +++ b/src/pages/history/index.html @@ -1,10 +1,10 @@ - + Blue Blocker History - - + + @@ -12,12 +12,16 @@

Block History

Users may be blocked while this page is open, refresh to update

-

blocked users so far!

+

+ blocked users so far! +

-

it looks like the block counter may have gotten desynced.to reset the counter to (currently )

-
- loading... -
+

+ it looks like the block counter may have gotten desynced.to reset the counter to (currently + ) +

+
loading...
diff --git a/src/pages/history/index.ts b/src/pages/history/index.ts index 2315a0d..84c3520 100644 --- a/src/pages/history/index.ts +++ b/src/pages/history/index.ts @@ -1,135 +1,157 @@ -import { commafy, EscapeHtml, RefId } from "../../utilities.js"; -import { api, logstr, HistoryStateBlocked, ReasonMap, ReasonExternal, HistoryStateUnblocked, HistoryStateGone } from "../../constants.js"; -import { ConnectDb, historyDbStore } from "../../background/db.js"; -import { BlockCounter } from "../../models/block_counter"; -import "../style.css"; -import "./style.css"; +import { commafy, EscapeHtml, RefId } from '../../utilities.js'; +import { + api, + logstr, + HistoryStateBlocked, + ReasonMap, + ReasonExternal, + HistoryStateUnblocked, + HistoryStateGone, +} from '../../constants.js'; +import { ConnectDb, historyDbStore } from '../../background/db.js'; +import { BlockCounter } from '../../models/block_counter'; +import '../style.css'; +import './style.css'; const blockCounter = new BlockCounter(api.storage.local); const refid = RefId(); // grab block counter critical point to compare counters safely -blockCounter.getCriticalPoint(refid) -.then(ConnectDb) -.then(db => { - return new Promise((resolve, reject) => { - const transaction = db.transaction([historyDbStore], "readonly"); - transaction.onabort = transaction.onerror = reject; - const store = transaction.objectStore(historyDbStore); - const index = store.index("time"); - const req = index.getAll(); - - req.onerror = reject; - req.onsuccess = () => { - const users = req.result as BlockedUser[]; - resolve(users); - }; - }).then(users => { - const queueDiv = document.getElementById("block-history") as HTMLElement; - - queueDiv.innerHTML = ""; - let blockedCount: number = 0; - - const reasons: { [r: number]: number } = { }; - users.reverse().forEach(item => { - if (!reasons.hasOwnProperty(item.reason)) { - reasons[item.reason] = 0; - } - - const div = document.createElement("div"); - const p = document.createElement("p"); - const screen_name = EscapeHtml(item.user.screen_name); - p.innerHTML = `${EscapeHtml(item.user.name)} (@${screen_name})`; - div.appendChild(p); - - const p2 = document.createElement("p"); - const reason = item?.external_reason ?? ReasonMap[item.reason]; - p2.innerText = "reason: " + reason; - div.appendChild(p2); - - const p3 = document.createElement("p"); - let state: string; - switch (item.state) { - case HistoryStateBlocked: - state = "blocked"; - blockedCount++; - reasons[item.reason]++; - break; - - case HistoryStateUnblocked: - state = "unblocked"; - blockedCount++; - reasons[item.reason]++; - break; - - case HistoryStateGone: - state = "user no longer exists"; - break; - - default: - state = "unreadable state"; - } - p3.innerText = "current state: " + state; - div.appendChild(p3); - - const p4 = document.createElement("p"); - const time = new Date(item.time); - const datetime = time.toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' }) - .replace(`, ${new Date().getFullYear()}`, '') - + ', ' - + time.toLocaleTimeString() - .toLowerCase(); - p4.innerText = state + " on " + datetime; - div.appendChild(p4); - - queueDiv.appendChild(div); - }); - - document.getElementsByName("blocked-users-count").forEach(e => - e.innerText = commafy(blockedCount) - ); +blockCounter + .getCriticalPoint(refid) + .then(ConnectDb) + .then(db => { + return new Promise((resolve, reject) => { + const transaction = db.transaction([historyDbStore], 'readonly'); + transaction.onabort = transaction.onerror = reject; + const store = transaction.objectStore(historyDbStore); + const index = store.index('time'); + const req = index.getAll(); + + req.onerror = reject; + req.onsuccess = () => { + const users = req.result as BlockedUser[]; + resolve(users); + }; + }).then(users => { + const queueDiv = document.getElementById('block-history') as HTMLElement; + + queueDiv.innerHTML = ''; + let blockedCount: number = 0; + + const reasons: { [r: number]: number } = {}; + users.reverse().forEach(item => { + if (!reasons.hasOwnProperty(item.reason)) { + reasons[item.reason] = 0; + } + + const div = document.createElement('div'); + const p = document.createElement('p'); + const screen_name = EscapeHtml(item.user.screen_name); + p.innerHTML = `${EscapeHtml( + item.user.name, + )} (@${screen_name})`; + div.appendChild(p); + + const p2 = document.createElement('p'); + const reason = item?.external_reason ?? ReasonMap[item.reason]; + p2.innerText = 'reason: ' + reason; + div.appendChild(p2); + + const p3 = document.createElement('p'); + let state: string; + switch (item.state) { + case HistoryStateBlocked: + state = 'blocked'; + blockedCount++; + reasons[item.reason]++; + break; + + case HistoryStateUnblocked: + state = 'unblocked'; + blockedCount++; + reasons[item.reason]++; + break; + + case HistoryStateGone: + state = 'user no longer exists'; + break; + + default: + state = 'unreadable state'; + } + p3.innerText = 'current state: ' + state; + div.appendChild(p3); + + const p4 = document.createElement('p'); + const time = new Date(item.time); + const datetime = + time + .toLocaleDateString('en', { + year: 'numeric', + month: 'short', + day: 'numeric', + }) + .replace(`, ${new Date().getFullYear()}`, '') + + ', ' + + time.toLocaleTimeString().toLowerCase(); + p4.innerText = state + ' on ' + datetime; + div.appendChild(p4); + + queueDiv.appendChild(div); + }); - blockCounter.getCriticalPoint(refid) - .then(() => blockCounter.storage.get({ BlockCounter: 0 })) - .then(items => items.BlockCounter as number) - .then(count => { - if (blockedCount === count) { + document + .getElementsByName('blocked-users-count') + .forEach(e => (e.innerText = commafy(blockedCount))); + + blockCounter + .getCriticalPoint(refid) + .then(() => blockCounter.storage.get({ BlockCounter: 0 })) + .then(items => items.BlockCounter as number) + .then(count => { + if (blockedCount === count) { + return; + } + + const blockCounterCurrentValue = document.getElementById( + 'block-counter-current-value', + ) as HTMLElement; + blockCounterCurrentValue.innerText = commafy(count); + + const resetCounter = document.getElementById('reset-counter') as HTMLElement; + const a = resetCounter.firstElementChild as HTMLElement; + a.addEventListener('click', () => { + const refid = RefId(); + blockCounter + .getCriticalPoint(refid) + .then(() => blockCounter.storage.set({ BlockCounter: blockedCount })) + .then(() => { + console.log(logstr, 'reset block counter to', blockedCount); + resetCounter.style.display = ''; + }) + .finally(() => blockCounter.releaseCriticalPoint(refid)); + }); + + resetCounter.style.display = 'block'; + }) + .finally(() => blockCounter.releaseCriticalPoint(refid)); + + if (users.length === 0) { + queueDiv.textContent = 'your block history is empty'; return; } - const blockCounterCurrentValue = document.getElementById("block-counter-current-value") as HTMLElement; - blockCounterCurrentValue.innerText = commafy(count); - - const resetCounter = document.getElementById("reset-counter") as HTMLElement; - const a = resetCounter.firstElementChild as HTMLElement; - a.addEventListener("click", () => { - const refid = RefId(); - blockCounter.getCriticalPoint(refid) - .then(() => - blockCounter.storage.set({ BlockCounter: blockedCount }) - ).then(() => { - console.log(logstr, "reset block counter to", blockedCount); - resetCounter.style.display = ""; - }).finally(() => - blockCounter.releaseCriticalPoint(refid) - ); - }); - - resetCounter.style.display = "block"; - }).finally(() => - blockCounter.releaseCriticalPoint(refid) - ); - - if (users.length === 0) { - queueDiv.textContent = "your block history is empty"; - return; - } - - const detailedCounts = document.getElementById("detailed-counts") as HTMLElement; - const reasonMap = { - [ReasonExternal]: "external extension", - ...ReasonMap, - }; - detailedCounts.innerText = "(" + Object.entries(reasons).map(item => reasonMap[parseInt(item[0])] + ": " + commafy(item[1])).join(", ") + ")"; + const detailedCounts = document.getElementById('detailed-counts') as HTMLElement; + const reasonMap = { + [ReasonExternal]: 'external extension', + ...ReasonMap, + }; + detailedCounts.innerText = + '(' + + Object.entries(reasons) + .map(item => reasonMap[parseInt(item[0])] + ': ' + commafy(item[1])) + .join(', ') + + ')'; + }); }); -}); diff --git a/src/pages/integrations/index.html b/src/pages/integrations/index.html index aa15c9d..b23d3c9 100644 --- a/src/pages/integrations/index.html +++ b/src/pages/integrations/index.html @@ -1,10 +1,10 @@ - + Manage Third-Party Integrations - - + + @@ -20,12 +20,12 @@

Manage Third-Party Integrations

- - + + -
+
diff --git a/src/pages/integrations/index.ts b/src/pages/integrations/index.ts index 9dfb690..cd911a0 100644 --- a/src/pages/integrations/index.ts +++ b/src/pages/integrations/index.ts @@ -1,19 +1,27 @@ -import { RefId } from "../../utilities.js"; -import { api, logstr, IntegrationStateDisabled, IntegrationStateReceiveOnly, IntegrationStateSendAndReceive, IntegrationStateSendOnly, SoupcanExtensionId } from "../../constants.js"; -import "../style.css"; -import "./style.css"; +import { RefId } from '../../utilities.js'; +import { + api, + logstr, + IntegrationStateDisabled, + IntegrationStateReceiveOnly, + IntegrationStateSendAndReceive, + IntegrationStateSendOnly, + SoupcanExtensionId, +} from '../../constants.js'; +import '../style.css'; +import './style.css'; interface Integration { - id: string, - name: string, - state: number, + id: string; + name: string; + state: number; } const [ExtensionStateNone, ExtensionStateDisabled, ExtensionStateEnabled] = [0, 1, 2]; -document.addEventListener("DOMContentLoaded", () => { - const integrationsDiv = document.getElementById("integrations") as HTMLElement; - const i: { [n: string]: Integration } = { }; +document.addEventListener('DOMContentLoaded', () => { + const integrationsDiv = document.getElementById('integrations') as HTMLElement; + const i: { [n: string]: Integration } = {}; function add(integration: Integration): void { const refid = RefId().toString(); @@ -24,146 +32,162 @@ document.addEventListener("DOMContentLoaded", () => { state: integration.state, }; - const div = document.createElement("div"); - div.id = integration.id || "placeholder"; + const div = document.createElement('div'); + div.id = integration.id || 'placeholder'; - const select = document.createElement("select"); - select.addEventListener("change", e => { + const logupdate = () => console.debug(logstr, 'updated integration', refid, i[refid]); + + const select = document.createElement('select'); + select.addEventListener('change', e => { const input = e.target as HTMLSelectElement; i[refid].state = parseInt(input.value); + logupdate(); }); - const optionDisabled = document.createElement("option"); + const optionDisabled = document.createElement('option'); optionDisabled.value = IntegrationStateDisabled.toString(); - optionDisabled.innerText = "disabled"; + optionDisabled.innerText = 'disabled'; select.appendChild(optionDisabled); - const optionRecvOnly = document.createElement("option"); + const optionRecvOnly = document.createElement('option'); optionRecvOnly.value = IntegrationStateReceiveOnly.toString(); - optionRecvOnly.innerText = "receive only"; + optionRecvOnly.innerText = 'receive only'; select.appendChild(optionRecvOnly); - const optionSendOnly = document.createElement("option"); + const optionSendOnly = document.createElement('option'); optionSendOnly.value = IntegrationStateSendOnly.toString(); - optionSendOnly.innerText = "send only"; + optionSendOnly.innerText = 'send only'; select.appendChild(optionSendOnly); - const optionSendRecv = document.createElement("option"); + const optionSendRecv = document.createElement('option'); optionSendRecv.value = IntegrationStateSendAndReceive.toString(); - optionSendRecv.innerText = "send and receive"; + optionSendRecv.innerText = 'send and receive'; select.appendChild(optionSendRecv); select.value = integration.state.toString(); div.appendChild(select); - const extId = document.createElement("input"); + const extId = document.createElement('input'); extId.value = integration.id; - extId.placeholder = "external extension id"; - extId.autocomplete = "off"; - extId.addEventListener("input", e => { + extId.placeholder = 'external extension id'; + extId.autocomplete = 'off'; + extId.addEventListener('input', e => { const input = e.target as HTMLInputElement; i[refid].id = input.value; div.id = input.value; + logupdate(); }); div.appendChild(extId); - const extName = document.createElement("input"); + const extName = document.createElement('input'); extName.value = integration.name; - extName.placeholder = "extension name"; - extName.autocomplete = "off"; - extName.addEventListener("input", e => { + extName.placeholder = 'extension name'; + extName.autocomplete = 'off'; + extName.addEventListener('input', e => { const input = e.target as HTMLInputElement; i[refid].name = input.value; + logupdate(); }); div.appendChild(extName); - const remove = document.createElement("button"); - remove.innerText = "Remove"; - remove.addEventListener("click", e => { + const remove = document.createElement('button'); + remove.innerText = 'Remove'; + remove.addEventListener('click', e => { + const deleted = i[refid]; delete i[refid]; integrationsDiv.removeChild(div); + console.debug(logstr, 'deleted integration', refid, deleted, i); }); div.appendChild(remove); integrationsDiv.appendChild(div); } - integrationsDiv.innerHTML = ""; + integrationsDiv.innerHTML = ''; let soupcanState: number = ExtensionStateNone; - document.addEventListener("soupcan-event", () => { - if (soupcanState === ExtensionStateEnabled) { - // we don't need a placeholder if we're going to put soupcan in, so remove it - const placeholder = document.getElementById("placeholder"); - if (placeholder) { - integrationsDiv.removeChild(placeholder); - } - add({ - id: SoupcanExtensionId, - name: "soupcan", - state: IntegrationStateDisabled, - }); - } - }); - - api.storage.local.get({ integrations: { } }) - .then(items => items.integrations as { [id: string]: { name: string, state: number } }) - .then(integrations => { - console.debug(logstr, "loaded integrations:", integrations); - const addButton = document.getElementById("add-button") as HTMLButtonElement; - const saveButton = document.getElementById("save-button") as HTMLButtonElement; - const saveStatus = document.getElementById("save-status") as HTMLButtonElement; - - // it's important that this runs *after* getting local storage back - api.runtime.sendMessage( - SoupcanExtensionId, - { action: "check_twitter_user", screen_name: "elonmusk" }, - ).then((r: any) => { - // we could check if response is the expected shape here, if we really wanted - if (!r) { - soupcanState = ExtensionStateDisabled; - throw new Error("extension not enabled"); - } - soupcanState = ExtensionStateEnabled; - }).catch(e => - console.debug(logstr, "soupcan error:", e, soupcanState) - ).finally(() => - // @ts-ignore - document.dispatchEvent(new CustomEvent("soupcan-event")) - ); - - addButton.addEventListener("click", e => add({ id: "", name: "", state: IntegrationStateDisabled })); - let saveTimeout: number | null = null; - saveButton.addEventListener("click", e => { - console.debug(logstr, "saving integrations:", i); - if (saveTimeout !== null) { - clearTimeout(saveTimeout); + // soupcan doesn't work with the new integration system for now + // document.addEventListener('soupcan-event', () => { + // if (soupcanState === ExtensionStateEnabled) { + // // we don't need a placeholder if we're going to put soupcan in, so remove it + // const placeholder = document.getElementById('placeholder'); + // if (placeholder) { + // // the only button in here should be the remove button + // placeholder.getElementsByTagName('button')[0].click(); + // } + // add({ + // id: SoupcanExtensionId, + // name: 'soupcan', + // state: IntegrationStateDisabled, + // }); + // } + // }); + + api.storage.local + .get({ integrations: {} }) + .then(items => items.integrations as { [id: string]: { name: string; state: number } }) + .then(integrations => { + console.debug(logstr, 'loaded integrations:', integrations); + const addButton = document.getElementById('add-button') as HTMLButtonElement; + const saveButton = document.getElementById('save-button') as HTMLButtonElement; + const saveStatus = document.getElementById('save-status') as HTMLButtonElement; + + // it's important that this runs *after* getting local storage back + if (!integrations.hasOwnProperty(SoupcanExtensionId)) { + api.runtime + .sendMessage(SoupcanExtensionId, { + action: 'check_twitter_user', + screen_name: 'elonmusk', + }) + .then((r: any) => { + // we could check if response is the expected shape here, if we really wanted + if (!r) { + soupcanState = ExtensionStateDisabled; + throw new Error('extension not enabled'); + } + soupcanState = ExtensionStateEnabled; + }) + .catch(e => console.debug(logstr, 'soupcan error:', e, soupcanState)) + .finally(() => + // @ts-ignore + document.dispatchEvent(new CustomEvent('soupcan-event')), + ); } - const integrations: { [id: string]: { name: string, state: number } } = { }; - for (const integration of Object.values(i)) { - integrations[integration.id] = { + addButton.addEventListener('click', e => + add({ id: '', name: '', state: IntegrationStateDisabled }), + ); + let saveTimeout: number | null = null; + saveButton.addEventListener('click', e => { + console.debug(logstr, 'saving integrations:', i); + if (saveTimeout !== null) { + clearTimeout(saveTimeout); + } + + const integrations: { [id: string]: { name: string; state: number } } = {}; + for (const integration of Object.values(i)) { + integrations[integration.id] = { + name: integration.name, + state: integration.state, + }; + } + api.storage.local.set({ integrations }).then(() => { + console.debug(logstr, 'saved integrations:', integrations); + saveStatus.innerText = 'saved!'; + saveTimeout = setTimeout(() => (saveStatus.innerText = ''), 1000); + }); + }); + + for (const [extensionId, integration] of Object.entries(integrations)) { + add({ + id: extensionId, name: integration.name, state: integration.state, - }; + }); } - api.storage.local.set({ integrations }).then(() => { - console.debug(logstr, "saved integrations:", integrations); - saveStatus.innerText = "saved!"; - saveTimeout = setTimeout(() => saveStatus.innerText = "", 1000); - }); - }); - - for (const [extensionId, integration] of Object.entries(integrations)) { - add({ - id: extensionId, - name: integration.name, - state: integration.state, - }); - } - if (Object.entries(i).length === 0) { - add({ id: "", name: "", state: IntegrationStateDisabled }); - } - }); + if (Object.entries(i).length === 0) { + add({ id: '', name: '', state: IntegrationStateDisabled }); + } + }); }); diff --git a/src/pages/integrations/style.css b/src/pages/integrations/style.css index efa20d1..9cb1fa8 100644 --- a/src/pages/integrations/style.css +++ b/src/pages/integrations/style.css @@ -1,4 +1,5 @@ -input, select { +input, +select { cursor: pointer; margin: 0; padding: 0.5em 1em; @@ -17,12 +18,15 @@ input, select { -o-transition: var(--transition) var(--fadetime); transition: var(--transition) var(--fadetime); } -input:hover, select:hover, select:active { +input:hover, +select:hover, +select:active { color: var(--interact); border-color: var(--borderhover); box-shadow: 0 0 10px 3px var(--activeshadowcolor); } -input:hover, input:focus { +input:hover, +input:focus { border-color: var(--borderhover); } @@ -32,7 +36,8 @@ input:hover, input:focus { margin: 0 auto; margin-bottom: 1.5em; } -#integrations input, #integrations select { +#integrations input, +#integrations select { margin-right: 1.5em; } #integrations input { @@ -55,7 +60,8 @@ input:hover, input:focus { } @media only screen and (max-width: 1200px) { - #integrations > div, .buttons { + #integrations > div, + .buttons { width: auto; } } diff --git a/src/pages/queue/index.html b/src/pages/queue/index.html index 90db909..d046ff1 100644 --- a/src/pages/queue/index.html +++ b/src/pages/queue/index.html @@ -1,21 +1,32 @@ - + Blue Blocker Queue - - + +

Block Queue

-

Users may be blocked while this page is open, refresh to update.   import

-

holy shit your queue is huge, results have been limited to the first 10,000

+

+ Users may be blocked while this page is open, refresh to update.   import +

+

+ holy shit your queue is huge, results have been limited to the first 10,000 +

-

Blocklist import currently only support json files. files must be arrays containing the following type:

-
interface BlockUser {
+				

+ Blocklist import currently only support json files. files must be arrays + containing the following type: +

+
+interface BlockUser {
 	user_id: string,
 	user: {
 		name: string,
@@ -23,21 +34,30 @@ 

Block Queue

}, reason: number, external_reason?: string, -}
+}

where reason is an int that refers to one of the following values:

-
const ReasonExternal: number         = -1; // use external_reason
-const ReasonBlueVerified: number     = 0;  // twitter blue user
-const ReasonNftAvatar: number        = 1;  // user has NFT avatar
-const ReasonBusinessVerified: number = 2;  // verified via a business
-const ReasonTransphobia: number      = 3;  // user is known transphobe
-const ReasonPromoted: number         = 4;  // promoted tweets
-

reason can also be assigned -1 and provided an external_reason.
external reason should be omitted if reason is not -1.

- - -
-
- loading... +
+const ReasonExternal: number                = -1; // use external_reason
+const ReasonBlueVerified: number            = 0;  // twitter blue user
+const ReasonNftAvatar: number               = 1;  // user has NFT avatar
+const ReasonBusinessVerified: number        = 2;  // verified via a business
+const ReasonTransphobia: number             = 3;  // user is known transphobe
+const ReasonPromoted: number                = 4;  // promoted tweets
+const ReasonDisallowedWordsOrEmojis: number = 5;  // disallowed words or emojis
+const ReasonUsingBlueFeatures: number       = 6;  // using Twitter Blue features
+

+ reason can also be assigned -1 and provided an + external_reason.
external reason should be omitted if reason + is not -1. +

+ +
+
loading...
diff --git a/src/pages/queue/index.ts b/src/pages/queue/index.ts index f5746a2..b813086 100644 --- a/src/pages/queue/index.ts +++ b/src/pages/queue/index.ts @@ -1,57 +1,61 @@ -import { commafy, EscapeHtml, FormatLegacyName } from "../../utilities.js"; -import { api, logstr } from "../../constants.js"; -import { AddUserToQueue, ConnectDb, queueDbStore, WholeQueue } from "../../background/db.js"; -import "../style.css"; -import "./style.css"; +import { commafy, EscapeHtml, FormatLegacyName } from '../../utilities.js'; +import { api, logstr } from '../../constants.js'; +import { AddUserToQueue, ConnectDb, queueDbStore, WholeQueue } from '../../background/db.js'; +import '../style.css'; +import './style.css'; async function unqueueUser(user_id: string, screen_name: string, safelist: boolean) { // because this page holds onto the critical point, we can modify the queue // without worrying about if it'll affect another tab if (safelist) { - api.storage.sync.get({ unblocked: { } }).then(items => { + api.storage.sync.get({ unblocked: {} }).then(items => { items.unblocked[String(user_id)] = screen_name; api.storage.sync.set(items); }); } - ConnectDb().then(db => { - return new Promise((resolve, reject) => { - const transaction = db.transaction([queueDbStore], "readwrite"); - transaction.onabort = transaction.onerror = reject; - const store = transaction.objectStore(queueDbStore); - store.delete(user_id); - transaction.commit(); - transaction.oncomplete = () => resolve(); + ConnectDb() + .then(db => { + return new Promise((resolve, reject) => { + const transaction = db.transaction([queueDbStore], 'readwrite'); + transaction.onabort = transaction.onerror = reject; + const store = transaction.objectStore(queueDbStore); + store.delete(user_id); + transaction.commit(); + transaction.oncomplete = () => resolve(); + }); + }) + .catch(e => { + console.error(logstr, 'could not remove user from queue:', e); }); - }).catch(e => { - console.error(logstr, "could not remove user from queue:", e); - }); } function loadQueue() { WholeQueue().then(cue => { - const queueDiv = document.getElementById("block-queue") as HTMLElement; + const queueDiv = document.getElementById('block-queue') as HTMLElement; if (cue.length === 0) { - queueDiv.textContent = "your block queue is empty"; + queueDiv.textContent = 'your block queue is empty'; return; } else if (cue.length >= 10e3) { - const dbLimitReached = document.getElementById("db-limit-reached") as HTMLElement; - dbLimitReached.style.display = "block"; + const dbLimitReached = document.getElementById('db-limit-reached') as HTMLElement; + dbLimitReached.style.display = 'block'; } - queueDiv.innerHTML = ""; + queueDiv.innerHTML = ''; cue.forEach(item => { const { user, user_id } = item; - const div = document.createElement("div"); + const div = document.createElement('div'); - const p = document.createElement("p"); - const screen_name = EscapeHtml(user.screen_name); // this shouldn't really do anything, but can't be too careful - p.innerHTML = `${EscapeHtml(user.name)} (@${screen_name})`; + const p = document.createElement('p'); + const screen_name = EscapeHtml(user.screen_name); // this shouldn't really do anything, but can't be too careful + p.innerHTML = `${EscapeHtml( + user.name, + )} (@${screen_name})`; div.appendChild(p); - const remove = document.createElement("button"); + const remove = document.createElement('button'); remove.onclick = () => { div.removeChild(remove); unqueueUser(user_id, user.screen_name, false).then(() => { @@ -59,18 +63,21 @@ function loadQueue() { queueDiv.removeChild(div); }); }; - remove.textContent = "remove"; + remove.textContent = 'remove'; div.appendChild(remove); - const never = document.createElement("button"); + const never = document.createElement('button'); never.onclick = () => { div.removeChild(never); unqueueUser(user_id, user.screen_name, true).then(() => { - console.log(logstr, `removed and safelisted ${FormatLegacyName(user)} from queue`); + console.log( + logstr, + `removed and safelisted ${FormatLegacyName(user)} from queue`, + ); queueDiv.removeChild(div); }); }; - never.textContent = "never block"; + never.textContent = 'never block'; div.appendChild(never); queueDiv.appendChild(div); @@ -79,29 +86,29 @@ function loadQueue() { } loadQueue(); -document.addEventListener("DOMContentLoaded", () => { - const importButton = document.getElementById("import-button") as HTMLElement; - const importArrow = document.getElementById("import-arrow") as HTMLElement; - const importBlock = document.getElementById("importer") as HTMLElement; +document.addEventListener('DOMContentLoaded', () => { + const importButton = document.getElementById('import-button') as HTMLElement; + const importArrow = document.getElementById('import-arrow') as HTMLElement; + const importBlock = document.getElementById('importer') as HTMLElement; - importButton.addEventListener("click", e => { + importButton.addEventListener('click', e => { switch (importArrow.innerText) { - case "▾": - importBlock.style.display = "block"; - importArrow.innerText = "▴"; + case '▾': + importBlock.style.display = 'block'; + importArrow.innerText = '▴'; break; - case "▴": - importBlock.style.display = ""; - importArrow.innerText = "▾"; + case '▴': + importBlock.style.display = ''; + importArrow.innerText = '▾'; break; default: - // what? + // what? } }); - const defaultInputText = "Click or Drag to Import File"; - const input = document.getElementById("block-import") as HTMLInputElement; - const importLabel = document.getElementById("block-import-label") as HTMLElement; + const defaultInputText = 'Click or Drag to Import File'; + const input = document.getElementById('block-import') as HTMLInputElement; + const importLabel = document.getElementById('block-import-label') as HTMLElement; const inputStatus = importLabel.firstElementChild as HTMLElement; inputStatus.innerText = defaultInputText; let timeout: number | null = null; @@ -119,69 +126,81 @@ document.addEventListener("DOMContentLoaded", () => { let loaded: number = 0; let failures: number = 0; let safelisted: number = 0; - reader.addEventListener("load", l => { - inputStatus.innerText = "importing..."; + reader.addEventListener('load', l => { + inputStatus.innerText = 'importing...'; // @ts-ignore const payload = l.target.result as string; - api.storage.sync.get({ unblocked: { }}) - .then(items => items.unblocked as { [k: string]: string | null }) - .then(safelist => { - return new Promise(async (resolve) => { - const userList = JSON.parse(payload) as BlockUser[]; - for (const user of userList) { - try { - // explicitly check to make sure all fields are populated - if ( - user?.user_id === undefined || - user?.user?.name === undefined || - user?.user?.screen_name === undefined || - user?.reason === undefined - ) { - throw new Error("user object could not be processed:"); - } - - if (safelist.hasOwnProperty(user.user_id)) { - safelisted++; - continue; + api.storage.sync + .get({ unblocked: {} }) + .then(items => items.unblocked as { [k: string]: string | null }) + .then(safelist => { + return new Promise(async resolve => { + const userList = JSON.parse(payload) as BlockUser[]; + for (const user of userList) { + try { + // explicitly check to make sure all fields are populated + if ( + user?.user_id === undefined || + user?.user?.name === undefined || + user?.user?.screen_name === undefined || + user?.reason === undefined + ) { + throw new Error('user object could not be processed:'); + } + + if (safelist.hasOwnProperty(user.user_id)) { + safelisted++; + continue; + } + + await AddUserToQueue(user); + loaded++; + } catch (_e) { + const e = _e as Error; + console.error(logstr, e.message, user, e); + failures++; + return; } - - await AddUserToQueue(user); - loaded++; - } catch (_e) { - const e = _e as Error; - console.error(logstr, e.message, user, e); - failures++; - return; } - } - resolve(); - }).then(() => { - console.log(logstr, "successfully loaded", loaded, "users into queue. failures:", failures); - inputStatus.innerText = `loaded ${commafy(loaded)} users into queue (${commafy(failures)} failures)`; - loadQueue(); - }).catch(e => { - console.error(logstr, e); - inputStatus.innerText = e.message; - }).finally(() => { - timeout = setTimeout(() => { - inputStatus.innerText = "Click or Drag to Import File"; - timeout = null; - }, 10e3); + resolve(); + }) + .then(() => { + console.log( + logstr, + 'successfully loaded', + loaded, + 'users into queue. failures:', + failures, + ); + inputStatus.innerText = `loaded ${commafy( + loaded, + )} users into queue (${commafy(failures)} failures)`; + loadQueue(); + }) + .catch(e => { + console.error(logstr, e); + inputStatus.innerText = e.message; + }) + .finally(() => { + timeout = setTimeout(() => { + inputStatus.innerText = 'Click or Drag to Import File'; + timeout = null; + }, 10e3); + }); }); - }); }); for (const i of files) { reader.readAsText(i); } } - input.addEventListener("input", e => { + input.addEventListener('input', e => { const target = e.target as HTMLInputElement; - onInput(target.files) + onInput(target.files); }); - importLabel.addEventListener("dragenter", e => e.preventDefault()); - importLabel.addEventListener("dragover", e => e.preventDefault()); - importLabel.addEventListener("drop", e => { + importLabel.addEventListener('dragenter', e => e.preventDefault()); + importLabel.addEventListener('dragover', e => e.preventDefault()); + importLabel.addEventListener('drop', e => { e.preventDefault(); onInput(e?.dataTransfer?.files); }); diff --git a/src/pages/safelist/index.html b/src/pages/safelist/index.html index 978d600..021b7b3 100644 --- a/src/pages/safelist/index.html +++ b/src/pages/safelist/index.html @@ -1,21 +1,30 @@ - + Blue Blocker Queue - - + +

SafeList

-

safelist importing supports json and csv. csv must use comma separators and no quotes

-

requires at least user_id or id and an optional name or screen_name

+

+ safelist importing supports json and csv. csv must use comma separators and no + quotes +

+

+ requires at least user_id or id and an optional name or screen_name +

safelist

- + export clear diff --git a/src/pages/safelist/index.ts b/src/pages/safelist/index.ts index 4db71fc..daef22d 100644 --- a/src/pages/safelist/index.ts +++ b/src/pages/safelist/index.ts @@ -1,7 +1,15 @@ -import { api, logstr, DefaultOptions, ErrorEvent, EventKey, MessageEvent, SoupcanExtensionId } from "../../constants.js"; -import { commafy } from "../../utilities.js"; -import "../style.css"; -import "./style.css"; +import { + api, + logstr, + DefaultOptions, + ErrorEvent, + EventKey, + MessageEvent, + SoupcanExtensionId, +} from '../../constants.js'; +import { commafy } from '../../utilities.js'; +import '../style.css'; +import './style.css'; function importSafelist(target: HTMLInputElement) { if (!target.files?.length) { @@ -10,79 +18,108 @@ function importSafelist(target: HTMLInputElement) { const reader = new FileReader(); let loaded: number = 0; let success: boolean; - reader.addEventListener("load", l => { + reader.addEventListener('load', l => { // @ts-ignore const payload = l.target.result as string; - api.storage.sync.get({ unblocked: { }}).then(items => { - // so we have plain text files as an accepted type, so lets try both json and csv formats - try { - // json - const userList = JSON.parse(payload) as [{ user_id?: string, id?: string, screen_name?: string, name?: string }]; - userList.forEach(user => { - const user_id = user?.user_id ?? user.id; - if (!user_id) { - throw new Error("failed to read user, expected at least one of: {user_id, id}."); - } - success = true; - items.unblocked[user_id] = user?.screen_name ?? user?.name ?? items.unblocked[user_id] ?? null; - loaded++; - }); - } catch (e) { - console.debug(logstr, "json failed", e, "trying csv..."); - } - try { - // csv - console.debug(logstr, "attempting to read file using csv scheme"); - let headers: Array; - payload.split("\n").map(i => i.trim()).forEach(line => { - if (line.match(/"'/)) { - throw new Error("failed to read file, csv must not include quotes."); - } - if (headers === undefined) { - headers = line.split(",").map(i => i.trim()); - if (!headers.includes("user_id") && !headers.includes("id")) { - throw new Error("failed to read file, expected at least one of: {user_id, id}."); + api.storage.sync + .get({ unblocked: {} }) + .then(items => { + // so we have plain text files as an accepted type, so lets try both json and csv formats + try { + // json + const userList = JSON.parse(payload) as [ + { user_id?: string; id?: string; screen_name?: string; name?: string }, + ]; + userList.forEach(user => { + const user_id = user?.user_id ?? user.id; + if (!user_id) { + throw new Error( + 'failed to read user, expected at least one of: {user_id, id}.', + ); } - console.debug(logstr, "headers:", headers); - return; - } - const user: { user_id?: string, id?: string, screen_name?: string, name?: string } = { }; - // @ts-ignore just ignore this monstrosity, it makes it easier afterwards - line.split(",").map(i => i.trim()).forEach((value, index) => user[headers[index]] = value); - const user_id = user?.user_id ?? user.id as string; - success = true; - const name = user?.screen_name ?? user?.name; - switch (name) { - default: - // theoretically we'd want to search for any character that can't - // be in a handle, but just whitespace will do - if (!name.match(/\s"'/)) { - items.unblocked[user_id] = name; - break; + success = true; + items.unblocked[user_id] = + user?.screen_name ?? user?.name ?? items.unblocked[user_id] ?? null; + loaded++; + }); + } catch (e) { + console.debug(logstr, 'json failed', e, 'trying csv...'); + } + try { + // csv + console.debug(logstr, 'attempting to read file using csv scheme'); + let headers: Array; + payload + .split('\n') + .map(i => i.trim()) + .forEach(line => { + if (line.match(/"'/)) { + throw new Error( + 'failed to read file, csv must not include quotes.', + ); + } + if (headers === undefined) { + headers = line.split(',').map(i => i.trim()); + if (!headers.includes('user_id') && !headers.includes('id')) { + throw new Error( + 'failed to read file, expected at least one of: {user_id, id}.', + ); + } + console.debug(logstr, 'headers:', headers); + return; + } + const user: { + user_id?: string; + id?: string; + screen_name?: string; + name?: string; + } = {}; + line.split(',') + .map(i => i.trim()) + // @ts-ignore just ignore this monstrosity, it makes it easier afterwards + .forEach((value, index) => (user[headers[index]] = value)); + const user_id = user?.user_id ?? (user.id as string); + success = true; + const name = user?.screen_name ?? user?.name; + switch (name) { + default: + // theoretically we'd want to search for any character that can't + // be in a handle, but just whitespace will do + if (!name.match(/\s"'/)) { + items.unblocked[user_id] = name; + break; + } + case null: + case undefined: + case '': + case 'null': + case 'undefined': + items.unblocked[user_id] = items.unblocked[user_id] ?? null; } - case null: - case undefined: - case "": - case "null": - case "undefined": - items.unblocked[user_id] = items.unblocked[user_id] ?? null; - } - loaded++; + loaded++; + }); + } catch (e) { + console.debug(logstr, 'csv failed.', e); + } + if (!success) { + throw new Error( + 'failed to read file. make sure file is csv or json and contains at least user_id or id for each user.', + ); + } + return api.storage.sync.set(items); + }) + .then(() => { + const msg = `loaded ${commafy(loaded)} users into safelist`; + console.log(logstr, msg); + document.getElementsByName('safelist-status').forEach(s => { + s.innerText = msg; }); - } catch (e) { - console.debug(logstr, "csv failed.", e); - } - if (!success) { - throw new Error("failed to read file. make sure file is csv or json and contains at least user_id or id for each user."); - } - return api.storage.sync.set(items); - }).then(() => { - const msg = `loaded ${commafy(loaded)} users into safelist`; - console.log(logstr, msg); - document.getElementsByName("safelist-status").forEach(s => { - s.innerText = msg; - }); - }).catch(e => document.getElementsByName("safelist-status").forEach(s => s.innerText = e.message)); + }) + .catch(e => + document + .getElementsByName('safelist-status') + .forEach(s => (s.innerText = e.message)), + ); }); for (const i of target.files) { reader.readAsText(i); @@ -90,28 +127,38 @@ function importSafelist(target: HTMLInputElement) { } function exportSafelist() { - api.storage.sync.get({ unblocked: { }}).then(items => { + api.storage.sync.get({ unblocked: {} }).then(items => { // the unblocked list needs to be put into a different format for export const safelist = items.unblocked as { [k: string]: string | undefined }; - const content = "user_id,screen_name\n" + Object.entries(safelist).map(i => i[0] + "," + (i[1] ?? "this user's @ is not stored")).join("\n"); + const content = + 'user_id,screen_name\n' + + Object.entries(safelist) + .map(i => i[0] + ',' + (i[1] ?? "this user's @ is not stored")) + .join('\n'); const e = document.createElement('a'); - e.href = "data:text/csv;charset=utf-8," + encodeURIComponent(content); - e.target = "_blank"; - e.download = "BlueBlockerSafelist.csv"; + e.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(content); + e.target = '_blank'; + e.download = 'BlueBlockerSafelist.csv'; e.click(); }); } -document.addEventListener("DOMContentLoaded", () => { - const safelistInput = document.getElementById("import-safelist") as HTMLInputElement; +document.addEventListener('DOMContentLoaded', () => { + const safelistInput = document.getElementById('import-safelist') as HTMLInputElement; // safelist logic - safelistInput.addEventListener("input", e => importSafelist(e.target as HTMLInputElement)); - document.getElementsByName("export-safelist").forEach(e => e.addEventListener("click", exportSafelist)); - document.getElementsByName("clear-safelist").forEach(e => { - e.addEventListener("click", () => - api.storage.sync.set({ unblocked: { }}).then(() => - document.getElementsByName("safelist-status").forEach(s => s.innerText = "cleared safelist.") - ) + safelistInput.addEventListener('input', e => importSafelist(e.target as HTMLInputElement)); + document + .getElementsByName('export-safelist') + .forEach(e => e.addEventListener('click', exportSafelist)); + document.getElementsByName('clear-safelist').forEach(e => { + e.addEventListener('click', () => + api.storage.sync + .set({ unblocked: {} }) + .then(() => + document + .getElementsByName('safelist-status') + .forEach(s => (s.innerText = 'cleared safelist.')), + ), ); }); -}); \ No newline at end of file +}); diff --git a/src/pages/safelist/style.css b/src/pages/safelist/style.css index 02918da..0dad418 100644 --- a/src/pages/safelist/style.css +++ b/src/pages/safelist/style.css @@ -13,7 +13,7 @@ main { margin: 0; } -input[type="file"] { +input[type='file'] { position: absolute; display: none; top: -100vh; diff --git a/src/pages/style.css b/src/pages/style.css index 3c1a024..a8d2d53 100644 --- a/src/pages/style.css +++ b/src/pages/style.css @@ -7,13 +7,13 @@ html { --bg2color: #151416; --bg3color: var(--bordercolor); --blockquote: var(--bordercolor); - --textcolor: #DDD; - --bordercolor: #2D333A; + --textcolor: #ddd; + --bordercolor: #2d333a; --linecolor: var(--bordercolor); --borderhover: var(--interact); - --subtle: #EEEEEE80; + --subtle: #eeeeee80; --shadowcolor: #00000080; - --activeshadowcolor: #000000B3; + --activeshadowcolor: #000000b3; --screen-cover: #00000080; --border-size: 1px; --border-radius: 3px; @@ -29,11 +29,16 @@ body { width: 100%; background: var(--bg1color); } -html, body, h1, p { +html, +body, +h1, +p { margin: 0; } -a, a:active, a:focus { +a, +a:active, +a:focus { color: var(--textcolor); text-shadow: 0 2px 3px 1px var(--shadowcolor); -webkit-transition: var(--transition) var(--fadetime); @@ -47,8 +52,12 @@ a:hover { color: var(--interact); } - button, button:active, button:focus, -.button, .button:active, .button:focus { +button, +button:active, +button:focus, +.button, +.button:active, +.button:focus { font-size: 1em; padding: 0.5em 1em; color: var(--textcolor); @@ -62,13 +71,15 @@ a:hover { transition: var(--transition) var(--fadetime); cursor: pointer; } -button:hover, .button:hover { +button:hover, +.button:hover { box-shadow: 0 0 10px 3px var(--activeshadowcolor); border-color: var(--interact); color: var(--interact); } -pre, code { +pre, +code { font-family: Hack, DejaVu Sans Mono, Inconsolata, monospace !important; background: var(--bg2color); border-radius: var(--border-radius); @@ -83,13 +94,13 @@ pre { ::-webkit-scrollbar { width: 12px; - height:12px; + height: 12px; } ::-webkit-scrollbar-track { background: var(--bg2color); } ::-webkit-scrollbar-thumb { - background: #4D535A; + background: #4d535a; border-radius: 4px; border: 2px solid var(--bg2color); } diff --git a/src/parsers/instructions.ts b/src/parsers/instructions.ts index 9cef434..b89024f 100644 --- a/src/parsers/instructions.ts +++ b/src/parsers/instructions.ts @@ -29,19 +29,19 @@ const UserObjectPath: string[] = [ 'user_results', 'result', ]; -const IgnoreTweetTypes = new Set(['TimelineTimelineCursor', 'TweetTombstone']); +const IgnoreTweetTypes = new Set(['TimelineTimelineCursor']); const PromotedStrings = new Set(['suggest_promoted', 'Promoted', 'promoted']); -function handleUserObject(obj: any, config: Config, from_blue: boolean) { +function handleUserObject(obj: any, config: CompiledConfig, from_blue: boolean) { let userObj = obj.user_results.result; - if (userObj.__typename === "UserUnavailable") { - console.log(logstr, "user is unavailable", userObj); + if (userObj.__typename === 'UserUnavailable') { + console.log(logstr, 'user is unavailable', userObj); return; } - if (userObj.__typename !== "User") { - console.error(logstr, "could not parse user object", userObj); + if (userObj.__typename !== 'User') { + console.error(logstr, 'could not parse user object', userObj); return; } @@ -52,17 +52,18 @@ function handleUserObject(obj: any, config: Config, from_blue: boolean) { BlockBlueVerified(obj.user_results.result, config); } -export function ParseTimelineUser(obj: any, config: Config, from_blue: boolean) { +export function ParseTimelineUser(obj: any, config: CompiledConfig, from_blue: boolean) { handleUserObject(obj, config, from_blue); } -function handleTweetObject(obj: any, config: Config, promoted: boolean) { +function handleTweetObject(obj: any, config: CompiledConfig, promoted: boolean) { let ptr = obj, uses_blue_feats = false; - if (ptr.__typename == 'TweetTombstone') { - return; - } for (const key of UserObjectPath) { + if (ptr.__typename == 'TweetTombstone') { + // If we hit a deleted tweet, we bail + return; + } if (ptr.hasOwnProperty(key)) { ptr = ptr[key]; if ( @@ -84,7 +85,7 @@ function handleTweetObject(obj: any, config: Config, promoted: boolean) { BlockBlueVerified(ptr as BlueBlockerUser, config); } -export function ParseTimelineTweet(tweet: any, config: Config) { +export function ParseTimelineTweet(tweet: any, config: CompiledConfig) { if (IgnoreTweetTypes.has(tweet.itemContent.itemType)) { return; } @@ -131,7 +132,7 @@ export function ParseTimelineTweet(tweet: any, config: Config) { export function HandleInstructionsResponse( e: CustomEvent, body: Body, - config: Config, + config: CompiledConfig, ) { // pull the "instructions" object from the tweet let _instructions = body; @@ -190,7 +191,7 @@ export function HandleInstructionsResponse( if (tweet.content.itemContent?.itemType == 'TimelineTweet') { ParseTimelineTweet(tweet.content, config); } else if (tweet.content.itemContent?.itemType == 'TimelineUser') { - const from_blue = (e.detail.parsedUrl[1] == "BlueVerifiedFollowers"); + const from_blue = e.detail.parsedUrl[1] == 'BlueVerifiedFollowers'; ParseTimelineUser(tweet.content.itemContent, config, from_blue); } break; diff --git a/src/parsers/search.ts b/src/parsers/search.ts index c94aaba..28ca384 100644 --- a/src/parsers/search.ts +++ b/src/parsers/search.ts @@ -1,7 +1,11 @@ import { BlockBlueVerified } from '../shared'; // This file handles requests made pertaining to search results. -export function HandleTypeahead(e: CustomEvent, body: Body, config: Config) { +export function HandleTypeahead( + e: CustomEvent, + body: Body, + config: CompiledConfig, +) { // This endpoints appears to be extra/miscellaneous response data returned // when doing a search. it has a user list in it, so run it through the gamut! if (!body?.users?.length) { diff --git a/src/parsers/timeline.ts b/src/parsers/timeline.ts index ee104d5..686cea6 100644 --- a/src/parsers/timeline.ts +++ b/src/parsers/timeline.ts @@ -3,7 +3,7 @@ import { BlockBlueVerified } from '../shared'; // including the "For You" page as well as the "Following" page. it also // seems to work for the "adaptive.json" response from search results -export function HandleForYou(e: CustomEvent, body: Body, config: Config) { +export function HandleForYou(e: CustomEvent, body: Body, config: CompiledConfig) { // This API endpoint currently does not deliver information required for // block filters (in particular, it's missing affiliates_highlighted_label). // The above doesn't seem completely true. it's missing affiliates specifically diff --git a/src/parsers/unblock.ts b/src/parsers/unblock.ts index 3db98fb..8d37655 100644 --- a/src/parsers/unblock.ts +++ b/src/parsers/unblock.ts @@ -2,13 +2,9 @@ import { api, logstr } from '../constants'; import { MakeToast, RemoveUserBlockHistory } from '../utilities'; import { UnblockCache } from '../shared'; -export function HandleUnblock( - e: CustomEvent, - body: any, - config: Config, -) { +export function HandleUnblock(e: CustomEvent, body: any, config: Config) { if (body?.id_str === undefined || body?.screen_name === undefined) { - console.error(logstr, "got and unknown or mangled response from an unblock request:", body); + console.error(logstr, 'got and unknown or mangled response from an unblock request:', body); MakeToast("couldn't parse unblock response", config); return; } @@ -18,9 +14,11 @@ export function HandleUnblock( return; } - RemoveUserBlockHistory(user_id).catch(console.error); // just log the error in case the user doesn't exist + RemoveUserBlockHistory(user_id).catch(console.error); // just log the error in case the user doesn't exist UnblockCache.add(user_id); const unblocked = config.unblocked; unblocked[user_id] = body.screen_name; - api.storage.sync.set({ unblocked }).then(() => MakeToast(`okay, @${body.screen_name} won't be blocked again.`, config)); + api.storage.sync + .set({ unblocked }) + .then(() => MakeToast(`okay, @${body.screen_name} won't be blocked again.`, config)); } diff --git a/src/popup/index.html b/src/popup/index.html index d8e47cf..f929c1b 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -1,4 +1,4 @@ - + @@ -35,11 +35,13 @@

🅱️lue Blocker< + - + +
+
+ + +