您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
This automatically selects submission notifications, that are advertisements.
当前为
// ==UserScript== // @name Auto-select advertisements // @namespace https://github.com/f1r3w4rr10r/fa-utils // @version 0.1.0 // @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"; // The second "c" is a kyrillic "s"; const commissionRegexString = "[cс]omm(?:ission)?s?"; const commissionBoundedRegexString = `(?:^|\\W)${commissionRegexString}\\b`; const commissionRegex = new RegExp(commissionBoundedRegexString, "i"); const userRefRegex = /(?:by|for|from)\s*(?::(?:icon\w+|\w+icon):|@@\w+)|YCH\s+for\s+\w+/i; /** @type {AdvertisementCheckSpec[]} */ const advertisementCheckSpecs = [ { specName: "obvious ads", name: { triggers: [ /\badopt(?:(?:able)?s?|ing)\b/i, /\bpicarto\.tv\b/i, /\breminder+\b/i, /\bstreaming\b/i, /^REM$/, ], isAlwaysAd: true, }, }, { specName: "WIPs", name: { triggers: [/\bwip\b/i], isAlwaysAd: true, }, tags: { triggers: [/\bwip\b/i], isAlwaysAd: true, }, }, { specName: "commission ads", name: { triggers: [/\bauction\b/i, commissionRegex, /\bwing.its?\b/i], isAdExpressions: [ /\bclosed\b/i, /\bhalfbody\b/i, /\bopen(?:ed)?\b/i, /\bsale\b/i, /\bslots?\b/i, ], isNotAdExpressions: [ /\bfor\b/i, new RegExp(`\\[${commissionRegexString}\\]`, "i"), new RegExp(`^${commissionRegexString}$`, "i"), ], }, description: { triggers: [/.*/i], isNotAdExpressions: [userRefRegex], }, }, { specName: "streams by name", name: { triggers: [/\b(?:live)?stream\b/i], isAdExpressions: [ /\blive\b/i, /\boffline\b/i, /\bonline\b/i, /\bpreorders?\b/i, /\bslots?\b/i, /\bup\b/i, ], }, }, { specName: "streams", name: { triggers: [/\bstream\b/i], isAlwaysAd: true, }, tags: { triggers: [/\bstream\b/i], isAlwaysAd: true, }, }, { specName: "YCHs by name", name: { triggers: [/\by ?c ?h\b/i], isAdExpressions: [ /\bauction\b/i, /\bavailable\b/i, /\bdiscount\b/i, /\bmultislot\b/i, /\bo ?p ?e ?n\b/i, /\bprice\b/i, /\bpreview\b/i, /\braffle\b/i, /\brem(?:ind(?:er)?)?\d*\b/i, /\brmd\b/i, /\bsale\b/i, /\bslots?\b/i, /\bsold\b/i, /\btaken\b/i, /\busd\b/i, /\b\$\d+\b/i, ], isNotAdExpressions: [commissionRegex, /\bfinished\b/i, /\bresult\b/i], }, description: { triggers: [/.*/i], isNotAdExpressions: [userRefRegex], }, untaggedIsAd: true, }, { specName: "YCHs by name and tag", name: { triggers: [/\by ?c ?h\b/i], }, tags: { triggers: [/\bauction\b/i, /\bych\b/i], isAlwaysAd: true, }, }, { specName: "discounts", name: { triggers: [/\bdiscount\b/i, /\bsale\b/i], isAdExpressions: [ /\$/, /\bbase\b/i, /\bclaimed\b/i, /\b(?:multi)?slot\b/i, /\boffer\b/i, /\bprice\b/i, ], }, }, { specName: "price lists", name: { triggers: [/\bprice\b/i], isAdExpressions: [/\blist\b/i, /\bsheet\b/i], }, }, { specName: "raffles", name: { triggers: [/\braffle\b/i], isAdExpressions: [/\bwinners?\b/i], }, }, { specName: "memberships in names", name: { triggers: [ /\bboosty\b/i, /\bp[@a]treon\b/i, /\bsub(?:scribe)?\s?star\b/i, ], isAdExpressions: [ /\bdiscount\b/i, /\bnow on\b/i, /\bposted to\b/i, /\bpreview\b/i, /\bteaser?\b/i, ], }, }, { specName: "memberships teasers in name and description", name: { triggers: [/\bpreview\b/i, /\bteaser\b/i], }, description: { triggers: [ /\b(?:available|n[eo]w|out)\b.*\bon\b.*\b(?:boosty|p[@a]treon|sub(?:scribe)?\s?star)\b/i, ], isAlwaysAd: true, }, }, { specName: "shops", name: { triggers: [/\bshop\b/i], isAdExpressions: [/\bprint\b/i], }, }, { specName: "multislots", name: { triggers: [/\b(?:multi)?slots?\b/i], isAdExpressions: [/\bavailable\b/i, /\bopen\b/i, /\bsketch\b/i], }, }, { specName: "remaining name", name: { triggers: [ /\bclosed\b/i, /\bopen\b/i, /\bpoll\b/i, /\bpreview\b/i, /\brem\b/i, /\bsold\b/i, /\bteaser\b/i, /\bwip\b/i, ], }, }, { specName: "remaining tags", tags: { triggers: [/\bteaser\b/i], }, }, ]; class AdvertisementCheckResult { /** * @param {boolean} isTagged * @param {AdvertisementLevel | null} [nameResult] * @param {AdvertisementLevel | null} [descriptionResult] * @param {AdvertisementLevel | null} [tagsResult] */ constructor(isTagged, nameResult, descriptionResult, tagsResult) { this.#isTagged = isTagged; this.#nameResult = nameResult ?? null; this.#descriptionResult = descriptionResult ?? null; this.#tagsResult = tagsResult ?? null; } /** @type {AdvertisementLevel | null} */ #nameResult = null; /** @type {AdvertisementLevel | null} */ #descriptionResult = null; /** @type {AdvertisementLevel | null} */ #tagsResult = null; #isTagged = false; /** @type {DecisionLogEntry[]} */ #decisionLog = []; /** * @returns {AdvertisementLevel | null} */ get nameResult() { return this.#nameResult; } /** * @param {AdvertisementLevel | null} value */ set nameResult(value) { this.#nameResult = this.#coalesceResultLevel(this.#nameResult, value); } /** * @returns {AdvertisementLevel | null} */ get descriptionResult() { return this.#descriptionResult; } /** * @param {AdvertisementLevel | null} value */ set descriptionResult(value) { this.#descriptionResult = this.#coalesceResultLevel( this.#descriptionResult, value, ); } /** * @returns {AdvertisementLevel | null} */ get tagsResult() { return this.#tagsResult; } /** * @param {AdvertisementLevel | null} value */ set tagsResult(value) { this.#tagsResult = this.#coalesceResultLevel(this.#tagsResult, value); } /** * @returns {AdvertisementLevel | null} */ get result() { if ( this.#nameResult === "notAdvertisement" || this.#descriptionResult === "notAdvertisement" || this.#tagsResult === "notAdvertisement" ) { return "notAdvertisement"; } if ( this.#nameResult === "advertisement" || this.#descriptionResult === "advertisement" || this.#tagsResult === "advertisement" ) { return "advertisement"; } if ( this.#nameResult === "ambiguous" || this.#descriptionResult === "ambiguous" || this.#tagsResult === "ambiguous" ) { return "ambiguous"; } return null; } /** * @returns {boolean} */ get isTagged() { return this.#isTagged; } get decisionLog() { return this.#decisionLog; } /** * @param {DecisionLogEntry} log */ addToLog(log) { if (log.name === null && log.description === null && log.tags === null) { return; } this.#decisionLog.push(log); } /** * @param {AdvertisementLevel | null} current * @param {AdvertisementLevel | null} newValue * @returns {AdvertisementLevel | null} */ #coalesceResultLevel(current, newValue) { if (newValue === null) { return current; } if (current === "notAdvertisement" || newValue === "notAdvertisement") { return "notAdvertisement"; } if (current === "advertisement" && newValue === "ambiguous") { return current; } return newValue; } } /** * @param {string} text * @param {AdvertisementCheckSpecPart} spec * @returns {[AdvertisementLevel | null, DecisionLogEntryPart | null]} */ function checkAgainstAdvertisementSpec(text, spec) { /** @type {AdvertisementLevel | null} */ let level = null; /** @type {DecisionLogEntryPart | null} */ let log = null; for (const regex of spec.triggers) { if (regex.test(text)) { level = "ambiguous"; log = { trigger: regex, level }; break; } } if (level === "ambiguous") { if (spec.isAlwaysAd) { level = "advertisement"; if (log) { log.isAlwaysAd = true; log.level = level; } } else if (spec.isAdExpressions) { for (const regex of spec.isAdExpressions) { if (regex.test(text)) { level = "advertisement"; if (log) { log.isAdExpression = regex; log.level = level; } break; } } } } if (level !== null && spec.isNotAdExpressions) { for (const regex of spec.isNotAdExpressions) { if (regex.test(text)) { level = "notAdvertisement"; if (log) { log.isNotAdExpression = regex; log.level = level; } break; } } } return [level, log]; } /** * @param {string} submissionName * @param {string} description * @param {string} tags * @returns {AdvertisementCheckResult} */ function checkAgainstAdvertisementSpecs(submissionName, description, tags) { const isUntagged = tags === ""; /** @type {AdvertisementCheckResult} */ const result = new AdvertisementCheckResult(!isUntagged); for (const spec of advertisementCheckSpecs) { const [nameResult, nameLog] = checkAgainstAdvertisementSpec( submissionName, { triggers: [], ...spec.name, }, ); const [descriptionResult, descriptionLog] = checkAgainstAdvertisementSpec( description, { triggers: [], ...spec.description, }, ); /** @type {AdvertisementLevel | null} */ let tagsResult = null; /** @type {DecisionLogEntryPart | null} */ let tagsLog = null; if (isUntagged) { if ( (["advertisement", "ambiguous"].includes(nameResult) || ["advertisement", "ambiguous"].includes(descriptionResult)) && spec.untaggedIsAd ) { tagsResult = "advertisement"; tagsLog = { level: "advertisement", trigger: /^$/, isAlwaysAd: true, }; } } else { [tagsResult, tagsLog] = checkAgainstAdvertisementSpec(tags, { triggers: [], ...spec.tags, }); } // TODO: Maybe change the accumulation to an overall weighting algorithm. // Parts present in the same spec are interpreted as being "AND" // connected. let specPartsCount = 0; if (spec.name) specPartsCount++; if (spec.description) specPartsCount++; if (spec.tags) specPartsCount++; if (specPartsCount > 1) { if (spec.name && !nameResult) continue; if (spec.description && !descriptionResult) continue; if (spec.tags && !tagsResult) continue; } result.nameResult = nameResult; result.descriptionResult = descriptionResult; result.tagsResult = tagsResult; result.addToLog({ specName: spec.specName, level: result.result, name: nameLog, description: descriptionLog, tags: tagsLog, }); } return result; } /** * @returns {[number, number, number]} */ function iterateSubmissions() { const figures = Array.from( document.querySelectorAll("section.gallery figure"), ); let advertisements = 0; let ambiguous = 0; let untagged = 0; 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 result = checkAgainstAdvertisementSpecs( submissionName, description, tags, ); const decisionLog = result.decisionLog; if (decisionLog.length) { const button = document.createElement("button"); button.type = "button"; button.textContent = "Log"; button.addEventListener("click", () => console.log(result.decisionLog)); checkbox.parentElement.appendChild(button); } switch (result.result) { case "advertisement": figure.classList.add("advertisement"); checkbox.checked = true; advertisements += 1; break; case "ambiguous": figure.classList.add("maybe-advertisement"); ambiguous += 1; break; } if (!result.isTagged) { 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"); advertisementsSelectMessage.textContent = "Checking for advertisement submissions…"; 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; })();