您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
This automatically selects submission notifications, that are advertisements.
// ==UserScript== // @name Auto-select advertisements // @namespace https://github.com/f1r3w4rr10r/fa-utils // @version 0.2.2 // @description This automatically selects submission notifications, that are advertisements. // @author f1r3w4rr10r // @match https://www.furaffinity.net/msg/submissions/* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @license MIT // @grant none // ==/UserScript== (async function () { "use strict"; const DEFINITELY_AD_THRESHOLD = 50; const AMBIGUOUS_AD_THRESHOLD = 25; // The second "c" is a Cyrillic "s"; const COMMISSION_REGEX_STRING = "[cс]omm?(?:ission)?s?"; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_COMMISSION_SELECTOR = { operator: "or", operands: [ { target: "name", pattern: /\bauction\b/i }, { target: "name", pattern: new RegExp(`(?:^|\\W)${COMMISSION_REGEX_STRING}\\b`, "i"), }, { target: "name", pattern: /\bwing.its?\b/i }, ], }; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_DISCOUNTS_SELECTOR = { operator: "or", operands: [ { target: "name", pattern: /\bdiscount\b/i }, { target: "name", pattern: /\bsale\b/i }, ], }; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_MEMBERSHIPS_SELECTOR = { operator: "or", operands: [ { target: "name", pattern: /\bboosty\b/i }, { target: "name", pattern: /\bp[@a]treon\b/i }, { target: "name", pattern: /\bsub(?:scribe)?\s*star\b/i }, { target: "name", pattern: /\bsupporters?\b/i }, { target: "tags", pattern: /\bboosty\b/i }, { target: "tags", pattern: /\bp[@a]treon\b/i }, { target: "tags", pattern: /\bsub(?:scribe)?\s*star\b/i }, ], }; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_PRICE_LIST_SELECTOR = { operator: "or", operands: [ { target: "name", pattern: /\bprice\s+(?:list|sheet)\b/i }, { target: "name", pattern: /\bcommission\s+info/i }, ], }; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_RAFFLES_SELECTOR = { target: "name", pattern: /\braffle\b/i }; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_SHOPS_SELECTOR = { operator: "or", operands: [ { target: "name", pattern: /\bshop\b/i }, { target: "name", pattern: /\bfurplanet\b/i }, { target: "description", pattern: /\bfurplanet\b/i }, { target: "tags", pattern: /\bfurplanet\b/i }, ], }; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_SLOTS_SELECTOR = { operator: "or", operands: [ { target: "name", pattern: /\b(?:multi)?slots?\b/i }, { target: "name", pattern: /\bart\s+marathon\b/i }, { target: "description", pattern: /\d\s+slots\b/i }, ], }; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_STREAM_SELECTOR = { target: "name", pattern: /\b(?:live)?stream\b/i, }; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_TEASER_SELECTOR = { operator: "or", operands: [ { target: "name", pattern: /\bpreview\b/i }, { target: "name", pattern: /\bspoiler\b/i }, { target: "name", pattern: /\bteaser\b/i }, ], }; /** @type {AdSelector | SelectorCombiner} */ const AMBIGUOUS_YCH_SELECTOR = { operator: "or", operands: [ { target: "name", pattern: /\by\s*c\s*h\s*s?\b/i, }, { target: "description", pattern: /\by\s*c\s*h\s*s?\b/i, }, ], }; /** @type {AdRules} */ const adRules = [ { ruleName: "adoptables", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "or", operands: [ { target: "name", pattern: /\badopt(?:(?:able)?s?|ing|ion)\b/i }, { target: "name", pattern: /\bcustoms\b/i }, { target: "tags", pattern: /\badopt(?:(?:able)?s?|ing|ion)\b/i }, { target: "description", pattern: /\badopt(?:(?:able)?s?|ing|ion)\b/i, }, ], }, }, { ruleName: "commission ads (ambiguous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_COMMISSION_SELECTOR, }, { ruleName: "commission ads (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "and", operands: [ AMBIGUOUS_COMMISSION_SELECTOR, { operator: "or", operands: [ { target: "name", pattern: /\bclosed\b/i }, { target: "name", pattern: /\bhalfbody\b/i }, { target: "name", pattern: /\bopen(?:ed)?\b/i }, { target: "name", pattern: /\bsale\b/i }, { target: "name", pattern: /\bslots?\b/i }, { target: "name", pattern: /\bych\b/i }, ], }, ], }, }, { ruleName: "convention dealers", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "or", operands: [ { target: "tags", pattern: /\bdealers?\s+den\b/i }, { target: "description", pattern: /\bdealers?\s+den\b/i }, ], }, }, { ruleName: "discounts (ambiguous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_DISCOUNTS_SELECTOR, }, { ruleName: "discounts (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "and", operands: [ AMBIGUOUS_DISCOUNTS_SELECTOR, { operator: "or", operands: [ { target: "name", pattern: /\$/ }, { target: "name", pattern: /\bbase\b/i }, { target: "name", pattern: /\bclaimed\b/i }, { target: "name", pattern: /\b(?:multi)?slot\b/i }, { target: "name", pattern: /\boffer\b/i }, { target: "name", pattern: /\bprice\b/i }, ], }, ], }, }, { ruleName: "memberships (ambgiuous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_MEMBERSHIPS_SELECTOR, }, { ruleName: "memberships (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "and", operands: [ { operator: "or", operands: [ ...AMBIGUOUS_MEMBERSHIPS_SELECTOR.operands, { target: "description", pattern: /\bboosty\b/i }, { target: "description", pattern: /\bp[@a]treon\b/i }, { target: "description", pattern: /\bsub(?:scribe)?\s*star\b/i }, ], }, { operator: "or", operands: [ { target: "name", pattern: /\bdiscount\b/i }, { target: "name", pattern: /\b(?:available|now)\s+on\b/i }, { target: "name", pattern: /\bposted\s+to\b/i }, { target: "name", pattern: /\bpreview\b/i }, { target: "name", pattern: /\bteaser?\b/i }, { target: "name", pattern: /\bup\s+on\b/i }, { target: "name", pattern: /\bis\s+up\b/i }, { target: "description", pattern: /\bup\s+on\b/i }, { target: "description", pattern: /\bfull.*\s+on\b/i }, { target: "description", pattern: /\bexclusive(?:ly)?.+for\b/i }, ], }, ], }, }, { ruleName: "price lists (ambiguous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_PRICE_LIST_SELECTOR, }, { ruleName: "price lists (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "and", operands: [ AMBIGUOUS_PRICE_LIST_SELECTOR, { operator: "or", operands: [], }, ], }, }, { ruleName: "raffles (ambiguous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_RAFFLES_SELECTOR, }, { ruleName: "raffles (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "and", operands: [ AMBIGUOUS_RAFFLES_SELECTOR, { target: "name", pattern: /\bwinners?\b/i }, ], }, }, { ruleName: "reminders", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "or", operands: [ { target: "name", pattern: /\breminder+\b/i }, { target: "name", pattern: /^REM$/ }, ], }, }, { ruleName: "shops (ambiguous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_SHOPS_SELECTOR, }, { ruleName: "shops (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "or", operands: [ { operator: "and", operands: [ AMBIGUOUS_SHOPS_SELECTOR, { operator: "or", operands: [ { target: "name", pattern: /\bprint\b/i }, { target: "description", pattern: /\bup\s+on\b/i }, ], }, ], }, { target: "tags", pattern: /\bmerch\b/i }, ], }, }, { ruleName: "slots (ambiguous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_SLOTS_SELECTOR, }, { ruleName: "slots (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "and", operands: [ AMBIGUOUS_SLOTS_SELECTOR, { operator: "or", operands: [ { target: "name", pattern: /\bavailable\b/i }, { target: "name", pattern: /\bopen\b/i }, { target: "name", pattern: /\bsketch\b/i }, ], }, ], }, }, { ruleName: "stream ads (ambiguous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_STREAM_SELECTOR, }, { ruleName: "stream ads (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "or", operands: [ { target: "name", pattern: /\bpicarto\.tv\b/i }, { target: "name", pattern: /\bstreaming\b/i }, { operator: "and", operands: [ AMBIGUOUS_STREAM_SELECTOR, { operator: "or", operands: [ { target: "name", pattern: /\blive\b/i }, { target: "name", pattern: /\boffline\b/i }, { target: "name", pattern: /\bonline\b/i }, { target: "name", pattern: /\bpreorders?\b/i }, { target: "name", pattern: /\bslots?\b/i }, { target: "name", pattern: /\bup\b/i }, ], }, ], }, { operator: "and", operands: [ { target: "name", pattern: /\bstream\b/i }, { target: "tags", pattern: /\bstream\b/i }, ], }, ], }, }, { ruleName: "teasers (ambiguous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_TEASER_SELECTOR, }, { ruleName: "teasers (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "and", operands: [ AMBIGUOUS_TEASER_SELECTOR, { target: "description", pattern: /\b(?:available|n[eo]w|out)\b.*\bon\b.*\b(?:boosty|p[@a]treon|sub(?:scribe)?\s*star)\b/i, }, ], }, }, { ruleName: "WIPs", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "and", operands: [ { target: "name", pattern: /\bwip\b/i }, { target: "tags", pattern: /\bwip\b/i }, ], }, }, { ruleName: "YCHs (ambiguous)", value: AMBIGUOUS_AD_THRESHOLD, selector: AMBIGUOUS_YCH_SELECTOR, }, { ruleName: "YCHs (definitive)", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "and", operands: [ { operator: "or", operands: [ AMBIGUOUS_YCH_SELECTOR, { target: "name", pattern: /^closed$/i }, ], }, { operator: "or", operands: [ { target: "name", pattern: /\bauction\b/i }, { target: "name", pattern: /\bavailable\b/i }, { target: "name", pattern: /\bdiscount\b/i }, { target: "name", pattern: /\bmultislot\b/i }, { target: "name", pattern: /\bo\s+p\s+e\s+n\b/i }, { target: "name", pattern: /\bprice\b/i }, { target: "name", pattern: /\bpreview\b/i }, { target: "name", pattern: /\braffle\b/i }, { target: "name", pattern: /\brem(?:ind(?:er)?)?\d*\b/i }, { target: "name", pattern: /\brmd\b/i }, { target: "name", pattern: /\bsale\b/i }, { target: "name", pattern: /\bslots?\b/i }, { target: "name", pattern: /\bsold\b/i }, { target: "name", pattern: /\btaken\b/i }, { target: "name", pattern: /\busd\b/i }, { target: "name", pattern: /\b\$\d+\b/i }, { target: "tags", pattern: /^$/ }, { target: "tags", pattern: /\bauction\b/i }, { target: "tags", pattern: /\bsale\b/i }, { target: "tags", pattern: /\bych\b/i }, { target: "description", pattern: /\bSB|starting\s+bid\b/i }, { target: "description", pattern: /https?:\/\/ych\.art\/auction\/\d+/i, }, ], }, ], }, }, { ruleName: "misc ambiguous", value: AMBIGUOUS_AD_THRESHOLD, selector: { operator: "or", operands: [ { target: "name", pattern: /\bart\s+pack\b/i }, { target: "name", pattern: /\bart.+earlier\b/i }, { target: "name", pattern: /\bavailable\s+now\b/i }, { target: "name", pattern: /\bclosed\b/i }, { target: "name", pattern: /\bopen\b/i }, { target: "name", pattern: /\bpoll\b/i }, { target: "name", pattern: /\brem\b/i }, { target: "name", pattern: /\bsold\b/i }, { target: "name", pattern: /\bwip\b/i }, { target: "tags", pattern: /\bteaser\b/i }, { target: "description", pattern: /\brules:/i }, ], }, }, { ruleName: "misc definitive", value: DEFINITELY_AD_THRESHOLD, selector: { operator: "or", operands: [ { target: "name", pattern: /\bcharacters?\s+for\s+sale\b/i }, { target: "description", pattern: /\bpoll\s+is\s+up\b/i }, ], }, }, { ruleName: "comic pages", value: -200, selector: { operator: "or", operands: [ { target: "name", pattern: /\bpage\s+\d+/i, }, ], }, }, { ruleName: "commission only names", value: -200, selector: { operator: "or", operands: [ { target: "name", pattern: new RegExp(`\\[${COMMISSION_REGEX_STRING}\\]`, "i"), }, { target: "name", pattern: new RegExp(`^${COMMISSION_REGEX_STRING}$`, "i"), }, ], }, }, { ruleName: "completed YCHs", value: -200, selector: { operator: "and", operands: [ AMBIGUOUS_YCH_SELECTOR, { operator: "or", operands: [ { target: "name", pattern: /\bfinished\b/i }, { target: "name", pattern: /\bresult\b/i }, { target: "description", pattern: /\bfinished\b/i }, ], }, ], }, }, { ruleName: "user reference", value: -200, selector: { operator: "or", operands: [ { target: "name", pattern: /\bfor\s+[\w\-.]+/i }, { target: "description", pattern: /\b(?:by|for|from):?\s+(?::(?:icon[\w\-.]+|[\w\-.]+icon):|@?@\w+)/i, }, { target: "description", pattern: /^:(?:icon[\w\-.]+|[\w\-.]+icon):$/i, }, { target: "description", pattern: /^(?:by|for|from)\s+\w+/i }, { target: "description", pattern: /\bych\s+for\s+\w+/i }, { target: "description", pattern: /\bcharacter\s+©\s+\w+/i }, { target: "description", pattern: /\bcharacter\s+belongs\s+to\s+@?@\w+/i, }, { target: "description", pattern: /\bcommission\s+for\b$/im }, { target: "description", pattern: /(?:©|\(c\))\s*(?::(?:icon[\w\-.]+|[\w\-.]+icon):|@?@\w+)$/im, }, { target: "description", pattern: /\bpatreon\s+reward\s+for\s+\w+/i, }, ], }, }, { ruleName: "artist reference", value: -200, selector: { operator: "or", operands: [ { target: "description", pattern: /🎨\s*:\s*(?::(?:icon[\w\-.]+|[\w\-.]+icon):|@?@\w+)/i, }, ], }, }, { ruleName: "rewards", value: -200, selector: { operator: "or", operands: [{ target: "description", pattern: /\breward\s+sketch\b/i }], }, }, ]; /** * An evaluator for {@link AdRules} on a submission. */ class AdRulesEvaluator { /** * Create a new {@link AdRulesEvaluator}. * @param {AdRules} rules - the rules to use */ constructor(rules) { this.adRuleEvaluators = rules.map((r) => new AdRuleEvaluator(r)); } /** * Explain the rating of a submission. * @param {SubmissionData} submissionData - the data of the submission to explain */ explain(submissionData) { const result = this.test(submissionData); console.group( `Submission: "${submissionData.name}" ${result.rating} -> "${result.level}"`, ); this.adRuleEvaluators.forEach((o) => o.explain(submissionData)); console.groupEnd(); } /** * Test a submission against the rules of the evaluator. * @param {SubmissionData} submissionData - the data of the submission to test * @returns {AdRulesResult} the rating result */ test(submissionData) { const values = this.adRuleEvaluators .map((e) => e.test(submissionData)) .filter((e) => e !== null); const rating = values.reduce((t, v) => t + v, 0); /** @type {AdvertisementLevel | null} */ let level = null; if (rating >= DEFINITELY_AD_THRESHOLD) level = "advertisement"; else if (rating >= AMBIGUOUS_AD_THRESHOLD) level = "ambiguous"; return { level, rating }; } } /** * Map a selector tree node to an evaluator instance. * @param {AdSelector | SelectorCombiner} selector * @return {AdSelectorEvaluator | SelectorCombinerEvaluator} */ function mapSelectorToEvaluator(selector) { if ("target" in selector) return new AdSelectorEvaluator(selector); return new SelectorCombinerEvaluator(selector); } /** * An evaluator for a single {@link AdRule} on a submission */ class AdRuleEvaluator { /** * Create a new {@link AdRuleEvaluator}. * @param {AdRule} rule - the rule to use */ constructor({ ruleName, value, selector }) { this.ruleName = ruleName; this.value = value; this.selectorEvaluator = mapSelectorToEvaluator(selector); } /** * Explain the rating of a submission. * @param {SubmissionData} submissionData - the data of the submission to explain */ explain(submissionData) { const matches = this.selectorEvaluator.test(submissionData); const groupName = `Rule "${this.ruleName}"`; if (matches) { console.group(groupName + ` matches: ${this.value}`); } else { console.groupCollapsed(groupName); } this.selectorEvaluator.explain(submissionData); console.groupEnd(); } /** * Test a submission against the rules of the evaluator. * @param {SubmissionData} submissionData - the data of the submission to test * @returns {number | null} the value of the rule, when there's a match; null otherwise */ test(submissionData) { if (this.selectorEvaluator.test(submissionData)) return this.value; return null; } } /** * Map the operands of a selector combiner to evaluators. * @param {(AdSelector | SelectorCombiner)[]} operands * @return {(AdSelectorEvaluator | SelectorCombinerEvaluator)[]} */ function mapOperandsToEvaluators(operands) { return operands.map((o) => mapSelectorToEvaluator(o)); } /** * An evaluator for a {@link SelectorCombiner} on a submission */ class SelectorCombinerEvaluator { /** * @param {SelectorCombiner} combiner */ constructor({ operator, operands }) { this.operator = operator; this.operandEvaluators = mapOperandsToEvaluators(operands); } /** * Explain the rating of a submission. * @param {SubmissionData} submissionData - the data of the submission to explain */ explain(submissionData) { const matches = this.test(submissionData); const groupName = `Combiner "${this.operator}"`; if (matches) { console.group(groupName + " matches"); } else { console.groupCollapsed(groupName); } for (const o of this.operandEvaluators) { const matched = o.explain(submissionData); if (matched && this.operator === "or") break; } console.groupEnd(); } /** * Test a submission against the rules of the combiner's operands. * @param {SubmissionData} submissionData - the data of the submission to test * @returns {boolean} whether the combiner in total matches the submission */ test(submissionData) { switch (this.operator) { case "and": return this.operandEvaluators.every((o) => o.test(submissionData)); case "or": return this.operandEvaluators.some((o) => o.test(submissionData)); } } } /** * An evaluator for an {@link AdSelector} on a submission */ class AdSelectorEvaluator { /** * @param {AdSelector} selector */ constructor({ target, pattern }) { this.target = target; this.pattern = pattern; } /** * Explain the rating of a submission. * @param {SubmissionData} submissionData - the data of the submission to explain * @return {boolean} whether the submission matched the selector */ explain(submissionData) { const target = this.#getTargetString(submissionData); const matched = this.pattern.test(target); console.log(this.pattern, matched, target); return matched; } /** * Test a submission against the rules of the combiner's operands. * @param {SubmissionData} submissionData - the data of the submission to test * @returns {boolean} whether the selector matches the submission */ test(submissionData) { return this.pattern.test(this.#getTargetString(submissionData)); } /** * @param {SubmissionData} submissionData * @return {string} */ #getTargetString(submissionData) { switch (this.target) { case "name": return submissionData.name; case "tags": return submissionData.tags; case "description": return submissionData.description; } } } /** * Iterate over all submissions on a page and test the ad rules against them. * @returns {[number, number, number]} number of ads, number of ambiguous ads, number of untagged submissions */ function iterateSubmissions() { const figures = Array.from( document.querySelectorAll("section.gallery figure"), ); let advertisements = 0; let ambiguous = 0; let untagged = 0; const evaluator = new AdRulesEvaluator(adRules); for (const figure of figures) { const figcaption = figure.querySelector("figcaption"); const checkbox = figure.querySelector("input"); const nameAnchor = figcaption?.querySelector("a"); const submissionName = nameAnchor?.textContent; const tags = figure?.querySelector("img")?.dataset["tags"]; const description = descriptions[checkbox?.value ?? ""]?.description; const submissionData = { name: submissionName ?? "", description: description ?? "", tags: tags ?? "", }; const result = evaluator.test(submissionData); const button = document.createElement("button"); button.type = "button"; button.textContent = `Ad-rating: ${result.rating}`; button.addEventListener("click", () => evaluator.explain(submissionData)); checkbox?.parentElement?.appendChild(button); if (result.level === "advertisement") { figure.classList.add("advertisement"); if (checkbox) checkbox.checked = true; advertisements += 1; } else if (result.level === "ambiguous") { figure.classList.add("maybe-advertisement"); ambiguous += 1; } if (tags === "") { figcaption?.classList.add("not-tagged"); untagged += 1; } } return [advertisements, ambiguous, untagged]; } const sheet = new CSSStyleSheet(); sheet.replaceSync( ` figure.advertisement { outline: orange 3px solid; } figure.maybe-advertisement { outline: yellow 3px solid; } figcaption.not-tagged input { outline: orange 3px solid; } figcaption button { line-height: 1; margin-left: 1rem; padding: 0; } `.trim(), ); document.adoptedStyleSheets.push(sheet); const sectionHeader = document.querySelector(".section-header"); const advertisementsSelectMessage = document.createElement("p"); sectionHeader?.appendChild(advertisementsSelectMessage); const [advertisements, ambiguous, untagged] = iterateSubmissions(); const message = `Selected ${advertisements} advertisement and ${ambiguous} ambiguous submissions. ${untagged} submissions were not tagged.`; advertisementsSelectMessage.textContent = message; })();