diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c5808f..208fdc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Sources/Grokii/Main.swift b/Sources/Grokii/Main.swift index ecee99d..fd36c0a 100644 --- a/Sources/Grokii/Main.swift +++ b/Sources/Grokii/Main.swift @@ -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 ) diff --git a/Sources/Grokii/MentionBot.swift b/Sources/Grokii/MentionBot.swift index b3126f2..4245500 100644 --- a/Sources/Grokii/MentionBot.swift +++ b/Sources/Grokii/MentionBot.swift @@ -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 = [] + private var processedThisSession: Set = [] 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 } } diff --git a/Sources/Grokii/StateManager.swift b/Sources/Grokii/StateManager.swift new file mode 100644 index 0000000..809616e --- /dev/null +++ b/Sources/Grokii/StateManager.swift @@ -0,0 +1,43 @@ +// retoor + +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) + } +}