diff --git a/.github/workflows/anti-spam-comment-moderator.yml b/.github/workflows/anti-spam-comment-moderator.yml deleted file mode 100644 index 7e424e6ba..000000000 --- a/.github/workflows/anti-spam-comment-moderator.yml +++ /dev/null @@ -1,270 +0,0 @@ -name: Anti-Spam Comment Moderator - -on: - issues: - types: [opened, edited] - pull_request: - types: [opened, edited] - issue_comment: - types: [created, edited] - pull_request_review_comment: - types: [created, edited] - -permissions: - issues: write # needed to delete/close issues and comments - pull-requests: write # needed to delete/close PRs and comments - contents: write # needed to delete commit comments - # (discussions not handled here; API differs) - -jobs: - moderate: - if: ${{ github.event.action == 'created' || github.event.action == 'edited' || github.event.action == 'opened' }} - runs-on: ubuntu-latest - steps: - - name: Run spam filter - uses: actions/github-script@v7 - with: - script: | - // 1) Collect event/comment/issue/PR info - const ev = context.eventName; - const comment = context.payload.comment || {}; - const issue = context.payload.issue || {}; - const pr = context.payload.pull_request || {}; - - // Determine the content source (comment, issue body, or PR body) - let body, assoc, actor, itemId, itemType; - if (ev === "issue_comment" || ev === "pull_request_review_comment") { - body = (comment.body || "").trim(); - assoc = comment.author_association || "NONE"; - actor = comment.user?.login || "unknown"; - itemId = comment.id; - itemType = "comment"; - } else if (ev === "issues") { - body = (issue.body || "").trim(); - assoc = issue.author_association || "NONE"; - actor = issue.user?.login || "unknown"; - itemId = issue.number; - itemType = "issue"; - } else if (ev === "pull_request") { - body = (pr.body || "").trim(); - assoc = pr.author_association || "NONE"; - actor = pr.user?.login || "unknown"; - itemId = pr.number; - itemType = "pr"; - } else { - core.warning(`Unhandled event: ${ev}`); - return; - } - - const bodyLower = body.toLowerCase(); - const owner = context.repo.owner; - const repo = context.repo.repo; - - // Block specific user outright (also block blaji-villeb106) - const blockedUsers = ["phuole818", "blaji-villeb106"]; - if (blockedUsers.some(u => (actor || "").toLowerCase() === u.toLowerCase())) { - try { - if (itemType === "comment") { - if (ev === "issue_comment") { - await github.rest.issues.deleteComment({ owner, repo, comment_id: itemId }); - core.notice(`Deleted comment from blocked user @${actor} (issue comment).`); - } else if (ev === "pull_request_review_comment") { - await github.rest.pulls.deleteReviewComment({ owner, repo, comment_id: itemId }); - core.notice(`Deleted comment from blocked user @${actor} (PR review comment).`); - } else if (ev === "commit_comment") { - await github.rest.repos.deleteCommitComment({ owner, repo, comment_id: itemId }); - core.notice(`Deleted comment from blocked user @${actor} (commit comment).`); - } - } else if (itemType === "issue") { - await github.rest.issues.update({ owner, repo, issue_number: itemId, state: "closed", state_reason: "not_planned" }); - core.notice(`Closed issue from blocked user @${actor} (issue #${itemId}).`); - } else if (itemType === "pr") { - await github.rest.pulls.update({ owner, repo, pull_number: itemId, state: "closed" }); - core.notice(`Closed PR from blocked user @${actor} (PR #${itemId}).`); - } - } catch (err) { - core.setFailed(`Failed to handle blocked user's content: ${err?.message || err}`); - } - return; - } - - // 2) Skip trusted roles or explicitly allowed text - const trustedRoles = new Set(["OWNER","MEMBER","COLLABORATOR"]); - if (trustedRoles.has(assoc)) { - core.info(`Skipping trusted author (${assoc}) @${actor}`); - return; - } - if (/#allow|#nospamfilter/i.test(body)) { - core.info("Skipping due to explicit allow tag in comment."); - return; - } - - // 3) Heuristic + sentiment-lite checks - // Link analysis with domain allowlist (do not penalize common safe docs/code links) - const safeDomains = [ - "github.com","docs.github.com","githubusercontent.com","gitlab.com","bitbucket.org", - "readthedocs.io","arxiv.org","pypi.org","npmjs.com","crates.io","stackoverflow.com","stackexchange.com" - ]; - const urlMatches = (body.match(/https?:\/\/[^\s)]+/gi) || []); - let safeLinkCount = 0; - let suspiciousLinkCount = 0; - for (const u of urlMatches) { - try { - const h = new URL(u).hostname.replace(/^www\./i, ""); - const isShortHost = /^(bit\.ly|t\.co|tinyurl\.com|goo\.gl|ow\.ly)$/i.test(h); - const isSafe = safeDomains.some(d => h === d || h.endsWith(`.${d}`)); - if (isSafe && !isShortHost) safeLinkCount += 1; - else suspiciousLinkCount += 1; - } catch { - suspiciousLinkCount += 1; - } - } - const linkCount = urlMatches.length; - const emailCount = (body.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi) || []).length; - const phoneCount = (body.match(/(\+?\d[\d\s().-]{8,}\d)/g) || []).length; - const mentions = (body.match(/@\w{1,39}/g) || []).length; - const exclaimBlk = /!{3,}/.test(body); - const repeatedChr = /(.)\1{6,}/.test(body); - const shortened = /https?:\/\/(?:bit\.ly|t\.co|tinyurl\.com|goo\.gl|ow\.ly)\//i.test(body); - - const lettersOnly = body.replace(/\s/g, ""); - const uniqueRatio = lettersOnly.length ? (new Set(lettersOnly).size / lettersOnly.length) : 1; - const lowUnique = lettersOnly.length > 80 && uniqueRatio < 0.30; - - // English/ASCII spam terms (word-boundary safe) - const blacklistAscii = [ - "whatsapp","telegram","crypto","forex","investment","binary options","broker", - "dm me","contact me","private message","girls","porn","xxx","nude","sex", - "loan approval","free followers","click here","visit my profile","earn $","% off", - "sugar daddy","promo code","join my group","passive income","weixin","vx","wx" - ]; - // Chinese/CJK spam phrases (substring match; \b doesn't work for CJK) - const blacklistCJK = [ - "微信","加我微信","添加微信","VX","V信","私信","联系我","电报","比特币","加密货币","外汇","投资","理财","二元期权", - "裸聊","色情","黄片","成人网站","约炮","兼职","推广","优惠","促销","关注我","点击这里","访问我的主页","我的主页", - "加入群","交流群","被动收入","糖爹","金主","优惠码","贷款","快速贷款","网贷","免费粉丝","粉丝增长", - "赚快钱","快速赚钱","轻松赚钱","保证收益","零风险","无风险","稳赚","返利","优惠券" - ]; - const asciiHit = blacklistAscii.some(k => new RegExp(`\\b${k.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, "i").test(body)); - const cjkHit = blacklistCJK.some(k => body.includes(k)); - const keywordHit = asciiHit || cjkHit; - const hype = /(100%|guarantee|risk[- ]?free|no (fees|risk)|quick money|make money)/i.test(body) || - /(保证|无风险|零风险|快速赚钱|轻松赚钱|立即联系|添加微信|加我微信|稳赚|包赚)/.test(body); - - // Attack/Insult/Tech-context term lists (EN + CJK) - const attackTermsAscii = [ - "fake stars","astroturf","bot accounts","paid stars","star farming","star boosting","shill", - "manipulated stars","kpi","kpi boosting","no maintainer","ignore issues","ignore prs", - "close pr","close issue","no response","waste of time","trash project","scam project", - "archive this project","unmaintained","low quality docs","unreadable docs","pitfall","avoid this project", - "dead project","abandoned project","team lost contact","stay away" - ]; - const attackTermsCJK = [ - "刷星","水军","kpi刷单","假号","买粉","造假","刷榜","刷人气", - "别踩坑","大坑","浪费时间","赶紧换","不靠谱","建议归档","建议archive", - "没人理你","没人管","装没看见","秒关","石沉大海","失联","团队失联","维护团队失联", - "问题一大堆","一塌糊涂","堪忧","离谱","看不懂","入不了门", - "警告","大踩雷","失望透顶","全靠刷星","社区大踩雷","死项目","远离","及早远离", - "异常增长","激增","数量异常","star异常","star数异常","内部号召","非自然" - ]; - const insultTermsAscii = [ - "trash","garbage","bullshit","idiot","moron","stupid","dumb","shameful","useless" - ]; - const insultTermsCJK = [ - "垃圾","辣鸡","废物","弱智","傻逼","脑残","狗屎","丢人" - ]; - const techContextAscii = [ - "bug","repro","reproduce","steps to reproduce","minimal repro","expected","actual", - "stack trace","traceback","stacktrace","log","logs","error","panic","poc","cve", - "version","v1","v2","v3","config","configuration","file","line","code snippet" - ]; - const techContextCJK = [ - "复现","复现步骤","最小复现","期望行为","实际行为","堆栈","栈追踪","日志","报错", - "版本","配置","文件","行号","代码片段","poc","cve" - ]; - - const escapeRe = (s) => s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); - const countMatchesAscii = (terms) => - terms.reduce((n, k) => n + (new RegExp(`\\b${escapeRe(k)}\\b`, "i").test(body) ? 1 : 0), 0); - const countMatchesCJK = (terms) => - terms.reduce((n, k) => n + (body.includes(k) ? 1 : 0), 0); - - const attackHits = countMatchesAscii(attackTermsAscii) + countMatchesCJK(attackTermsCJK); - const insultHit = (countMatchesAscii(insultTermsAscii) + countMatchesCJK(insultTermsCJK)) > 0; - const techCtxHit = (countMatchesAscii(techContextAscii) + countMatchesCJK(techContextCJK)) > 0; - const strongCJK = /(失望透顶|离谱|警告|大踩雷|失联|死项目|远离|异常增长|激增|刷星|刷人气)/.test(body); - - // Sentiment-lite (AFINN-style mini-lexicon) - const afinn = { - "amazing": 2, "great": 2, "free": 1, "guaranteed": -1, - "scam": -3, "profit": 1, "winner": 1, "urgent": -1, "risk-free": -2 - }; - const tokens = body.toLowerCase().split(/[^a-z0-9+\-]+/); - let sentiment = 0; - for (const t of tokens) if (afinn[t] != null) sentiment += afinn[t]; - - // Score: Only use attack/insult signals for blocking (ignore links/emails/phones) - let points = 0; - // Attack/insult scoring with guardrails for technical context - let attackContribution = 0; - if (insultHit) attackContribution += 2; - if (attackHits >= 3) attackContribution += 2; - else if (attackHits >= 1) attackContribution += 1; - if ((exclaimBlk || strongCJK) && attackContribution > 0) attackContribution += 1; - if (techCtxHit) attackContribution = Math.min(1, attackContribution); // cap if technical context detected - points += attackContribution; - - core.info(`Spam score for @${actor} = ${points} (attackOnly; links/emails/phones ignored) (links:${linkCount} safe:${safeLinkCount} suspicious:${suspiciousLinkCount}, emails:${emailCount}, phones:${phoneCount}, mentions:${mentions}, sentiment:${sentiment}, attackHits:${attackHits}, insult:${insultHit}, techCtx:${techCtxHit}, itemType:${itemType})`); - - // Only block when attack/insult crosses threshold - const isSpam = attackContribution >= 2; // adjust threshold if needed - if (!isSpam) { - core.info("Content not flagged as spam."); - return; - } - - // 4) Delete/close the spam content using the appropriate endpoint - try { - if (itemType === "comment") { - if (ev === "issue_comment") { - await github.rest.issues.deleteComment({ - owner, repo, comment_id: itemId - }); - core.notice(`Deleted spam issue comment from @${actor}.`); - } else if (ev === "pull_request_review_comment") { - await github.rest.pulls.deleteReviewComment({ - owner, repo, comment_id: itemId - }); - core.notice(`Deleted spam PR review comment from @${actor}.`); - } else if (ev === "commit_comment") { - await github.rest.repos.deleteCommitComment({ - owner, repo, comment_id: itemId - }); - core.notice(`Deleted spam commit comment from @${actor}.`); - } - } else if (itemType === "issue") { - await github.rest.issues.update({ - owner, repo, issue_number: itemId, state: "closed", state_reason: "not_planned" - }); - await github.rest.issues.createComment({ - owner, repo, issue_number: itemId, - body: "This issue has been automatically closed as spam." - }); - core.notice(`Closed spam issue #${itemId} from @${actor}.`); - } else if (itemType === "pr") { - await github.rest.pulls.update({ - owner, repo, pull_number: itemId, state: "closed" - }); - await github.rest.issues.createComment({ - owner, repo, issue_number: itemId, - body: "This pull request has been automatically closed as spam." - }); - core.notice(`Closed spam PR #${itemId} from @${actor}.`); - } else { - core.warning(`Unhandled item type: ${itemType}`); - } - } catch (err) { - core.setFailed(`Failed to handle spam content: ${err?.message || err}`); - } - - diff --git a/.github/workflows/anti-spam-filter.yml b/.github/workflows/anti-spam-filter.yml new file mode 100644 index 000000000..d25355b2e --- /dev/null +++ b/.github/workflows/anti-spam-filter.yml @@ -0,0 +1,51 @@ +name: Anti-Spam Filter (Hidden Logic) + +on: + issues: + types: [opened, edited] + pull_request: + types: [opened, edited] + issue_comment: + types: [created, edited] + pull_request_review_comment: + types: [created, edited] + +permissions: + issues: write + pull-requests: write + contents: write + +jobs: + moderate: + if: ${{ github.event.action == 'created' || github.event.action == 'edited' || github.event.action == 'opened' }} + runs-on: ubuntu-latest + steps: + - name: Run spam filter + uses: actions/github-script@v7 + env: + # The entire spam detection logic is stored here + SPAM_DETECTION_SCRIPT: ${{ secrets.SPAM_DETECTION_SCRIPT }} + with: + script: | + // Load and execute the spam detection script from secret + const detectionScript = process.env.SPAM_DETECTION_SCRIPT; + + if (!detectionScript) { + core.error("SPAM_DETECTION_SCRIPT secret not found!"); + core.setFailed("Spam filter not configured"); + return; + } + + try { + // Execute the hidden script + // The script has access to: github, context, core + const detectSpam = eval(detectionScript); + + // Run the detection + await detectSpam(github, context, core); + + } catch (err) { + core.error(`Spam filter error: ${err.message}`); + core.setFailed(`Filter execution failed: ${err.message}`); + } + diff --git a/.github/workflows/cleanup-existing-spam.yml b/.github/workflows/cleanup-existing-spam.yml index bef174a64..87163f047 100644 --- a/.github/workflows/cleanup-existing-spam.yml +++ b/.github/workflows/cleanup-existing-spam.yml @@ -38,6 +38,9 @@ jobs: steps: - name: Scan and cleanup spam uses: actions/github-script@v7 + env: + # Uses the same hidden detection logic as the main filter + SPAM_DETECTION_SCRIPT: ${{ secrets.SPAM_DETECTION_SCRIPT }} with: script: | const dryRun = '${{ inputs.dry_run }}' === 'true'; @@ -47,80 +50,98 @@ jobs: const owner = context.repo.owner; const repo = context.repo.repo; - core.info(`Starting cleanup - Dry run: ${dryRun}, Scan issues: ${scanIssues}, Scan comments: ${scanComments}`); + core.info(`Starting cleanup with hidden detection logic`); + core.info(` Dry run: ${dryRun}`); + core.info(` Scan issues: ${scanIssues}`); + core.info(` Scan comments: ${scanComments}`); + core.info(``); - // Blocked users list - const blockedUsers = ["phuole818", "blaji-villeb106"]; + // Load detection script from secret + const detectionScript = process.env.SPAM_DETECTION_SCRIPT; - // Spam detection function (same as the main filter) - function analyzeContent(body, actor) { - const bodyLower = body.toLowerCase(); - - // Check blocked users - if (blockedUsers.some(u => (actor || "").toLowerCase() === u.toLowerCase())) { - return { isSpam: true, reason: "Blocked user", score: 999 }; - } - - // Attack/Insult terms - const attackTermsAscii = [ - "fake stars","astroturf","bot accounts","paid stars","star farming","star boosting","shill", - "manipulated stars","kpi","kpi boosting","no maintainer","ignore issues","ignore prs", - "close pr","close issue","no response","waste of time","trash project","scam project", - "archive this project","unmaintained","low quality docs","unreadable docs","pitfall","avoid this project", - "dead project","abandoned project","team lost contact","stay away" - ]; - const attackTermsCJK = [ - "刷星","水军","kpi刷单","假号","买粉","造假","刷榜","刷人气", - "别踩坑","大坑","浪费时间","赶紧换","不靠谱","建议归档","建议archive", - "没人理你","没人管","装没看见","秒关","石沉大海","失联","团队失联","维护团队失联", - "问题一大堆","一塌糊涂","堪忧","离谱","看不懂","入不了门", - "警告","大踩雷","失望透顶","全靠刷星","社区大踩雷","死项目","远离","及早远离", - "异常增长","激增","数量异常","star异常","star数异常","内部号召","非自然" - ]; - const insultTermsAscii = [ - "trash","garbage","bullshit","idiot","moron","stupid","dumb","shameful","useless" - ]; - const insultTermsCJK = [ - "垃圾","辣鸡","废物","弱智","傻逼","脑残","狗屎","丢人" - ]; - const techContextAscii = [ - "bug","repro","reproduce","steps to reproduce","minimal repro","expected","actual", - "stack trace","traceback","stacktrace","log","logs","error","panic","poc","cve", - "version","v1","v2","v3","config","configuration","file","line","code snippet" - ]; - const techContextCJK = [ - "复现","复现步骤","最小复现","期望行为","实际行为","堆栈","栈追踪","日志","报错", - "版本","配置","文件","行号","代码片段","poc","cve" - ]; + if (!detectionScript) { + core.error("SPAM_DETECTION_SCRIPT secret not found!"); + core.setFailed("Spam detection not configured"); + return; + } + + // Create analyzer function (all logic hidden in secret) + let analyzeContent; + + try { + // Wrap the detection script to create a reusable analyzer + const analyzerWrapper = ` + (async function(github, context, core) { + ${detectionScript} + }) + `; - const escapeRe = (s) => s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); - const countMatchesAscii = (terms) => - terms.reduce((n, k) => n + (new RegExp(`\\b${escapeRe(k)}\\b`, "i").test(body) ? 1 : 0), 0); - const countMatchesCJK = (terms) => - terms.reduce((n, k) => n + (body.includes(k) ? 1 : 0), 0); + // Execute to get the detection capabilities + const detectionModule = eval(analyzerWrapper); + await detectionModule(github, context, core); - const attackHits = countMatchesAscii(attackTermsAscii) + countMatchesCJK(attackTermsCJK); - const insultHit = (countMatchesAscii(insultTermsAscii) + countMatchesCJK(insultTermsCJK)) > 0; - const techCtxHit = (countMatchesAscii(techContextAscii) + countMatchesCJK(techContextCJK)) > 0; - const strongCJK = /(失望透顶|离谱|警告|大踩雷|失联|死项目|远离|异常增长|激增|刷星|刷人气)/.test(body); - const exclaimBlk = /!{3,}/.test(body); + // Create a mock context for analysis + analyzeContent = async function(body, actor, assoc) { + // Create mock event for analysis + const mockContext = { + ...context, + eventName: 'issues', + payload: { + issue: { + body: body, + user: { login: actor }, + author_association: assoc, + number: 0 + }, + action: 'opened' + } + }; + + // Run detection on mock event + let isSpam = false; + let reason = "Clean"; + let score = 0; + + // Capture the detection result by checking if action would be taken + const originalUpdate = github.rest.issues.update; + const originalDelete = github.rest.issues.deleteComment; + + let detectionResult = { isSpam: false }; + + github.rest.issues.update = async (params) => { + detectionResult = { isSpam: true, reason: "Would close", score: 2 }; + return { data: {} }; + }; + + github.rest.issues.deleteComment = async (params) => { + detectionResult = { isSpam: true, reason: "Would delete", score: 2 }; + return { data: {} }; + }; + + github.rest.issues.createComment = async () => ({ data: {} }); + github.rest.pulls.update = async () => ({ data: {} }); + github.rest.pulls.deleteReviewComment = async () => ({ data: {} }); + + try { + await detectionModule(github, mockContext, core); + } catch (e) { + // Ignore errors from mock execution + } + + // Restore original functions + github.rest.issues.update = originalUpdate; + github.rest.issues.deleteComment = originalDelete; + + return detectionResult; + }; - let attackContribution = 0; - if (insultHit) attackContribution += 2; - if (attackHits >= 3) attackContribution += 2; - else if (attackHits >= 1) attackContribution += 1; - if ((exclaimBlk || strongCJK) && attackContribution > 0) attackContribution += 1; - if (techCtxHit) attackContribution = Math.min(1, attackContribution); + core.info("✅ Loaded spam detection from secret"); + core.info(""); - const isSpam = attackContribution >= 2; - return { - isSpam, - score: attackContribution, - attackHits, - insultHit, - techCtxHit, - reason: isSpam ? `Attack score: ${attackContribution} (attackHits: ${attackHits}, insult: ${insultHit}, tech: ${techCtxHit})` : "Clean" - }; + } catch (err) { + core.error(`Failed to load detection: ${err.message}`); + core.setFailed(`Detection script error`); + return; } let totalScanned = 0; @@ -132,67 +153,48 @@ jobs: if (scanIssues) { core.info("Scanning open issues..."); let page = 1; - let hasMore = true; - while (hasMore && (maxIssues === 0 || totalScanned < maxIssues)) { + while (maxIssues === 0 || totalScanned < maxIssues) { const issues = await github.rest.issues.listForRepo({ - owner, - repo, - state: 'open', - per_page: 100, - page: page + owner, repo, state: 'open', per_page: 100, page }); - if (issues.data.length === 0) { - hasMore = false; - break; - } + if (issues.data.length === 0) break; for (const issue of issues.data) { - if (issue.pull_request) continue; // Skip PRs for now + if (issue.pull_request) continue; if (maxIssues > 0 && totalScanned >= maxIssues) break; totalScanned++; - const body = issue.body || ""; - const actor = issue.user?.login || "unknown"; - const assoc = issue.author_association || "NONE"; - - // Skip trusted users - if (["OWNER", "MEMBER", "COLLABORATOR"].includes(assoc)) { - continue; - } - - const analysis = analyzeContent(body, actor); + const analysis = await analyzeContent( + issue.body || "", + issue.user?.login || "unknown", + issue.author_association || "NONE" + ); if (analysis.isSpam) { totalSpam++; - core.warning(`Found spam issue #${issue.number} by @${actor}: ${analysis.reason}`); - core.warning(`Preview: ${body.substring(0, 200)}...`); + core.warning(`[SPAM] Issue #${issue.number} by @${issue.user?.login}`); + core.warning(` Preview: ${(issue.body || "").substring(0, 150)}...`); if (!dryRun) { try { await github.rest.issues.update({ - owner, - repo, - issue_number: issue.number, - state: "closed", - state_reason: "not_planned" + owner, repo, issue_number: issue.number, + state: "closed", state_reason: "not_planned" }); await github.rest.issues.createComment({ - owner, - repo, - issue_number: issue.number, + owner, repo, issue_number: issue.number, body: "This issue has been automatically closed as spam during cleanup." }); totalClosed++; - core.notice(`Closed spam issue #${issue.number}`); + core.notice(`✓ Closed spam issue #${issue.number}`); } catch (err) { - core.error(`Failed to close issue #${issue.number}: ${err.message}`); + core.error(`✗ Failed to close issue #${issue.number}: ${err.message}`); } } } } - page++; } } @@ -201,60 +203,42 @@ jobs: if (scanComments) { core.info("Scanning issue comments..."); let page = 1; - let hasMore = true; let commentCount = 0; - while (hasMore) { + while (page <= 10) { const comments = await github.rest.issues.listCommentsForRepo({ - owner, - repo, - per_page: 100, - page: page, - sort: 'created', - direction: 'desc' + owner, repo, per_page: 100, page, sort: 'created', direction: 'desc' }); - if (comments.data.length === 0) { - hasMore = false; - break; - } + if (comments.data.length === 0) break; for (const comment of comments.data) { commentCount++; - const body = comment.body || ""; - const actor = comment.user?.login || "unknown"; - const assoc = comment.author_association || "NONE"; - - // Skip trusted users - if (["OWNER", "MEMBER", "COLLABORATOR"].includes(assoc)) { - continue; - } - - const analysis = analyzeContent(body, actor); + const analysis = await analyzeContent( + comment.body || "", + comment.user?.login || "unknown", + comment.author_association || "NONE" + ); if (analysis.isSpam) { totalSpam++; - core.warning(`Found spam comment #${comment.id} by @${actor}: ${analysis.reason}`); - core.warning(`Preview: ${body.substring(0, 200)}...`); + core.warning(`[SPAM] Comment #${comment.id} by @${comment.user?.login}`); + core.warning(` Preview: ${(comment.body || "").substring(0, 150)}...`); if (!dryRun) { try { await github.rest.issues.deleteComment({ - owner, - repo, - comment_id: comment.id + owner, repo, comment_id: comment.id }); totalDeleted++; - core.notice(`Deleted spam comment #${comment.id}`); + core.notice(`✓ Deleted spam comment #${comment.id}`); } catch (err) { - core.error(`Failed to delete comment #${comment.id}: ${err.message}`); + core.error(`✗ Failed to delete comment #${comment.id}: ${err.message}`); } } } } - page++; - if (page > 10) break; // Limit to first 1000 comments to avoid timeout } core.info(`Scanned ${commentCount} comments`); @@ -262,14 +246,13 @@ jobs: // Summary core.notice("=".repeat(60)); - core.notice(`Cleanup Summary (Dry run: ${dryRun})`); + core.notice(`Cleanup Summary ${dryRun ? '(DRY RUN)' : '(EXECUTED)'}`); core.notice(`Total scanned: ${totalScanned} issues`); core.notice(`Total spam found: ${totalSpam}`); if (!dryRun) { core.notice(`Issues closed: ${totalClosed}`); core.notice(`Comments deleted: ${totalDeleted}`); } else { - core.notice("DRY RUN - No actions taken. Set dry_run to 'false' to actually clean up."); + core.notice("DRY RUN - No actions taken. Set dry_run to 'false' to execute."); } core.notice("=".repeat(60)); -