|
// 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...")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|