From ece11a8352cb950a7a0d2388741314e5dac42d18 Mon Sep 17 00:00:00 2001 From: wagesj45 Date: Tue, 25 Aug 2026 01:02:35 -0500 Subject: [PATCH] Fix Threads post text extraction --- README.md | 4 ++-- src/content/definitions/default.json | 17 +++++++++++++++++ tests/definition-engine.test.ts | 18 ++++++++++++++++++ tests/fixtures/threads-feed.html | 15 +++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/threads-feed.html diff --git a/README.md b/README.md index 8b4fba3..bc93800 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ VibeGuard is a local-first browser extension that hides toxic social-media posts - Configurable toxicity threshold and filtering mode: collapse posts with a Show button or hide them completely. - Optional toxicity scores on filtered-post placeholders. - Declarative site definitions with URL patterns, selectors, and stable identifiers. -- Built-in support for the official Mastodon web interface. +- Built-in support for the official Mastodon web interface and Threads. - Versioned JSON definition collections with weekly subscriptions and manual refresh. - Toolbar popup with current-site status, matched definition, pause/resume control, and settings access. - Firefox Manifest V2 and Chromium Manifest V3 builds. @@ -47,7 +47,7 @@ Preparation writes the Transformers.js-compatible files and `model-manifest.json If the model uses generic labels such as `LABEL_0` and `LABEL_1`, pass `--toxic-index` and `--non-toxic-index` to `prepare`; the command refuses to guess an ambiguous mapping. -VibeGuard injects a lightweight content script on ordinary web pages, but reads and processes content only when a validated site definition matches the page. The extension includes a bundled definition collection for the official Mastodon web interface. The default subscription follows the repository's raw JSON collection and refreshes weekly, while the bundled copy remains available offline. Additional JSON subscriptions, local overrides, and disabled definitions are managed in the options page. Definitions are declarative selectors and URL patterns, never executable code. +VibeGuard injects a lightweight content script on ordinary web pages, but reads and processes content only when a validated site definition matches the page. The extension includes a bundled definition collection for the official Mastodon web interface and Threads. The Threads definition covers both `threads.com` and legacy `threads.net` URLs using semantic and data-attribute selectors. The default subscription follows the repository's raw JSON collection and refreshes weekly, while the bundled copy remains available offline. Additional JSON subscriptions, local overrides, and disabled definitions are managed in the options page. Definitions are declarative selectors and URL patterns, never executable code. ## Runtime architecture diff --git a/src/content/definitions/default.json b/src/content/definitions/default.json index 52cd3a7..f5af089 100644 --- a/src/content/definitions/default.json +++ b/src/content/definitions/default.json @@ -16,6 +16,23 @@ "idAttributes": ["data-id"], "permalinkSelectors": ["a.status__relative-time[href]", "a.detailed-status__datetime[href]", "a.u-url.u-uid[href]"] } + }, + { + "id": "threads", + "name": "Threads", + "urlPatterns": [ + "https://threads.com/*", + "https://www.threads.com/*", + "https://threads.net/*", + "https://www.threads.net/*" + ], + "requiredSelectors": ["[data-pressable-container=\"true\"], [role=\"article\"], article"], + "post": { + "rootSelectors": ["[data-pressable-container=\"true\"]", "[role=\"article\"]", "article"], + "textSelectors": ["span[dir=\"auto\"]:not(a span):not(time span):not([role=\"button\"] span)"], + "excludedSelectors": ["button", "[role=\"button\"]", "time", "svg"], + "permalinkSelectors": ["a[href*=\"/post/\"]"] + } } ] } diff --git a/tests/definition-engine.test.ts b/tests/definition-engine.test.ts index 2f32356..b096446 100644 --- a/tests/definition-engine.test.ts +++ b/tests/definition-engine.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; import { DefinitionEngine, matchesUrlPattern, selectDefinition } from "../src/content/definition-engine"; import { BUILTIN_DEFINITIONS } from "../src/content/definitions"; import type { SiteDefinition } from "../src/shared/types"; @@ -59,4 +60,21 @@ describe("DefinitionEngine", () => { stop(); expect(observed).toContain("content warning revealed text"); }); + + it("discovers Threads posts from stable semantic/data selectors and permalink IDs", () => { + document.body.innerHTML = readFileSync("tests/fixtures/threads-feed.html", "utf8"); + const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; + const posts = new DefinitionEngine(threads).discover(document); + expect(posts.map((post) => post.id)).toEqual(["threads:http://localhost:3000/@alice/post/ABC123", "threads:http://localhost:3000/@bob/post/DEF456"]); + expect(posts.map((post) => post.text)).toEqual(["first threads post with visible text", "a reply in the conversation"]); + }); + + it("selects Threads on current and legacy domains", () => { + const threads = BUILTIN_DEFINITIONS.find((candidate) => candidate.id === "threads")!; + document.body.innerHTML = '
'; + for (const url of ["https://threads.com/home", "https://www.threads.com/@alice/post/ABC123", "https://threads.net/home", "https://www.threads.net/@alice/post/ABC123"]) { + expect(selectDefinition([threads], url)).toBe(threads); + } + expect(selectDefinition([threads], "https://example.com/home")).toBeUndefined(); + }); }); diff --git a/tests/fixtures/threads-feed.html b/tests/fixtures/threads-feed.html new file mode 100644 index 0000000..c40e2b6 --- /dev/null +++ b/tests/fixtures/threads-feed.html @@ -0,0 +1,15 @@ +
+
+ alice + + First Threads post with visible text + + +
+
+ bob + + A reply in the conversation + +
+