// retoor import Foundation import SwiftDevRant struct SpamActions: Sendable { let api: DevRantRequest let token: AuthToken let logger: BotLogger let dryRun: Bool init(api: DevRantRequest, token: AuthToken, logger: BotLogger, dryRun: Bool = false) { self.api = api self.token = token self.logger = logger self.dryRun = dryRun } func downvoteRant(_ rantId: Int, reason: DownvoteReason = .offensiveOrSpam) async { guard !dryRun else { await logger.info("DRY RUN: Would downvote rant \(rantId)") return } do { _ = try await api.voteOnRant(token: token, rantId: rantId, vote: .downvoted, downvoteReason: reason) await logger.info("Downvoted rant \(rantId)") } catch { await logger.error("Failed to downvote rant \(rantId): \(error)") } } func downvoteComment(_ commentId: Int, reason: DownvoteReason = .offensiveOrSpam) async { guard !dryRun else { await logger.info("DRY RUN: Would downvote comment \(commentId)") return } do { _ = try await api.voteOnComment(token: token, commentId: commentId, vote: .downvoted, downvoteReason: reason) await logger.info("Downvoted comment \(commentId)") } catch { await logger.error("Failed to downvote comment \(commentId): \(error)") } } func upvoteRant(_ rantId: Int) async { guard !dryRun else { await logger.info("DRY RUN: Would upvote rant \(rantId)") return } do { _ = try await api.voteOnRant(token: token, rantId: rantId, vote: .upvoted) await logger.info("Upvoted rant \(rantId)") } catch { await logger.error("Failed to upvote rant \(rantId): \(error)") } } func upvoteComment(_ commentId: Int) async { guard !dryRun else { await logger.info("DRY RUN: Would upvote comment \(commentId)") return } do { _ = try await api.voteOnComment(token: token, commentId: commentId, vote: .upvoted) await logger.info("Upvoted comment \(commentId)") } catch { await logger.error("Failed to upvote comment \(commentId): \(error)") } } } actor SpamScanner { private let api: DevRantRequest private let token: AuthToken private let detector: SpamDetector private let actions: SpamActions private let logger: BotLogger private var processedRantIds: Set = [] private var processedCommentIds: Set = [] private var userPostCache: [Int: [PostInfo]] = [:] private var spammerIds: Set = [] init(api: DevRantRequest, token: AuthToken, logger: BotLogger, dryRun: Bool = false) { self.api = api self.token = token self.detector = SpamDetector() self.actions = SpamActions(api: api, token: token, logger: logger, dryRun: dryRun) self.logger = logger } func run(continuous: Bool, intervalSeconds: Int) async { await logger.info("SpamScanner started") repeat { await scanRecentFeed() await scanAlgorithmFeed() if continuous { await logger.info("Sleeping for \(intervalSeconds) seconds...") try? await Task.sleep(nanoseconds: UInt64(intervalSeconds) * 1_000_000_000) } } while continuous } private func scanRecentFeed() async { await logger.debug("Scanning recent feed...") do { let feed = try await api.getRantFeed(token: token, sort: .recent, limit: 50, skip: 0, sessionHash: nil) await processRants(feed.rants) } catch { await logger.error("Failed to fetch recent feed: \(error)") } } private func scanAlgorithmFeed() async { await logger.debug("Scanning algorithm feed...") do { let feed = try await api.getRantFeed(token: token, sort: .algorithm, limit: 50, skip: 0, sessionHash: nil) await processRants(feed.rants) } catch { await logger.error("Failed to fetch algorithm feed: \(error)") } } private func processRants(_ rants: [Rant]) async { for rant in rants { guard !processedRantIds.contains(rant.id) else { continue } processedRantIds.insert(rant.id) await analyzeRant(rant) do { let (_, comments) = try await api.getRant(token: token, rantId: rant.id, lastCommentId: nil) await processComments(comments) } catch { await logger.error("Failed to fetch comments for rant \(rant.id): \(error)") } } } private func processComments(_ comments: [Comment]) async { for comment in comments { guard !processedCommentIds.contains(comment.id) else { continue } processedCommentIds.insert(comment.id) await analyzeComment(comment) } } private func analyzeRant(_ rant: Rant) async { let analysis = detector.analyze(rant.text) trackUserPost(PostInfo( id: rant.id, text: rant.text, created: rant.created, authorId: rant.author.id )) if analysis.isSpam { await logger.spam("RANT SPAM [id=\(rant.id)] [user=\(rant.author.name)] [patterns=\(analysis.matchedPatterns.joined(separator: ","))]") await actions.downvoteRant(rant.id) } let userPosts = userPostCache[rant.author.id] ?? [] let behaviorAnalysis = detector.analyzeUserBehavior(posts: userPosts) if behaviorAnalysis.isSuspicious && !spammerIds.contains(rant.author.id) { spammerIds.insert(rant.author.id) await logger.spam("SUSPICIOUS USER [id=\(rant.author.id)] [name=\(rant.author.name)]") } } private func analyzeComment(_ comment: Comment) async { let analysis = detector.analyze(comment.text) trackUserPost(PostInfo( id: comment.id, text: comment.text, created: comment.created, authorId: comment.author.id )) if analysis.isSpam { await logger.spam("COMMENT SPAM [id=\(comment.id)] [user=\(comment.author.name)] [patterns=\(analysis.matchedPatterns.joined(separator: ","))]") await actions.downvoteComment(comment.id) } let userPosts = userPostCache[comment.author.id] ?? [] let behaviorAnalysis = detector.analyzeUserBehavior(posts: userPosts) if behaviorAnalysis.isSuspicious && !spammerIds.contains(comment.author.id) { spammerIds.insert(comment.author.id) await logger.spam("SUSPICIOUS USER [id=\(comment.author.id)] [name=\(comment.author.name)]") } } private func trackUserPost(_ post: PostInfo) { var posts = userPostCache[post.authorId] ?? [] posts.append(post) if posts.count > 50 { posts = Array(posts.suffix(50)) } userPostCache[post.authorId] = posts } func getStats() -> (rants: Int, comments: Int, spammers: Int) { return (processedRantIds.count, processedCommentIds.count, spammerIds.count) } }