diff --git a/assets/icon-128-greyscale.png b/assets/icon-128-greyscale.png new file mode 100644 index 0000000..5160d01 Binary files /dev/null and b/assets/icon-128-greyscale.png differ diff --git a/constants.js b/constants.js index 222cb4f..f595cea 100644 --- a/constants.js +++ b/constants.js @@ -20,8 +20,10 @@ export const DefaultOptions = { skipVerified: true, skipAffiliated: true, skip1Mplus: true, + skipFollowerCount: 1e6, blockNftAvatars: false, blockInterval: 15, + popupTimer: 30, // this isn't set, but is used // TODO: when migrating to firefox manifest v3, check to see if sets can be stored yet diff --git a/injected/inject.js b/injected/inject.js index e34d344..3efd711 100644 --- a/injected/inject.js +++ b/injected/inject.js @@ -21,7 +21,17 @@ // determine if request is a timeline/tweet-returning request const parsedUrl = RequestRegex.exec(this._url); if(this._url && parsedUrl) { - document.dispatchEvent(new CustomEvent("blue-blocker-event", { detail: { url : this._url, parsedUrl, 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, + }, + })); } }); return send.apply(this, arguments); diff --git a/makefile b/makefile index 06df58f..80410d6 100644 --- a/makefile +++ b/makefile @@ -10,6 +10,7 @@ firefox: mv manifest.json chrome-manifest.json mv firefox-manifest.json manifest.json zip "blue-blocker-firefox-${VERSION}.zip" \ + assets/icon-128-greyscale.png \ assets/icon-128.png \ assets/icon.png \ assets/error.png \ @@ -17,6 +18,7 @@ firefox: models/* \ parsers/* \ popup/* \ + pages/* \ manifest.json \ LICENSE \ readme.md \ @@ -30,6 +32,7 @@ chrome: # rm "blue-blocker-chrome-${VERSION}.zip" # endif zip "blue-blocker-chrome-${VERSION}.zip" \ + assets/icon-128-greyscale.png \ assets/icon-128.png \ assets/icon.png \ assets/error.png \ @@ -37,6 +40,7 @@ chrome: models/* \ parsers/* \ popup/* \ + pages/* \ manifest.json \ LICENSE \ readme.md \ diff --git a/models/block_counter.js b/models/block_counter.js index a41286f..689a4d4 100644 --- a/models/block_counter.js +++ b/models/block_counter.js @@ -23,7 +23,7 @@ export class BlockCounter { // try to access the critical point await this.storage.set({ [criticalPointKey]: { refId: this._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; + 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 diff --git a/models/block_queue.js b/models/block_queue.js index d7d546e..bc02810 100644 --- a/models/block_queue.js +++ b/models/block_queue.js @@ -23,7 +23,7 @@ export class BlockQueue { // try to access the critical point await this.storage.set({ [criticalPointKey]: { refId: this._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; + 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 diff --git a/models/queue_consumer.js b/models/queue_consumer.js index f6ca6af..9e7a56f 100644 --- a/models/queue_consumer.js +++ b/models/queue_consumer.js @@ -33,7 +33,7 @@ export class QueueConsumer { // try to access the critical point await this.storage.set({ [criticalPointKey]: { refId: this._refId, time: (new Date()).valueOf() + this._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; + cpRefId = (await this.storage.get({ [criticalPointKey]: null }))[criticalPointKey]?.refId; } else { return false; diff --git a/pages/queue.html b/pages/queue.html new file mode 100644 index 0000000..f24dda1 --- /dev/null +++ b/pages/queue.html @@ -0,0 +1,22 @@ + + + + + Blue Blocker Queue + + + + + +
+
+

Block Queue

+

Users will not be added to the queue or blocked while this page is open

+
+ loading... +
+
+
+ + + diff --git a/pages/queue.js b/pages/queue.js new file mode 100644 index 0000000..1aa092b --- /dev/null +++ b/pages/queue.js @@ -0,0 +1,81 @@ +import { BlockQueue } from "../models/block_queue.js"; +import { FormatLegacyName } from "../utilities.js"; +import { api, logstr } from "../constants.js"; + +// Define constants that shouldn't be exported to the rest of the addon +const queue = new BlockQueue(api.storage.local); + +// we need to obtain and hold on to the critical point as long as this tab is +// open so that any twitter tabs that are open are unable to block users +setInterval(async () => { + await queue.getCriticalPoint() +}, 500); + +async function unqueueUser(user_id, safelist) { + // 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 => { + items.unblocked[String(user_id)] = null; + api.storage.sync.set(items); + }); + } + + const items = await api.storage.local.get({ BlockQueue: [] }); + + for (let i = 0; i < items.BlockQueue.length; i++) { + if (items.BlockQueue[i].user_id === user_id) { + items.BlockQueue.splice(i, 1); + break; + } + } + + await api.storage.local.set(items); +} + +// interval doesn't run immediately, so do that here +queue.getCriticalPoint() +.then(() => api.storage.local.get({ BlockQueue: [] })) +.then(items => { + const queueDiv = document.getElementById("block-queue"); + + if (items.BlockQueue.length === 0) { + queueDiv.textContent = "your block queue is empty"; + return; + } + + queueDiv.innerHTML = null; + + items.BlockQueue.forEach(item => { + const { user, user_id } = item; + const div = document.createElement("div"); + + const p = document.createElement("p"); + p.innerHTML = `${user.legacy.name} (@${user.legacy.screen_name})`; + div.appendChild(p); + + const remove = document.createElement("button"); + remove.onclick = () => { + div.removeChild(remove); + unqueueUser(user_id, false).then(() => { + queueDiv.removeChild(div); + }); + console.log(logstr, `removed ${FormatLegacyName(user)} from queue`); + }; + remove.textContent = "remove"; + div.appendChild(remove); + + const never = document.createElement("button"); + never.onclick = () => { + div.removeChild(never); + unqueueUser(user_id, true).then(() => { + queueDiv.removeChild(div); + }); + console.log(logstr, `removed and safelisted ${FormatLegacyName(user)} from queue`); + }; + never.textContent = "never block"; + div.appendChild(never); + + queueDiv.appendChild(div); + }); +}); diff --git a/pages/style.css b/pages/style.css new file mode 100644 index 0000000..e219aff --- /dev/null +++ b/pages/style.css @@ -0,0 +1,124 @@ +html { + --transition: ease; + --fadetime: 0.15s; + --interact: #1da1f2; + --bg0color: #000000; + --bg1color: #1e1f25; + --bg2color: #151416; + --bg3color: var(--bordercolor); + --blockquote: var(--bordercolor); + --textcolor: #DDD; + --bordercolor: #2D333A; + --linecolor: var(--bordercolor); + --borderhover: var(--interact); + --subtle: #EEEEEE80; + --shadowcolor: #00000080; + --activeshadowcolor: #000000B3; + --screen-cover: #00000080; + --border-size: 1px; + --border-radius: 3px; + + background: var(--bg0color); +} +html * { + font-family: Bitstream Vera Sans, DejaVu Sans, Arial, Helvetica, sans-serif; +} +body { + background: var(--bg1color); + color: var(--textcolor); +} +html, body, main, h1, p { + margin: 0; +} +main { + color: var(--textcolor); + text-align: center; + width: 100%; + background: #1E1F25; +} +.inner { + padding: 25px; +} +.subtitle { + margin-bottom: 25px; +} + + +#block-queue { + bottom: 0; + display: inline-flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; +} + +#block-queue div { + display: flex; + justify-content: center; + align-items: center; + background: url('../assets/icon.png') var(--bg2color); + background-repeat: no-repeat; + background-size: 2.5em; + background-position-x: 1em; + background-position-y: center; + pointer-events: all; + padding: 1em 1.5em 1em 4.25em; + margin: 0 0 25px; + border: var(--border-size) solid var(--bordercolor); + border-radius: var(--border-radius); + color: var(--textcolor); + box-shadow: 0 2px 3px 1px var(--shadowcolor); + min-height: calc(2em + 4px); +} +#block-queue div:last-child { + margin-bottom: 0; +} +#block-queue div.error { + background: url('../assets/error.png') var(--bg1color); + background-repeat: no-repeat; + background-size: 2.5em; + background-position-x: 1em; + background-position-y: center; +} + +#block-queue div a, +#block-queue div a:active, +#block-queue div a:focus { + color: var(--textcolor); + text-shadow: 0 2px 3px 1px var(--shadowcolor); + -webkit-transition: var(--transition) var(--fadetime); + -moz-transition: var(--transition) var(--fadetime); + -o-transition: var(--transition) var(--fadetime); + transition: var(--transition) var(--fadetime); + cursor: pointer; +} + +#block-queue div a:hover { + text-shadow: 0 0 10px 3px var(--activeshadowcolor); + color: var(--interact); +} + +#block-queue div button, +#block-queue div button:active, +#block-queue div button:focus { + margin-left: 1em; + font-size: 1em; + padding: 0.5em 1em; + color: var(--textcolor); + background: var(--bg1color); + box-shadow: 0 2px 3px 1px var(--shadowcolor); + border: var(--border-size) solid var(--bordercolor); + border-radius: var(--border-radius); + -webkit-transition: var(--transition) var(--fadetime); + -moz-transition: var(--transition) var(--fadetime); + -o-transition: var(--transition) var(--fadetime); + transition: var(--transition) var(--fadetime); + cursor: pointer; +} + +#block-queue div button:hover { + box-shadow: 0 0 10px 3px var(--activeshadowcolor); + border-color: var(--interact); + color: var(--interact); +} + diff --git a/parsers/instructions.js b/parsers/instructions.js index 54375fd..def36f6 100644 --- a/parsers/instructions.js +++ b/parsers/instructions.js @@ -51,7 +51,7 @@ export const IgnoreTweetTypes = new Set([ "TimelineTimelineCursor", ]); -function handleTweetObject(obj, headers, config) { +function handleTweetObject(obj, config) { let ptr = obj; for (const key of UserObjectPath) { if (ptr.hasOwnProperty(key)) { @@ -62,21 +62,21 @@ function handleTweetObject(obj, headers, config) { console.error(logstr, "could not parse tweet", obj); return; } - BlockBlueVerified(ptr, headers, config); + BlockBlueVerified(ptr, config); } -export function ParseTimelineTweet(tweet, headers, config) { +export function ParseTimelineTweet(tweet, config) { if(tweet.itemType=="TimelineTimelineCursor") { return; } // Handle retweets and quoted tweets (check the retweeted user, too) if(tweet?.tweet_results?.result?.quoted_status_result) { - handleTweetObject(tweet.tweet_results.result.quoted_status_result.result, headers, config); + handleTweetObject(tweet.tweet_results.result.quoted_status_result.result, config); } else if(tweet?.tweet_results?.result?.legacy?.retweeted_status_result) { - handleTweetObject(tweet.tweet_results.result.legacy.retweeted_status_result.result, headers, config); + handleTweetObject(tweet.tweet_results.result.legacy.retweeted_status_result.result, config); } - handleTweetObject(tweet, headers, config); + handleTweetObject(tweet, config); } export function HandleInstructionsResponse(e, body, config) { @@ -124,13 +124,13 @@ export function HandleInstructionsResponse(e, body, config) { case "TimelineTimelineItem": if (tweet.content.itemContent.itemType=="TimelineTweet") { - ParseTimelineTweet(tweet.content.itemContent, e.detail.request.headers, config); + ParseTimelineTweet(tweet.content.itemContent, config); } break; case "TimelineTimelineModule": for (const innerTweet of tweet.content.items) { - ParseTimelineTweet(innerTweet.item.itemContent, e.detail.request.headers, config) + ParseTimelineTweet(innerTweet.item.itemContent, config) } break; @@ -144,9 +144,4 @@ export function HandleInstructionsResponse(e, body, config) { } } } - - if (isAddToModule) { - tweets.moduleItems = tweets.entries[0]?.content?.items || []; - delete tweets.entries; - } } diff --git a/parsers/search.js b/parsers/search.js index e3efc7d..b48ac3f 100644 --- a/parsers/search.js +++ b/parsers/search.js @@ -1,7 +1,7 @@ import { BlockBlueVerified } from "../shared.js"; // This file handles requests made pertaining to search results. -export function HandleTypeahead(e, body, config) { +export function HandleTypeahead(_, body, config) { // This endpoints appears to be extra/miscilaneous response data returned // when doing a search. it has a user list in it, so run it through the gamut! if (!body.users) { @@ -25,6 +25,6 @@ export function HandleTypeahead(e, body, config) { }, super_following: false, // meh rest_id: user.id_str, - }, e.detail.request.headers, config); + }, config); } } diff --git a/parsers/timeline.js b/parsers/timeline.js index 3fe40e3..50802ab 100644 --- a/parsers/timeline.js +++ b/parsers/timeline.js @@ -3,7 +3,7 @@ import { BlockBlueVerified } from "../shared.js"; // 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, body, config) { +export function HandleForYou(_, body, config) { // 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 @@ -28,6 +28,6 @@ export function HandleForYou(e, body, config) { }, super_following: user.ext?.superFollowMetadata?.r?.ok?.superFollowing, rest_id: user_id, - }, e.detail.request.headers, config) + }, config) } } diff --git a/popup/index.html b/popup/index.html index cbfb790..a2c36c6 100644 --- a/popup/index.html +++ b/popup/index.html @@ -8,14 +8,14 @@

🅱️lue Blocker

-

blocked users so far! ( queued)

+

blocked users so far! ( queued: view)

@@ -45,7 +45,7 @@

🅱️lue Blocker

-

allow blocking of people I follow

+

block people I follow

@@ -55,7 +55,7 @@

🅱️lue Blocker

-

allow blocking of people who follow me

+

block people who follow me

@@ -65,7 +65,7 @@

🅱️lue Blocker

-

skip users verified by other means

+

skip legacy verified users (unreliable)

@@ -75,7 +75,7 @@

🅱️lue Blocker

-

skip users verified through affiliations

+

skip users verified via businesses ()

@@ -85,10 +85,14 @@

🅱️lue Blocker

-

skip users w/ 1M+ followers

+

skip users with over 1M followers

+
+

threshold: followers

+ +
-
-
+
+

block interval

@@ -109,6 +113,16 @@

🅱️lue Blocker

+ diff --git a/popup/options.js b/popup/options.js index 92c2afe..dd35b5a 100644 --- a/popup/options.js +++ b/popup/options.js @@ -1,10 +1,12 @@ import { api, DefaultOptions } from '../constants.js'; -import { commafy } from '../utilities.js'; +import { abbreviate, commafy } from '../utilities.js'; // restore state from storage document.addEventListener("DOMContentLoaded", () => { api.storage.sync.get(DefaultOptions).then(items => { + api.action.setIcon({ path: items.suspendedBlockCollection ? "../assets/icon-128-greyscale.png" : "../assets/icon-128.png"}); document.getElementById("suspend-block-collection").checked = items.suspendedBlockCollection; + document.getElementById("show-block-popups").checked = items.showBlockPopups; document.getElementById("mute-instead-of-block").checked = items.mute; document.getElementById("block-following").checked = items.blockFollowing; @@ -12,28 +14,36 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById("skip-verified").checked = items.skipVerified; document.getElementById("skip-affiliated").checked = items.skipAffiliated; document.getElementById("skip-1mplus").checked = items.skip1Mplus; + document.getElementById("skip-follower-count").value = items.skipFollowerCount; + document.getElementById("skip-follower-count-option").style.display = items.skip1Mplus ? null : "none"; + document.getElementById("skip-follower-count-value").textContent = abbreviate(items.skipFollowerCount); document.getElementById("block-nft-avatars").checked = items.blockNftAvatars; + document.getElementById("block-interval").value = items.blockInterval; - document.getElementById("block-interval-value").innerText = items.blockInterval.toString() + "s"; + document.getElementById("block-interval-value").textContent = items.blockInterval.toString() + "s"; + + document.getElementById("popup-timer-slider").style.display = items.showBlockPopups ? null : "none"; + document.getElementById("popup-timer").value = items.popupTimer; + document.getElementById("popup-timer-value").textContent = items.popupTimer.toString() + "s"; }); }); // set the block value immediately api.storage.local.get({ BlockCounter: 0, BlockQueue: [] }).then(items => { - document.getElementById("blocked-users-count").innerText = commafy(items.BlockCounter); - document.getElementById("blocked-user-queue-length").innerText = commafy(items.BlockQueue.length); + document.getElementById("blocked-users-count").textContent = commafy(items.BlockCounter); + document.getElementById("blocked-user-queue-length").textContent = commafy(items.BlockQueue.length); }); api.storage.local.onChanged.addListener(items => { if (items.hasOwnProperty("BlockCounter")) { - document.getElementById("blocked-users-count").innerText = commafy(items.BlockCounter.newValue); + document.getElementById("blocked-users-count").textContent = commafy(items.BlockCounter.newValue); } if (items.hasOwnProperty("BlockQueue")) { - document.getElementById("blocked-user-queue-length").innerText = commafy(items.BlockQueue.newValue.length); + document.getElementById("blocked-user-queue-length").textContent = commafy(items.BlockQueue.newValue.length); } // if we want to add other values, add them here }); -document.getElementById("version").innerText = "v" + api.runtime.getManifest().version; +document.getElementById("version").textContent = "v" + api.runtime.getManifest().version; document.getElementById("suspend-block-collection").addEventListener("input", e => { api.storage.sync.set({ @@ -41,7 +51,8 @@ document.getElementById("suspend-block-collection").addEventListener("input", e }).then(() => { // Update status to let user know options were saved. const status = document.getElementById("suspend-block-collection-status"); - status.textContent = "saved"; + status.textContent = e.target.checked ? "paused" : "resumed"; + api.action.setIcon({ path: e.target.checked ? "../assets/icon-128-greyscale.png" : "../assets/icon-128.png"}); setTimeout(() => status.textContent = null, 1000); }); }); @@ -51,6 +62,7 @@ document.getElementById("show-block-popups").addEventListener("input", e => { showBlockPopups: e.target.checked, }).then(() => { // Update status to let user know options were saved. + document.getElementById("popup-timer-slider").style.display = e.target.checked ? null : "none"; const status = document.getElementById("show-block-popups-status"); status.textContent = "saved"; setTimeout(() => status.textContent = null, 1000); @@ -119,6 +131,20 @@ document.getElementById("skip-1mplus").addEventListener("input", e => { // Update status to let user know options were saved. const status = document.getElementById("skip-1mplus-status"); status.textContent = "saved"; + document.getElementById("skip-follower-count-option").style.display = e.target.checked ? null : "none"; + setTimeout(() => status.textContent = null, 1000); + }); +}); + +document.getElementById("skip-follower-count").addEventListener("input", e => { + const value = parseInt(e.target.value); + document.getElementById("skip-follower-count-value").textContent = abbreviate(value); + api.storage.sync.set({ + skipFollowerCount: value, + }).then(() => { + // Update status to let user know options were saved. + const status = document.getElementById("skip-follower-count-status"); + status.textContent = "saved"; setTimeout(() => status.textContent = null, 1000); }); }); @@ -136,7 +162,7 @@ document.getElementById("block-nft-avatars").addEventListener("input", e => { const blockIntervalValueElement = document.getElementById("block-interval-value"); document.getElementById("block-interval").addEventListener("input", e => { - blockIntervalValueElement.innerText = e.target.value.toString() + "s"; + blockIntervalValueElement.textContent = e.target.value.toString() + "s"; }); document.getElementById("block-interval").addEventListener("change", e => { @@ -149,3 +175,19 @@ document.getElementById("block-interval").addEventListener("change", e => { setTimeout(() => status.textContent = null, 1000); }); }); + +const popupTimerValueElement = document.getElementById("popup-timer-value"); +document.getElementById("popup-timer").addEventListener("input", e => { + popupTimerValueElement.textContent = e.target.value.toString() + "s"; +}); + +document.getElementById("popup-timer").addEventListener("change", e => { + api.storage.sync.set({ + popupTimer: parseInt(e.target.value), + }).then(() => { + // Update status to let user know options were saved. + const status = document.getElementById("popup-timer-status"); + status.textContent = "saved"; + setTimeout(() => status.textContent = null, 1000); + }); +}); diff --git a/popup/style.css b/popup/style.css index 9c38ba1..bba9cc3 100644 --- a/popup/style.css +++ b/popup/style.css @@ -17,6 +17,11 @@ html { --screen-cover: #00000080; --border-size: 1px; --border-radius: 3px; + + border: var(--border-size) solid var(--bordercolor); +} +script { + display: none; } html * { font-family: Bitstream Vera Sans, DejaVu Sans, Arial, Helvetica, sans-serif; @@ -32,6 +37,13 @@ body { padding: 0; font-size: 1.1rem; } +body > div { + margin-bottom: 1em; +} +body > div:last-child, .last { + margin-bottom: 0; +} + #version { color: var(--subtle); @@ -62,6 +74,12 @@ a, input, label, textarea { -o-transition: var(--transition) var(--fadetime); transition: var(--transition) var(--fadetime); } +a:link, a:visited +{ color: var(--textcolor); } +a:hover { + color: var(--interact) !important; + opacity: 1 !important; +} .option p { text-align: center; @@ -70,15 +88,10 @@ a, input, label, textarea { .option { display: flex; - margin-bottom: 1em; justify-content: space-between; } -.last { - margin-bottom: 0; -} - -.option input { +input[type="checkbox"] { position: absolute; display: none; top: -100vh; @@ -146,6 +159,42 @@ input:checked + label div.checkmark div, label.checked div.checkmark div { transform: rotate(45deg); } +#skip-follower-count-option { + margin-left: 2em; +} +input#skip-follower-count { + text-align: right; + width: 7em; + height: 1em; + position: relative; + top: -0.1em; + display: inline-block; + padding: 0.5em 1em; + background: var(--bg0color); + color: var(--textcolor); + border: var(--border-size) solid var(--bordercolor); + box-shadow: 0 2px 3px 1px var(--shadowcolor); + border-radius: var(--border-radius); + -webkit-transition: var(--transition) var(--fadetime); + -moz-transition: var(--transition) var(--fadetime); + -o-transition: var(--transition) var(--fadetime); + transition: var(--transition) var(--fadetime); +} +input#skip-follower-count:hover { + color: var(--interact); + border-color: var(--borderhover); + box-shadow: 0 0 10px 3px var(--activeshadowcolor); +} +input#skip-follower-count:focus { + color: var(--textcolor); +} + +.gold-check { + margin: -0.2em 0; + width: 1.2em; + height: 1.2em; +} + .blocked-users-count { text-align: center; color: var(--subtle); diff --git a/script.js b/script.js index caab278..1aad09a 100644 --- a/script.js +++ b/script.js @@ -1,4 +1,4 @@ -import { ClearCache, ErrorEvent, EventKey } from './shared.js'; +import { ClearCache, ErrorEvent, EventKey, SetHeaders } from './shared.js'; import { api, DefaultOptions } from './constants.js'; import { HandleInstructionsResponse } from './parsers/instructions.js'; import { HandleForYou } from './parsers/timeline.js'; @@ -22,6 +22,14 @@ document.body.appendChild(t); document.addEventListener("blue-blocker-event", function (e) { // TODO: we may want to seriously consider clearing the cache on a much less frequent // cadence since we're no longer able to block users immediately and need the queue + + // TODO: probably also check status code here so that we're not parsing error responses + // for no reason + + if (e.detail.status < 300) { + SetHeaders(e.detail.request.headers); + } + ClearCache(); api.storage.sync.get(DefaultOptions).then(config => { const body_str = e.detail.body; diff --git a/shared.js b/shared.js index 5f387b8..aa83968 100644 --- a/shared.js +++ b/shared.js @@ -1,6 +1,7 @@ import { BlockCounter } from "./models/block_counter.js"; import { BlockQueue } from "./models/block_queue.js"; import { QueueConsumer } from "./models/queue_consumer.js"; +import { commafy, FormatLegacyName } from "./utilities.js"; import { api, DefaultOptions, logstr, Headers, ReasonBlueVerified, ReasonNftAvatar, ReasonMap } from "./constants.js"; // Define constants that shouldn't be exported to the rest of the addon @@ -13,67 +14,82 @@ export function SetOptions(items) { options = items; } -function unblockUser(user, user_id, headers, reason, attempt = 1) { +export function SetHeaders(headers) { + api.storage.local.set({ headers }); +} + +function unblockUser(user, user_id, reason, attempt = 1) { api.storage.sync.get({ unblocked: { } }).then(items => { items.unblocked[String(user_id)] = null; api.storage.sync.set(items); }); - const formdata = new FormData(); - formdata.append("user_id", user_id); + api.storage.local.get({ headers: null }).then((items) => { + const headers = items.headers; + const formdata = new FormData(); + formdata.append("user_id", user_id); - const ajax = new XMLHttpRequest(); + const ajax = new XMLHttpRequest(); - ajax.addEventListener('load', event => { - if (event.target.status === 403) { - // user has been logged out, we need to stop queue and re-add - console.log(logstr, "user is logged out, failed to unblock user."); - return; - } - else if (event.target.status >= 300) { - queue.push({user, user_id, headers, reason}); - console.error(logstr, `failed to unblock ${formatLegacyName(user)}:`, user, event); + ajax.addEventListener('load', event => { + if (event.target.status === 403) { + // user has been logged out, we need to stop queue and re-add + console.log(logstr, "user is logged out, failed to unblock user."); + } + else if (event.target.status === 404) { + // notice the wording here is different than the blocked 404. the difference is that if the user + // is unbanned, they will still be blocked and we want the user to know about that + + const t = document.createElement("div"); + t.className = "toast"; + t.innerText = `could not unblock @${user.legacy.screen_name}, user has been suspended or no longer exists.`; + const ele = document.getElementById("injected-blue-block-toasts"); + ele.appendChild(t); + setTimeout(() => ele.removeChild(t), options.popupTimer * 1000); + console.log(logstr, `failed to unblock ${FormatLegacyName(user)}, user no longer exists`); + } + else if (event.target.status >= 300) { + console.error(logstr, `failed to unblock ${FormatLegacyName(user)}:`, user, event); + } + else { + const t = document.createElement("div"); + t.className = "toast"; + t.innerText = `unblocked @${user.legacy.screen_name}, they won't be blocked again.`; + const ele = document.getElementById("injected-blue-block-toasts"); + ele.appendChild(t); + setTimeout(() => ele.removeChild(t), options.popupTimer * 1000); + console.log(logstr, `unblocked ${FormatLegacyName(user)}`); + } + }); + ajax.addEventListener('error', error => { + if (attempt < 3) { + unblockUser(user, user_id, reason, attempt + 1); + } else { + console.error(logstr, `failed to unblock ${FormatLegacyName(user)}:`, user, error); + } + }); + + if (options.mute) { + ajax.open('POST', "https://twitter.com/i/api/1.1/mutes/users/destroy.json"); } else { - const t = document.createElement("div"); - t.className = "toast"; - t.innerText = `unblocked @${user.legacy.screen_name}, they won't be blocked again.`; - const ele = document.getElementById("injected-blue-block-toasts"); - ele.appendChild(t); - setTimeout(() => ele.removeChild(t), 30e3); - console.log(logstr, `unblocked ${formatLegacyName(user)}`); + ajax.open('POST', "https://twitter.com/i/api/1.1/blocks/destroy.json"); } - }); - ajax.addEventListener('error', error => { - console.error(logstr, 'error:', error); - if (attempt < 3) { - unblockUser(user, user_id, headers, reason, attempt + 1); - } else { - console.error(logstr, `failed to unblock ${formatLegacyName(user)}:`, user, error); + for (const header of Headers) { + ajax.setRequestHeader(header, headers[header]); } - }); - - if (options.mute) { - ajax.open('POST', "https://twitter.com/i/api/1.1/mutes/users/destroy.json"); - } - else { - ajax.open('POST', "https://twitter.com/i/api/1.1/blocks/destroy.json"); - } - - for (const header of Headers) { - ajax.setRequestHeader(header, headers[header]); - } - // attempt to manually set the csrf token to the current active cookie - const csrf = CsrfTokenRegex.exec(document.cookie); - if (csrf) { - ajax.setRequestHeader("x-csrf-token", csrf[1]); - } - else { - // default to the request's csrf token - ajax.setRequestHeader("x-csrf-token", headers["x-csrf-token"]); - } - ajax.send(formdata); + // attempt to manually set the csrf token to the current active cookie + const csrf = CsrfTokenRegex.exec(document.cookie); + if (csrf) { + ajax.setRequestHeader("x-csrf-token", csrf[1]); + } + else { + // default to the request's csrf token + ajax.setRequestHeader("x-csrf-token", headers["x-csrf-token"]); + } + ajax.send(formdata); + }); } export const EventKey = "MultiTabEvent"; @@ -89,21 +105,21 @@ api.storage.local.onChanged.addListener(items => { switch (e.type) { case UserBlockedEvent: if (options.showBlockPopups) { - const { user, user_id, headers, reason } = e; + const { user, user_id, reason } = e; const t = document.createElement("div"); t.className = "toast"; const name = user.legacy.name.length > 25 ? user.legacy.name.substring(0, 23).trim() + "..." : user.legacy.name; t.innerHTML = `blocked ${name} (@${user.legacy.screen_name})`; const b = document.createElement("button"); b.onclick = () => { - unblockUser(user, user_id, headers, reason); + unblockUser(user, user_id, reason); t.removeChild(b); }; b.innerText = "undo"; t.appendChild(b); const ele = document.getElementById("injected-blue-block-toasts"); ele.appendChild(t); - setTimeout(() => ele.removeChild(t), 30e3); + setTimeout(() => ele.removeChild(t), options.popupTimer * 1000); } break; @@ -118,7 +134,7 @@ api.storage.local.onChanged.addListener(items => { const ele = document.getElementById("injected-blue-block-toasts"); ele.appendChild(t); - setTimeout(() => ele.removeChild(t), 60e3); + setTimeout(() => ele.removeChild(t), options.popupTimer * 2000); break; default: @@ -133,13 +149,13 @@ export function ClearCache() { blockCache.clear(); } -function queueBlockUser(user, user_id, headers, reason) { +function queueBlockUser(user, user_id, reason) { if (blockCache.has(user_id)) { return; } blockCache.add(user_id); - queue.push({user, user_id, headers, reason}); - console.log(logstr, `queued ${formatLegacyName(user)} for a block due to ${ReasonMap[reason]}.`); + queue.push({user, user_id, reason}); + console.log(logstr, `queued ${FormatLegacyName(user)} for a block due to ${ReasonMap[reason]}.`); consumer.start(); } @@ -153,8 +169,8 @@ function checkBlockQueue() { consumer.stop(); return; } - const {user, user_id, headers, reason} = item; - blockUser(user, user_id, headers, reason); + const {user, user_id, reason} = item; + blockUser(user, user_id, reason); }).catch(error => api.storage.local.set({ [EventKey]: { type: ErrorEvent, message: "unexpected error occurred while processing block queue", detail: { error, event } } })); } @@ -165,67 +181,72 @@ const consumer = new QueueConsumer(api.storage.local, checkBlockQueue, async s = consumer.start(); const CsrfTokenRegex = /ct0=\s*(\w+);/; -function blockUser(user, user_id, headers, reason, attempt=1) { - const formdata = new FormData(); - formdata.append("user_id", user_id); +function blockUser(user, user_id, reason, attempt=1) { + api.storage.local.get({ headers: null }).then((items) => { + const headers = items.headers; + const formdata = new FormData(); + formdata.append("user_id", user_id); + + const ajax = new XMLHttpRequest(); + + ajax.addEventListener('load', event => { + if (event.target.status === 403) { + // user has been logged out, we need to stop queue and re-add + consumer.stop(); + queue.push({user, user_id, reason}); + console.log(logstr, "user is logged out, queue consumer has been halted."); + } + else if (event.target.status === 404) { + console.log(logstr, `did not block ${FormatLegacyName(user)}, user no longer exists`); + } + else if (event.target.status >= 300) { + queue.push({user, user_id, reason}); + console.error(logstr, `failed to block ${FormatLegacyName(user)}:`, user, event); + } + else { + blockCounter.increment(); + console.log(logstr, `blocked ${FormatLegacyName(user)} due to ${ReasonMap[reason]}.`); + api.storage.local.set({ [EventKey]: { type: UserBlockedEvent, user, user_id, reason } }) + } + }); + ajax.addEventListener('error', error => { + console.error(logstr, 'error:', error); + + if (attempt < 3) { + blockUser(user, user_id, reason, attempt + 1); + } else { + queue.push({user, user_id, reason}); + console.error(logstr, `failed to block ${FormatLegacyName(user)}:`, user, error); + } + }); - const ajax = new XMLHttpRequest(); + if (options.mute) { + ajax.open('POST', "https://twitter.com/i/api/1.1/mutes/users/create.json"); + } + else { + ajax.open('POST', "https://twitter.com/i/api/1.1/blocks/create.json"); + } - ajax.addEventListener('load', event => { - if (event.target.status === 403) { - // user has been logged out, we need to stop queue and re-add - consumer.stop(); - queue.push({user, user_id, headers, reason}); - console.log(logstr, "user is logged out, queue consumer has been halted."); - return; + for (const header of Headers) { + ajax.setRequestHeader(header, headers[header]); } - else if (event.target.status >= 300) { - queue.push({user, user_id, headers, reason}); - console.error(logstr, `failed to block ${formatLegacyName(user)}:`, user, event); + + // attempt to manually set the csrf token to the current active cookie + const csrf = CsrfTokenRegex.exec(document.cookie); + if (csrf) { + ajax.setRequestHeader("x-csrf-token", csrf[1]); } else { - blockCounter.increment(); - console.log(logstr, `blocked ${formatLegacyName(user)} due to ${ReasonMap[reason]}.`); - api.storage.local.set({ [EventKey]: { type: UserBlockedEvent, user, user_id, headers, reason } }) + // default to the request's csrf token + ajax.setRequestHeader("x-csrf-token", headers["x-csrf-token"]); } + ajax.send(formdata); }); - ajax.addEventListener('error', error => { - console.error(logstr, 'error:', error); - - if (attempt < 3) { - blockUser(user, user_id, headers, reason, attempt + 1); - } else { - queue.push({user, user_id, headers, reason}); - console.error(logstr, `failed to block ${formatLegacyName(user)}:`, user, error); - } - }); - - if (options.mute) { - ajax.open('POST', "https://twitter.com/i/api/1.1/mutes/users/create.json"); - } - else { - ajax.open('POST', "https://twitter.com/i/api/1.1/blocks/create.json"); - } - - for (const header of Headers) { - ajax.setRequestHeader(header, headers[header]); - } - - // attempt to manually set the csrf token to the current active cookie - const csrf = CsrfTokenRegex.exec(document.cookie); - if (csrf) { - ajax.setRequestHeader("x-csrf-token", csrf[1]); - } - else { - // default to the request's csrf token - ajax.setRequestHeader("x-csrf-token", headers["x-csrf-token"]); - } - ajax.send(formdata); } const blockableAffiliateLabels = new Set(["AutomatedLabel"]); const blockableVerifiedTypes = new Set(["Business"]); -export function BlockBlueVerified(user, headers, config) { +export function BlockBlueVerified(user, config) { // We're not currently adding anything to the queue so give up. if (config.suspendedBlockCollection) { return; @@ -236,7 +257,7 @@ export function BlockBlueVerified(user, headers, config) { return; } - const formattedUserName = formatLegacyName(user); + const formattedUserName = FormatLegacyName(user); // since we can be fairly certain all user objects will be the same, break this into a separate function if (user.legacy?.verified_type && !blockableVerifiedTypes.has(user.legacy.verified_type)) { @@ -280,12 +301,12 @@ export function BlockBlueVerified(user, headers, config) { } else if ( // verified by follower count - config.skip1Mplus && user.legacy?.followers_count > 1000000 + config.skip1Mplus && user.legacy?.followers_count > (config.skipFollowerCount) ) { - console.log(logstr, `did not block Twitter Blue verified user ${formattedUserName} because they have over a million followers and Elon is an idiot.`); + console.log(logstr, `did not block Twitter Blue verified user ${formattedUserName} because they have over ${commafy(config.skipFollowerCount)} followers and Elon is an idiot.`); } else { - queueBlockUser(user, String(user.rest_id), headers, ReasonBlueVerified); + queueBlockUser(user, String(user.rest_id), ReasonBlueVerified); } } else if (config.blockNftAvatars && user.has_nft_avatar) { @@ -306,9 +327,3 @@ export function BlockBlueVerified(user, headers, config) { } } } - -function formatLegacyName(user) { - const legacyName = user.legacy?.name; - const screenName = user.legacy?.screen_name; - return `${legacyName} (@${screenName})`; -} \ No newline at end of file diff --git a/utilities.js b/utilities.js index 04037ee..ff70cde 100644 --- a/utilities.js +++ b/utilities.js @@ -25,3 +25,9 @@ export function commafy(x) // due to floating point bullshit, but it's good enough const MaxId = 0xffffffffffffffff; export const RefId = () => Math.round(Math.random() * MaxId); + +export function FormatLegacyName(user) { + const legacyName = user.legacy?.name; + const screenName = user.legacy?.screen_name; + return `${legacyName} (@${screenName})`; +}