feat: add persistent state management for mention processing
This commit is contained in:
parent
4554a8a58f
commit
eca01e8427
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
Sources/Grokii/StateManager.swift
Normal file
43
Sources/Grokii/StateManager.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user