// retoor <retoor@molodetz.nl>
import Foundation
import ArgumentParser
import SwiftDevRant
@preconcurrency import KreeRequest
struct NetworkLogger: Logger, @unchecked Sendable {
let enabled: Bool
func log(_ message: String) {
if enabled {
print("[NET] \(message)")
}
}
}
@main
struct Grokii: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "grokii",
abstract: "AI-powered assistant bot for devRant",
discussion: """
Grokii is an intelligent bot that monitors devRant for @mentions and responds
using AI. It can answer questions, provide programming help, and engage in
developer discussions. Additionally includes spam detection capabilities
to help maintain community quality.
Requires OPENROUTER_API_KEY environment variable for AI functionality.
""",
version: "2.0.0",
subcommands: [Chat.self, Spam.self],
defaultSubcommand: Chat.self
)
}
struct CommonOptions: ParsableArguments {
@Option(name: .shortAndLong, help: "devRant username for bot account")
var username: String
@Option(name: .shortAndLong, help: "devRant password for bot account")
var password: String
@Flag(name: .shortAndLong, help: "Run continuously in daemon mode")
var continuous: Bool = false
@Option(name: .shortAndLong, help: "Polling interval in seconds")
var interval: Int = 30
@Flag(name: .long, help: "Dry run mode - no posting or voting")
var dryRun: Bool = false
@Option(name: .shortAndLong, help: "Path to log file")
var logFile: String?
@Flag(name: .long, help: "Enable verbose debug logging")
var debug: Bool = false
}
extension Grokii {
struct Chat: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Monitor mentions and respond with AI",
discussion: """
Monitors devRant notifications for @mentions of the bot account.
When mentioned, Grokii extracts the question, gathers context from
the rant and comments, and generates an AI response.
The bot understands programming questions, can explain code concepts,
and engages naturally in developer discussions.
"""
)
@OptionGroup var options: CommonOptions
@Option(name: .long, help: "OpenRouter model ID")
var model: String = "x-ai/grok-code-fast-1"
@Option(name: .long, help: "Custom system prompt for AI personality")
var systemPrompt: String?
@Option(name: .long, help: "Maximum tokens in AI response")
var maxTokens: Int = 500
@Option(name: .long, help: "AI temperature (0.0-2.0)")
var temperature: Double = 0.7
mutating func run() async throws {
let logger = BotLogger(
logPath: options.logFile,
logToConsole: true,
minLevel: options.debug ? .debug : .info
)
await logger.info("Grokii v2.0.0 - AI Chat Mode")
await logger.info("Model: \(model)")
guard let apiKey = ProcessInfo.processInfo.environment["OPENROUTER_API_KEY"] else {
await logger.error("OPENROUTER_API_KEY environment variable not set")
await logger.error("Get your API key at: https://openrouter.ai/keys")
throw ExitCode.failure
}
await logger.info("Authenticating as @\(options.username)...")
let networkLogger = NetworkLogger(enabled: options.debug)
let api = DevRantRequest(requestLogger: networkLogger, ignoreCertificateErrors: true)
let token: AuthToken
do {
token = try await api.logIn(username: options.username, password: options.password)
await logger.info("Authentication successful")
} catch {
await logger.error("Authentication failed: \(error)")
throw ExitCode.failure
}
let ai = OpenRouterClient(
apiKey: apiKey,
model: model,
systemPrompt: systemPrompt,
maxTokens: maxTokens,
temperature: temperature
)
let stateManager = StateManager()
let bot = MentionBot(
api: api,
token: token,
ai: ai,
logger: logger,
stateManager: stateManager,
botUsername: options.username,
dryRun: options.dryRun
)
if options.continuous {
await logger.info("Running in continuous mode (interval: \(options.interval)s)")
}
await withTaskCancellationHandler {
await bot.run(continuous: options.continuous, intervalSeconds: options.interval)
let processedCount = await bot.getStats()
await logger.info("Session complete. Processed \(processedCount) mentions.")
} onCancel: {
Task {
await logger.info("Received shutdown signal. Exiting gracefully...")
}
}
}
}
struct Spam: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Detect and downvote spam content",
discussion: """
Scans devRant feeds for spam content using pattern matching and
behavioral analysis. Detected spam is automatically downvoted
with the 'offensive/spam' reason.
Detection includes: crypto scams, self-promotion, adult content,
phishing, gambling, and malware links.
"""
)
@OptionGroup var options: CommonOptions
mutating func run() async throws {
let logger = BotLogger(
logPath: options.logFile,
logToConsole: true,
minLevel: options.debug ? .debug : .info
)
await logger.info("Grokii v2.0.0 - Spam Detection Mode")
await logger.info("Authenticating as @\(options.username)...")
let networkLogger = NetworkLogger(enabled: options.debug)
let api = DevRantRequest(requestLogger: networkLogger, ignoreCertificateErrors: true)
let token: AuthToken
do {
token = try await api.logIn(username: options.username, password: options.password)
await logger.info("Authentication successful")
} catch {
await logger.error("Authentication failed: \(error)")
throw ExitCode.failure
}
let scanner = SpamScanner(
api: api,
token: token,
logger: logger,
dryRun: options.dryRun
)
if options.continuous {
await logger.info("Running in continuous mode (interval: \(options.interval)s)")
}
await withTaskCancellationHandler {
await scanner.run(continuous: options.continuous, intervalSeconds: options.interval)
let stats = await scanner.getStats()
await logger.info("Session complete.")
await logger.info("Scanned: \(stats.rants) rants, \(stats.comments) comments")
await logger.info("Flagged: \(stats.spammers) suspicious users")
} onCancel: {
Task {
await logger.info("Received shutdown signal. Exiting gracefully...")
}
}
}
}
}