diff --git a/CHANGELOG.md b/CHANGELOG.md index 970bf39..98828cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ Summary 5. version timestamp follow the yyyy.MM.dd format ``` +# 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 + # 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 diff --git a/package.json b/package.json index d74b9f3..35b74dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blue-blocker", - "version": "0.3.3", + "version": "0.3.4", "author": "DanielleMiu", "description": "Blocks all Twitter Blue verified users on twitter.com", "type": "module", diff --git a/src/background/db.ts b/src/background/db.ts index 4c4ce1e..a731c1f 100644 --- a/src/background/db.ts +++ b/src/background/db.ts @@ -1,4 +1,4 @@ -import { api, logstr, EventKey, LegacyVerifiedUrl, MessageEvent, ErrorEvent, HistoryStateBlocked, HistoryStateUnblocked } from "../constants"; +import { api, logstr, EventKey, LegacyVerifiedUrl, MessageEvent, ErrorEvent, HistoryStateUnblocked } from "../constants"; import { commafy } from "../utilities"; const expectedVerifiedUsersCount = 407520; @@ -71,7 +71,7 @@ export async function PopulateVerifiedDb() { }); } catch (_e) { - const e = _e as Error; + // const e = _e as Error; await new Promise((resolve, reject) => { const transaction = legacyDb.transaction([legacyDbStore], "readwrite"); const store = transaction.objectStore(legacyDbStore); @@ -161,6 +161,7 @@ export async function PopulateVerifiedDb() { } 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"); transaction.onabort = transaction.onerror = reject; @@ -175,68 +176,63 @@ export function CheckDbIsUserLegacyVerified(user_id: string, handle: string): Pr }).catch(e => { // if the db has already been loaded, we can safely reconnect if (legacyDbLoaded) { - PopulateVerifiedDb(); + return PopulateVerifiedDb() + .finally(() => { + throw e; // re-throw error + }); } throw e; // re-throw error }); } -let historyDb: IDBDatabase; - -export interface BlockedUser { - user_id: string, - user: { name: string, screen_name: string }, - reason: number, - external_reason?: string, - state: number, - time: Date, -} +let db: IDBDatabase; -const historyDbName = "blue-blocker-db"; +const dbName = "blue-blocker-db"; export const historyDbStore = "blocked_users"; -const historyDbVersion = 1; +const dbVersion = 1; +// used so we don't load the db twice +let dbLoaded: boolean = false; -export function ConnectHistoryDb(): Promise { +export function ConnectDb(): Promise { // why use a promise instead of a normal async? so that we can resolve or reject on db connect - return new Promise(async (resolve, reject) => { + return new Promise((resolve, reject) => { // this logic should also be much easier because we don't need to populate anything (thank god) - const DBOpenRequest = indexedDB.open(historyDbName, historyDbVersion); + const DBOpenRequest = indexedDB.open(dbName, dbVersion); DBOpenRequest.onerror = DBOpenRequest.onblocked = () => { - console.error(logstr, "failed to connect history database:", DBOpenRequest); + console.error(logstr, "failed to connect database:", DBOpenRequest); return reject(); }; DBOpenRequest.onupgradeneeded = () => { - console.debug(logstr, "upgrading history db:", DBOpenRequest); - historyDb = DBOpenRequest.result; - - if (historyDb.objectStoreNames.contains(historyDbStore)) { - return; + console.debug(logstr, "upgrading db:", DBOpenRequest); + 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 = historyDb.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."); }; - DBOpenRequest.onsuccess = () => { - historyDb = DBOpenRequest.result; - console.log(logstr, "successfully connected to history db"); - return resolve(historyDb); + DBOpenRequest.onsuccess = async () => { + db = DBOpenRequest.result; + if (dbLoaded) { + return resolve(db); + } + dbLoaded = true; + + console.log(logstr, "successfully connected to db"); + return resolve(db); }; }); } -export function AddUserToHistory(blockUser: BlockUser): Promise { - const user: BlockedUser = { - ...blockUser, - state: HistoryStateBlocked, - time: new Date(), - }; - return new Promise(async (resolve, reject) => { - const transaction = historyDb.transaction([historyDbStore], "readwrite"); +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"); transaction.onabort = transaction.onerror = reject; transaction.oncomplete = () => resolve(); @@ -244,17 +240,20 @@ export function AddUserToHistory(blockUser: BlockUser): Promise { store.add(user); transaction.commit(); - }).catch(async (e) => { + }).catch(e => // attempt to reconnect to the db - await ConnectHistoryDb(); - throw e; // re-throw error to retry - }); + ConnectDb() + .finally(() => { + throw e; // re-throw error to retry + }) + ); } 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 = historyDb.transaction([historyDbStore], "readwrite"); + const transaction = db.transaction([historyDbStore], "readwrite"); transaction.onabort = transaction.onerror = reject; transaction.oncomplete = () => resolve(); @@ -266,6 +265,8 @@ export function RemoveUserFromHistory(user_id: string): Promise { const user = req.result as BlockedUser; res(user); }; + }).catch(e => { + throw e; }); user.state = HistoryStateUnblocked; @@ -276,9 +277,11 @@ export function RemoveUserFromHistory(user_id: string): Promise { } catch (e) { reject(e); } - }).catch(async (e) => { + }).catch(e => // attempt to reconnect to the db - await ConnectHistoryDb(); - throw e; // re-throw error to retry - }); + ConnectDb() + .finally(() => { + throw e; // re-throw error to retry + }) + ); } diff --git a/src/background/index.ts b/src/background/index.ts index f3c92a6..a12e7ec 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -1,6 +1,6 @@ -import { api, logstr, AddToHistoryAction, ErrorStatus, IsVerifiedAction, MessageStatus, ReasonExternal, RemoveFromHistoryAction, SoupcanExtensionId, SuccessStatus, DefaultOptions } from '../constants'; -import { abbreviate } from '../utilities'; -import { AddUserToHistory, CheckDbIsUserLegacyVerified, ConnectHistoryDb, PopulateVerifiedDb, RemoveUserFromHistory } from './db'; +import { api, logstr, AddToHistoryAction, ErrorStatus, IsVerifiedAction, ReasonExternal, RemoveFromHistoryAction, SoupcanExtensionId, SuccessStatus, DefaultOptions } from '../constants'; +import { abbreviate, RefId } from '../utilities'; +import { AddUserToHistory, CheckDbIsUserLegacyVerified, ConnectDb, PopulateVerifiedDb, RemoveUserFromHistory } from './db'; import { BlockQueue } from '../models/block_queue'; api.action.setBadgeBackgroundColor({ color: "#666" }); @@ -37,66 +37,46 @@ api.storage.sync.onChanged.addListener(async items => { } }); -ConnectHistoryDb(); +ConnectDb(); -interface Response { - status: MessageStatus, -} - -interface SuccessResponse { - status: "SUCCESS", - result: any, -} - -interface ErrorResponse { - status: "ERROR", - message: string, - error?: Error, -} - -api.runtime.onMessage.addListener((m, s, r) => { (async (_message, sender, respond) => { +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 - let response: Response; - switch (_message?.action) { - case IsVerifiedAction: - const verifiedMessage = _message as { user_id: string, handle: string }; - try { + 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; - } catch (e) { - response = { status: ErrorStatus, message: "unknown error", error: e } as ErrorResponse; - } - break; + break; - case AddToHistoryAction: - const historyMessage = _message.data as BlockUser; - try { + case AddToHistoryAction: + const historyMessage = message.data as BlockedUser; await AddUserToHistory(historyMessage); response = { status: SuccessStatus, result: null } as SuccessResponse; - } catch (e) { - response = { status: ErrorStatus, message: "unknown error", error: e } as ErrorResponse; - } - break; + break; - case RemoveFromHistoryAction: - const removeMessage = _message.data as { user_id: string }; - try { + case RemoveFromHistoryAction: + const removeMessage = message.data as { user_id: string }; await RemoveUserFromHistory(removeMessage.user_id); response = { status: SuccessStatus, result: null } as SuccessResponse; - } catch (e) { - response = { status: ErrorStatus, message: "unknown error", error: e } as ErrorResponse; - } - break; - - default: - console.error(logstr, "got a message that couldn't be handled from sender:", sender, _message); - response = { status: ErrorStatus, message: "unknown action" } as ErrorResponse; + 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; } - respond(response); -})(m, s, r); return true }); + console.debug(logstr, refid, "respond:", response); +})(m, s).finally(() => r(response)); return true }); ////////////////////////////////////////////////// EXTERNAL MESSAGE HANDLING ////////////////////////////////////////////////// @@ -104,7 +84,9 @@ const queue = new BlockQueue(api.storage.local); const [blockAction] = ["BLOCK"]; const allowedExtensionIds = new Set([SoupcanExtensionId]); -api.runtime.onMessageExternal.addListener((m, s, r) => { (async (_message, sender, respond) => { +api.runtime.onMessageExternal.addListener((m, s, r) => { let response: MessageResponse; (async (message, sender) => { + const refid = RefId(); + console.debug(logstr, refid, "ext recv:", message, sender); if (!allowedExtensionIds.has(sender?.id ?? "")) { return; } @@ -113,21 +95,23 @@ api.runtime.onMessageExternal.addListener((m, s, r) => { (async (_message, sende // 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 - let response: Response; - switch (_message?.action) { - case blockAction: - const message = _message as { action: string, user_id: string, name: string, screen_name: string, reason: string }; - try { - await queue.push({ user_id: message.user_id, user: { name: message.name, screen_name: message.screen_name }, reason: ReasonExternal, external_reason: message.reason }); + try { + switch (message?.action) { + case blockAction: + const blockMessage = message as { user_id: string, name: string, screen_name: string, reason: string }; + const user: BlockUser = { user_id: blockMessage.user_id, user: { name: blockMessage.name, screen_name: blockMessage.screen_name }, reason: ReasonExternal, external_reason: blockMessage.reason }; + await queue.push(user); response = { status: SuccessStatus, result: "user queued for blocking" } as SuccessResponse; - } catch (e) { - response = { status: ErrorStatus, message: "unknown error", error: e } as ErrorResponse; - } - break; - - default: - console.error(logstr, "got a message that couldn't be handled from sender:", sender, _message); - response = { status: ErrorStatus, message: "unknown action" } as ErrorResponse; + 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; } - respond(response); -})(m, s, r); return true }); + console.debug(logstr, refid, "respond:", response); +})(m, s).finally(() => r(response)); return true }); diff --git a/src/constants.ts b/src/constants.ts index 7a9d528..4bd483c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,8 +1,19 @@ -let _api: typeof chrome | typeof browser; +let _api: { + action: typeof chrome.action | typeof browser.browserAction, + runtime: typeof chrome.runtime, + storage: typeof chrome.storage | typeof browser.storage, +}; try { - _api = browser; - // manifest v2 has the action api stored in browserAction, so manually assign it to action - _api.action = browser.browserAction; + _api = { + // @ts-ignore + runtime: { + ...browser.runtime, + OnInstalledReason: chrome.runtime.OnInstalledReason, + restartAfterDelay: chrome.runtime.restartAfterDelay, + }, + storage: browser.storage, + action: browser.browserAction, + }; } catch (ReferenceError) { _api = chrome; } @@ -59,7 +70,7 @@ export const Headers = [ "x-twitter-auth-type", "x-twitter-client-language", ]; -export const [HistoryStateBlocked, HistoryStateUnblocked] = [0, 1]; +export const [HistoryStateBlocked, HistoryStateUnblocked, HistoryStateGone] = [0, 1, 2]; export const ReasonExternal: number = -1; export const ReasonBlueVerified: number = 0; export const ReasonNftAvatar: number = 1; @@ -74,14 +85,13 @@ export const ReasonMap = { [ReasonPromoted]: "promoting tweets", }; export const LegacyVerifiedUrl: string = "https://gist.githubusercontent.com/travisbrown/b50d6745298cccd6b1f4697e4ec22103/raw/012009351630dc351e3a763b49bf24fa50ca3eb7/legacy-verified.csv"; -export const Browser = chrome.runtime.getManifest()?.browser_specific_settings?.gecko === undefined ? "chrome" : "firefox"; +export const Browser = api.runtime.getManifest()?.browser_specific_settings?.gecko === undefined ? "chrome" : "firefox"; export const SoupcanExtensionId = Browser === "chrome" ? "hcneafegcikghlbibfmlgadahjfckonj" : "soupcan@beth.lgbt"; // internal message actions export const [IsVerifiedAction, AddToHistoryAction, RemoveFromHistoryAction] = ["is_verified", "add_user_to_history", "remove_user_from_history"]; -export type MessageStatus = "SUCCESS" | "ERROR"; -export const SuccessStatus: MessageStatus = "SUCCESS"; -export const ErrorStatus: MessageStatus = "ERROR"; +export const SuccessStatus: SuccessStatus = "SUCCESS"; +export const ErrorStatus: ErrorStatus = "ERROR"; // multi-tab event keys export const EventKey = "MultiTabEvent"; diff --git a/src/content/startup.ts b/src/content/startup.ts index 80adbcf..f3e2ae4 100644 --- a/src/content/startup.ts +++ b/src/content/startup.ts @@ -5,7 +5,7 @@ import { api, DefaultOptions } from "../constants"; // @ts-ignore import inject from "/src/injected/inject?script&module"; const script = document.createElement("script"); -script.src = chrome.runtime.getURL(inject); +script.src = api.runtime.getURL(inject); script.id = "injected-blue-block-xhr"; script.type = "text/javascript"; document.head.prepend(script); diff --git a/src/global.d.ts b/src/global.d.ts index 57ac80a..8f87363 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -44,6 +44,30 @@ interface BlueBlockerUser { promoted_tweet?: boolean, } +// extension message types +type SuccessStatus = "SUCCESS"; +type ErrorStatus = "ERROR"; +type MessageStatus = SuccessStatus | ErrorStatus; + +interface RuntimeMessage { + action: string, + data: any, +} + +interface MessageResponse { + status: MessageStatus, +} + +interface SuccessResponse { + status: SuccessStatus, + result: any, +} + +interface ErrorResponse { + status: ErrorStatus, + message: string, +} + interface BlueBlockerEvent { url: URL | string, parsedUrl: RegExpExecArray, @@ -79,3 +103,12 @@ interface BlockUser { reason: number, external_reason?: string, } + +interface BlockedUser { + user_id: string, + user: { name: string, screen_name: string }, + reason: number, + external_reason?: string, + state: number, + time: Date, +} diff --git a/src/injected/style.css b/src/injected/style.css index 2900f5f..7e7920e 100644 --- a/src/injected/style.css +++ b/src/injected/style.css @@ -85,6 +85,13 @@ background-position-x: 1em; background-position-y: center; } +#injected-blue-block-toasts .toast.warn { + background: url('/icon/warn.png') var(--bg1color); + background-repeat: no-repeat; + background-size: 2.5em; + background-position-x: 1em; + background-position-y: center; +} #injected-blue-block-toasts .toast a, #injected-blue-block-toasts .toast a:active, diff --git a/src/manifest.ts b/src/manifest.ts index 07ad45c..6d420da 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -3,7 +3,7 @@ import { defineManifest } from "@crxjs/vite-plugin"; export default defineManifest({ name: "Blue Blocker", description: "Blocks all Twitter Blue verified users on twitter.com", - version: "0.3.3", + version: "0.3.4", manifest_version: 3, icons: { "128": "icon/icon-128.png", diff --git a/src/pages/history/index.ts b/src/pages/history/index.ts index 548ffa1..db73454 100644 --- a/src/pages/history/index.ts +++ b/src/pages/history/index.ts @@ -1,6 +1,6 @@ import { commafy, EscapeHtml, RefId } from "../../utilities.js"; -import { api, logstr, HistoryStateBlocked, ReasonMap, ReasonExternal } from "../../constants.js"; -import { BlockedUser, ConnectHistoryDb, historyDbStore } from "../../background/db.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"; @@ -9,9 +9,9 @@ const refid = RefId(); // grab block counter critical point to compare counters safely blockCounter.getCriticalPoint(refid) -.then(ConnectHistoryDb) +.then(ConnectDb) .then(db => { - new Promise>((resolve, reject) => { + return new Promise((resolve, reject) => { const transaction = db.transaction([historyDbStore], "readonly"); transaction.onabort = transaction.onerror = reject; const store = transaction.objectStore(historyDbStore); @@ -26,15 +26,73 @@ blockCounter.getCriticalPoint(refid) }).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(users.length) + e.innerText = commafy(blockedCount) ); blockCounter.getCriticalPoint(refid) .then(() => blockCounter.storage.get({ BlockCounter: 0 })) .then(items => items.BlockCounter as number) .then(count => { - if (users.length === count) { + if (blockedCount === count) { return; } @@ -47,9 +105,9 @@ blockCounter.getCriticalPoint(refid) const refid = RefId(); blockCounter.getCriticalPoint(refid) .then(() => - blockCounter.storage.set({ BlockCounter: users.length }) + blockCounter.storage.set({ BlockCounter: blockedCount }) ).then(() => { - console.log(logstr, "reset block counter to", users.length); + console.log(logstr, "reset block counter to", blockedCount); resetCounter.style.display = ""; }).finally(() => blockCounter.releaseCriticalPoint(refid) @@ -66,43 +124,6 @@ blockCounter.getCriticalPoint(refid) return; } - queueDiv.innerHTML = ""; - - const reasons: { [r: number]: number } = { }; - users.reverse().forEach(item => { - if (!reasons.hasOwnProperty(item.reason)) { - reasons[item.reason] = 0; - } - reasons[item.reason]++; - - 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"); - const state = item.state === HistoryStateBlocked ? "blocked" : "unblocked"; - p3.innerText = "current state: " + state; - div.appendChild(p3); - - const p4 = document.createElement("p"); - const datetime = item.time.toLocaleDateString('en', { year: 'numeric', month: 'short', day: 'numeric' }) - .replace(`, ${new Date().getFullYear()}`, '') - + ', ' - + item.time.toLocaleTimeString() - .toLowerCase(); - p4.innerText = state + " on " + datetime; - div.appendChild(p4); - - queueDiv.appendChild(div); - }); - const detailedCounts = document.getElementById("detailed-counts") as HTMLElement; const reasonMap = { [ReasonExternal]: "external extension", diff --git a/src/pages/history/style.css b/src/pages/history/style.css index 5528562..411bb6b 100644 --- a/src/pages/history/style.css +++ b/src/pages/history/style.css @@ -96,6 +96,7 @@ main { } .subtitle { margin-bottom: 1em; + color: var(--subtle); } #reset-counter { diff --git a/src/pages/queue/index.html b/src/pages/queue/index.html index d4d3295..5052565 100644 --- a/src/pages/queue/index.html +++ b/src/pages/queue/index.html @@ -22,12 +22,12 @@

Block Queue

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

diff --git a/src/pages/queue/index.ts b/src/pages/queue/index.ts index 16d0b4a..d21b279 100644 --- a/src/pages/queue/index.ts +++ b/src/pages/queue/index.ts @@ -117,9 +117,11 @@ document.addEventListener("DOMContentLoaded", () => { } }); + 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; function onInput(files: FileList | null | undefined) { @@ -134,47 +136,57 @@ document.addEventListener("DOMContentLoaded", () => { const reader = new FileReader(); let loaded: number = 0; let failures: number = 0; + let safelisted: number = 0; reader.addEventListener("load", l => { inputStatus.innerText = "importing..."; // @ts-ignore const payload = l.target.result as string; - api.storage.local.get({ BlockQueue: [] }).then(items => { - const queue: { [u: string]: BlockUser } = { }; - for (const user of items.BlockQueue as BlockUser[]) { - queue[user.user_id] = user; - } - - const userList = JSON.parse(payload) as BlockUser[]; - userList.forEach(user => { - // 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 - ) { - console.error(logstr, "user object could not be processed:", user); - failures++; - return; + api.storage.sync.get({ unblocked: { }}) + .then(items => items.unblocked as { [k: string]: string | null }) + .then(safelist => { + api.storage.local.get({ BlockQueue: [] }).then(items => { + const queue: { [u: string]: BlockUser } = { }; + for (const user of items.BlockQueue as BlockUser[]) { + queue[user.user_id] = user; } - queue[user.user_id] = user; - loaded++; + const userList = JSON.parse(payload) as BlockUser[]; + userList.forEach(user => { + // 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 + ) { + console.error(logstr, "user object could not be processed:", user); + failures++; + return; + } + + if (safelist.hasOwnProperty(user.user_id)) { + safelisted++; + return; + } + + queue[user.user_id] = user; + loaded++; + }); + + return api.storage.local.set({ BlockQueue: Array.from(Object.values(queue)) }); + }).then(() => { + console.log(logstr, "successfully loaded", loaded, "users into queue. failures:", failures, "safelisted:", safelisted); + inputStatus.innerText = `loaded ${commafy(loaded)} users into queue (${commafy(failures)} failures, ${commafy(safelisted)} safelisted)`; + loadQueue(); + }).catch(e => { + console.error(logstr, e); + inputStatus.innerText = e.message; + }).finally(() => { + timeout = setTimeout(() => { + inputStatus.innerText = defaultInputText; + timeout = null; + }, 10e3); }); - - return api.storage.local.set({ BlockQueue: Array.from(Object.values(queue)) }); - }).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) { diff --git a/src/popup/index.html b/src/popup/index.html index d6521a5..6380ccb 100644 --- a/src/popup/index.html +++ b/src/popup/index.html @@ -29,7 +29,7 @@

🅱️lue Blocker -
+