feat: add persistent state management for mention processing

This commit is contained in:
retoor 2025-12-20 20:52:45 +01:00
parent 4554a8a58f
commit eca01e8427
4 changed files with 63 additions and 9 deletions

View File

@ -4,7 +4,7 @@
## Version 0.1.0 - 2025-12-20 ## Version 0.1.0 - 2025-12-20
Adds Grokii, an AI-powered devRant bot that supports chat interactions and spam detection modes. Developers can now integrate this bot to enhance community engagement and moderation on devRant platforms. Adds persistent state management for mention processing, ensuring continuity across application restarts.
**Changes:** 9 files, 1684 lines **Changes:** 3 files, 66 lines
**Languages:** Markdown (386 lines), Other (1298 lines) **Languages:** Other (66 lines)

View File

@ -123,11 +123,14 @@ extension Grokii {
temperature: temperature temperature: temperature
) )
let stateManager = StateManager()
let bot = MentionBot( let bot = MentionBot(
api: api, api: api,
token: token, token: token,
ai: ai, ai: ai,
logger: logger, logger: logger,
stateManager: stateManager,
botUsername: options.username, botUsername: options.username,
dryRun: options.dryRun dryRun: options.dryRun
) )

View File

@ -41,16 +41,18 @@ actor MentionBot {
private let token: AuthToken private let token: AuthToken
private let ai: OpenRouterClient private let ai: OpenRouterClient
private let logger: BotLogger private let logger: BotLogger
private let stateManager: StateManager
private let botUsername: String private let botUsername: String
private let dryRun: Bool private let dryRun: Bool
private var processedNotificationIds: Set<String> = [] private var processedThisSession: Set<String> = []
init( init(
api: DevRantRequest, api: DevRantRequest,
token: AuthToken, token: AuthToken,
ai: OpenRouterClient, ai: OpenRouterClient,
logger: BotLogger, logger: BotLogger,
stateManager: StateManager,
botUsername: String, botUsername: String,
dryRun: Bool = false dryRun: Bool = false
) { ) {
@ -58,6 +60,7 @@ actor MentionBot {
self.token = token self.token = token
self.ai = ai self.ai = ai
self.logger = logger self.logger = logger
self.stateManager = stateManager
self.botUsername = botUsername.lowercased() self.botUsername = botUsername.lowercased()
self.dryRun = dryRun self.dryRun = dryRun
} }
@ -80,9 +83,12 @@ actor MentionBot {
do { do {
let mentions = try await fetchExternalMentions() let mentions = try await fetchExternalMentions()
let lastTime = await stateManager.lastMentionTime
let newMentions = mentions.filter { mention in let newMentions = mentions.filter { mention in
mention.to.lowercased() == botUsername && !processedNotificationIds.contains(mention.identifiers.guid) mention.to.lowercased() == botUsername &&
mention.createdTime > lastTime &&
!processedThisSession.contains(mention.identifiers.guid)
} }
if newMentions.isEmpty { if newMentions.isEmpty {
@ -92,7 +98,7 @@ actor MentionBot {
await logger.info("Found \(newMentions.count) new mention(s)") await logger.info("Found \(newMentions.count) new mention(s)")
for mention in newMentions { for mention in newMentions.sorted(by: { $0.createdTime < $1.createdTime }) {
await processMention(mention) await processMention(mention)
} }
@ -116,12 +122,12 @@ actor MentionBot {
} }
private func processMention(_ mention: ExternalMention) async { private func processMention(_ mention: ExternalMention) async {
guard !processedNotificationIds.contains(mention.identifiers.guid) else { guard !processedThisSession.contains(mention.identifiers.guid) else {
await logger.debug("Already processed mention: \(mention.identifiers.guid)") await logger.debug("Already processed mention: \(mention.identifiers.guid)")
return return
} }
processedNotificationIds.insert(mention.identifiers.guid) processedThisSession.insert(mention.identifiers.guid)
do { do {
let (rant, comments) = try await api.getRant( let (rant, comments) = try await api.getRant(
@ -151,6 +157,8 @@ actor MentionBot {
response: response response: response
) )
await stateManager.updateLastMentionTime(mention.createdTime)
} catch { } catch {
await logger.error("Failed to process mention: \(error)") await logger.error("Failed to process mention: \(error)")
} }
@ -215,7 +223,7 @@ actor MentionBot {
} }
func getStats() -> Int { func getStats() -> Int {
return processedNotificationIds.count return processedThisSession.count
} }
} }

View File

@ -0,0 +1,43 @@
// retoor <retoor@molodetz.nl>
import Foundation
actor StateManager {
private struct State: Codable {
var lastMentionTime: Int
}
private let stateURL: URL
private var state: State
init() {
let homeDir = FileManager.default.homeDirectoryForCurrentUser
let grokiiDir = homeDir.appendingPathComponent(".grokii")
try? FileManager.default.createDirectory(at: grokiiDir, withIntermediateDirectories: true)
self.stateURL = grokiiDir.appendingPathComponent("state.json")
if let data = try? Data(contentsOf: stateURL),
let loaded = try? JSONDecoder().decode(State.self, from: data) {
self.state = loaded
} else {
self.state = State(lastMentionTime: 0)
}
}
var lastMentionTime: Int {
state.lastMentionTime
}
func updateLastMentionTime(_ time: Int) {
guard time > state.lastMentionTime else { return }
state.lastMentionTime = time
save()
}
private func save() {
guard let data = try? JSONEncoder().encode(state) else { return }
try? data.write(to: stateURL, options: .atomic)
}
}