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< + - + +
+
+ + +