Last active
July 27, 2026 18:49
-
-
Save Luckz/3ee02911d57da5b1793eb221beb6f169 to your computer and use it in GitHub Desktop.
SteamCommentAnalysis.user.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const assert = require('node:assert/strict'); | |
| const { | |
| findRawComment, | |
| formatBBCode, | |
| placeNotice, | |
| placeholdersInNode, | |
| } = require('./SteamCommentAnalysis.user.js'); | |
| assert.equal( | |
| formatBBCode('[b]bold[/b]\n[url=javascript:alert(1)]unsafe[/url]\n<script>x</script>'), | |
| '<b>bold</b><br><a class="bb_link">unsafe</a><br><script>x</script>' | |
| ); | |
| assert.equal( | |
| formatBBCode('[quote=Alice;123]Hello [i]there[/i][/quote]'), | |
| '<blockquote class="bb_blockquote with_author"><div class="bb_quoteauthor">Originally posted by <b><a href="#c123">Alice</a></b>:</div>Hello <i>there</i></blockquote>' | |
| ); | |
| const expected = { text: 'found' }; | |
| const threads = { | |
| wrong: { GetRawComment: () => null }, | |
| right: { GetRawComment: id => id === '42' ? expected : null }, | |
| }; | |
| assert.equal(findRawComment(threads, '42'), expected); | |
| const placeholder = { nodeType: 1, matches: selector => selector === '.needs_content_check' }; | |
| assert.deepEqual(placeholdersInNode(placeholder), [placeholder]); | |
| const container = { | |
| nodeType: 1, | |
| matches: () => false, | |
| querySelectorAll: selector => selector === '.needs_content_check' ? [placeholder] : [], | |
| }; | |
| assert.deepEqual(placeholdersInNode(container), [placeholder]); | |
| const timestamp = {}; | |
| let insertion; | |
| const authorNameGroup = { | |
| querySelector: selector => selector === '.commentthread_comment_timestamp' ? timestamp : null, | |
| insertBefore: (notice, reference) => { insertion = { notice, reference }; }, | |
| }; | |
| const comment = { | |
| querySelector: selector => selector === '.author_name_group' ? authorNameGroup : null, | |
| }; | |
| const notice = {}; | |
| assert.equal(placeNotice(comment, notice), true); | |
| assert.deepEqual(insertion, { notice, reference: timestamp }); | |
| console.log('SteamCommentAnalysis.user.js tests passed'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name [AI] Steam Community: Show comments awaiting analysis | |
| // @namespace luckz | |
| // @author luckz | |
| // @LLMs GPT-5.6-SOL | |
| // @version 0.8.5 | |
| // @description Shows forum comments stuck in Valve's automated content check system | |
| // @match https://steamcommunity.com/groups/*/discussions/* | |
| // @match https://steamcommunity.com/app/*/discussions/* | |
| // @match https://steamcommunity.com/app/*/eventcomments/* | |
| // @match https://steamcommunity.com/discussions/forum/* | |
| // @grant none | |
| // @run-at document-body | |
| // @og-author Ryzhehvost | |
| // @source https://greasyfork.org/en/scripts/409913-show-not-checked-content | |
| // @license Apache-2.0 | |
| // @downloadURL https://gist.github.com/Luckz/3ee02911d57da5b1793eb221beb6f169/raw/SteamCommentAnalysis.user.js | |
| // ==/UserScript== | |
| 'use strict'; | |
| function escapeHTML(value) { | |
| return String(value) | |
| .replaceAll('&', '&') | |
| .replaceAll('<', '<') | |
| .replaceAll('>', '>') | |
| .replaceAll('"', '"') | |
| .replaceAll("'", '''); | |
| } | |
| function safeURL(value) { | |
| try { | |
| const url = new URL(value, 'https://steamcommunity.com/'); | |
| return ['http:', 'https:', 'steam:'].includes(url.protocol) ? value : null; | |
| } catch (_) { | |
| return null; | |
| } | |
| } | |
| function openingTag(name, argument) { | |
| const simpleTags = { | |
| b: ['<b>', '</b>'], | |
| i: ['<i>', '</i>'], | |
| u: ['<u>', '</u>'], | |
| h1: ['<div class="bb_h1">', '</div>'], | |
| strike: ['<span class="bb_strike">', '</span>'], | |
| spoiler: ['<span class="bb_spoiler"><span>', '</span></span>'], | |
| list: ['<ul class="bb_ul">', '</ul>'], | |
| olist: ['<ol>', '</ol>'], | |
| }; | |
| if (simpleTags[name]) { | |
| return simpleTags[name]; | |
| } | |
| if (name === 'url' && argument !== null) { | |
| const url = safeURL(argument); | |
| return url | |
| ? [`<a class="bb_link" href="${escapeHTML(url)}" target="_blank" rel="noreferrer">`, '</a>'] | |
| : ['<a class="bb_link">', '</a>']; | |
| } | |
| if (name === 'quote') { | |
| if (argument === null) { | |
| return ['<blockquote class="bb_blockquote">', '</blockquote>']; | |
| } | |
| const separator = argument.lastIndexOf(';'); | |
| const author = separator === -1 ? argument : argument.slice(0, separator); | |
| const commentID = separator === -1 ? '' : argument.slice(separator + 1); | |
| const escapedAuthor = escapeHTML(author); | |
| const attribution = /^\d+$/.test(commentID) | |
| ? `<a href="#c${commentID}">${escapedAuthor}</a>` | |
| : escapedAuthor; | |
| return [ | |
| `<blockquote class="bb_blockquote with_author"><div class="bb_quoteauthor">Originally posted by <b>${attribution}</b>:</div>`, | |
| '</blockquote>', | |
| ]; | |
| } | |
| return null; | |
| } | |
| function formatBBCode(text) { | |
| const tokenPattern = /\[\/?([a-z]+)(?:=([^\]]*))?\]/gi; | |
| const stack = []; | |
| let output = ''; | |
| let position = 0; | |
| let match; | |
| const appendText = value => { | |
| output += escapeHTML(value).replace(/\r?\n/g, '<br>'); | |
| }; | |
| while ((match = tokenPattern.exec(text)) !== null) { | |
| appendText(text.slice(position, match.index)); | |
| position = tokenPattern.lastIndex; | |
| const closing = match[0][1] === '/'; | |
| const name = match[1].toLowerCase(); | |
| const argument = match[2] === undefined ? null : match[2]; | |
| if (!closing && (name === 'noparse' || name === 'code')) { | |
| const endPattern = new RegExp(`\\[/${name}\\]`, 'ig'); | |
| endPattern.lastIndex = position; | |
| const end = endPattern.exec(text); | |
| if (!end) { | |
| appendText(match[0]); | |
| continue; | |
| } | |
| const content = escapeHTML(text.slice(position, end.index)); | |
| output += name === 'code' ? `<div class="bb_code">${content}</div>` : content; | |
| position = endPattern.lastIndex; | |
| tokenPattern.lastIndex = position; | |
| continue; | |
| } | |
| if (closing) { | |
| const open = stack.at(-1); | |
| if (open && open.name === name) { | |
| output += open.close; | |
| stack.pop(); | |
| } else { | |
| appendText(match[0]); | |
| } | |
| continue; | |
| } | |
| const tag = openingTag(name, argument); | |
| if (!tag) { | |
| appendText(match[0]); | |
| continue; | |
| } | |
| output += tag[0]; | |
| stack.push({ name, close: tag[1] }); | |
| } | |
| appendText(text.slice(position)); | |
| while (stack.length) { | |
| output += stack.pop().close; | |
| } | |
| return output; | |
| } | |
| function findRawComment(threadCollections, commentID) { | |
| const collections = Array.isArray(threadCollections) ? threadCollections : [threadCollections]; | |
| for (const threads of collections) { | |
| if (!threads) { | |
| continue; | |
| } | |
| for (const key of Object.keys(threads)) { | |
| try { | |
| const rawComment = threads[key]?.GetRawComment?.(commentID); | |
| if (rawComment?.text !== undefined) { | |
| return rawComment; | |
| } | |
| } catch (_) { | |
| // This comment belongs to a different thread. | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| function placeholdersInNode(node) { | |
| if (node.nodeType !== 1) { | |
| return []; | |
| } | |
| const placeholders = node.matches('.needs_content_check') ? [node] : []; | |
| return placeholders.concat(Array.from(node.querySelectorAll?.('.needs_content_check') || [])); | |
| } | |
| function placeNotice(comment, notice) { | |
| const authorNameGroup = comment.querySelector('.author_name_group'); | |
| const timestamp = authorNameGroup?.querySelector('.commentthread_comment_timestamp'); | |
| if (!timestamp) { | |
| return false; | |
| } | |
| authorNameGroup.insertBefore(notice, timestamp); | |
| return true; | |
| } | |
| function start() { | |
| const threadCollections = [ | |
| globalThis.g_rgForumTopicCommentThreads, | |
| globalThis.g_rgCommentThreads, | |
| ]; | |
| function reveal(placeholder) { | |
| const comment = placeholder.closest('.commentthread_comment'); | |
| const match = comment?.id.match(/^comment_(\d+)$/); | |
| if (!match) { | |
| return; | |
| } | |
| const rawComment = findRawComment(threadCollections, match[1]); | |
| if (!rawComment) { | |
| return; | |
| } | |
| const content = placeholder.closest('.commentthread_comment_text'); | |
| if (!content) { | |
| return; | |
| } | |
| content.innerHTML = formatBBCode(rawComment.text); | |
| if (!comment.querySelector('.improved_content_check_notice')) { | |
| const notice = document.createElement('span'); | |
| notice.className = 'forum_comment_author_banned improved_content_check_notice'; | |
| notice.textContent = '(comment is awaiting analysis)'; | |
| placeNotice(comment, notice); | |
| } | |
| } | |
| document.querySelectorAll('.needs_content_check').forEach(reveal); | |
| const observer = new MutationObserver(mutations => { | |
| for (const mutation of mutations) { | |
| for (const node of mutation.addedNodes) { | |
| placeholdersInNode(node).forEach(reveal); | |
| } | |
| } | |
| }); | |
| document.querySelectorAll('.commentthread_area').forEach(area => { | |
| observer.observe(area, { childList: true, subtree: true }); | |
| }); | |
| } | |
| if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = { findRawComment, formatBBCode, placeNotice, placeholdersInNode }; | |
| } else { | |
| start(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment