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
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
**Languages:** Markdown (386 lines), Other (1298 lines)
**Changes:** 3 files, 66 lines
**Languages:** Other (66 lines)

View File

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

View File

@ -41,16 +41,18 @@ actor MentionBot {
private let token: AuthToken
private let ai: OpenRouterClient
private let logger: BotLogger
private let stateManager: StateManager
private let botUsername: String
private let dryRun: Bool
private var processedNotificationIds: Set<String> = []
private var processedThisSession: Set<String> = []
init(
api: DevRantRequest,
token: AuthToken,
ai: OpenRouterClient,
logger: BotLogger,
stateManager: StateManager,
botUsername: String,
dryRun: Bool = false
) {
@ -58,6 +60,7 @@ actor MentionBot {
self.token = token
self.ai = ai
self.logger = logger
self.stateManager = stateManager
self.botUsername = botUsername.lowercased()
self.dryRun = dryRun
}
@ -80,9 +83,12 @@ actor MentionBot {
do {
let mentions = try await fetchExternalMentions()
let lastTime = await stateManager.lastMentionTime
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 {
@ -92,7 +98,7 @@ actor MentionBot {
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)
}
@ -116,12 +122,12 @@ actor MentionBot {
}
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)")
return
}
processedNotificationIds.insert(mention.identifiers.guid)
processedThisSession.insert(mention.identifiers.guid)
do {
let (rant, comments) = try await api.getRant(
@ -151,6 +157,8 @@ actor MentionBot {
response: response
)
await stateManager.updateLastMentionTime(mention.createdTime)
} catch {
await logger.error("Failed to process mention: \(error)")
}
@ -215,7 +223,7 @@ actor MentionBot {
}
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)
}
}