semantic bufo search find-bufo.com
bufo
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(bot): add global cooldown to throttle overall post rate

per-bufo cooldowns only prevent repeats of the same bufo — different
bufos matching in quick succession all post. adds a 30min global
cooldown (GLOBAL_COOLDOWN_MINUTES) as a minimum gap between any post.
match stats still track all matches regardless of cooldown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

zzstoatzz 41fc25fa e47d2d81

+37 -2
+2
bot/src/config.zig
··· 8 8 min_phrase_words: u32, 9 9 posting_enabled: bool, 10 10 cooldown_minutes: u32, 11 + global_cooldown_minutes: u32, 11 12 exclude_patterns: []const u8, 12 13 stats_port: u16, 13 14 backend_url: []const u8, ··· 20 21 .min_phrase_words = parseU32(posix.getenv("MIN_PHRASE_WORDS"), 4), 21 22 .posting_enabled = parseBool(posix.getenv("POSTING_ENABLED")), 22 23 .cooldown_minutes = parseU32(posix.getenv("COOLDOWN_MINUTES"), 120), 24 + .global_cooldown_minutes = parseU32(posix.getenv("GLOBAL_COOLDOWN_MINUTES"), 30), 23 25 .exclude_patterns = posix.getenv("EXCLUDE_PATTERNS") orelse "what-have-you-done,what-have-i-done,sad,crying,cant-take,knife,what-are-you-doing-with-that", 24 26 .stats_port = parseU16(posix.getenv("STATS_PORT"), 8080), 25 27 .backend_url = posix.getenv("BACKEND_URL") orelse "https://find-bufo.com",
+14 -2
bot/src/main.zig
··· 138 138 state.mutex.lock(); 139 139 defer state.mutex.unlock(); 140 140 141 - // check cooldown (scaled by match frequency, persisted across restarts) 142 141 const now = std.time.timestamp(); 142 + 143 + // global cooldown: minimum time between any post regardless of bufo 144 + const global_cooldown_secs: i64 = @intCast(@as(u64, state.config.global_cooldown_minutes) * 60); 145 + if (state.stats.getLastGlobalPost()) |last_global| { 146 + if (now - last_global < global_cooldown_secs) { 147 + state.stats.incCooldownsHit(); 148 + std.debug.print("global cooldown: {} min remaining, skipping\n", .{@divTrunc(@as(u64, @intCast(global_cooldown_secs - (now - last_global))), 60)}); 149 + return; 150 + } 151 + } 152 + 153 + // per-bufo cooldown (scaled by match frequency, persisted across restarts) 143 154 const base_secs: u64 = @as(u64, state.config.cooldown_minutes) * 60; 144 155 const cooldown_secs: i64 = @intCast(state.stats.getCooldownSeconds(match.name, base_secs)); 145 156 ··· 248 259 // track our post for cleanup on delete/block 249 260 state.stats.addTrackedPost(our_rkey, post.uri, post.did, now); 250 261 251 - // update cooldown cache (persisted to disk) 262 + // update cooldown caches (persisted to disk) 252 263 state.stats.setLastPosted(match.name, now); 264 + state.stats.setLastGlobalPost(now); 253 265 } 254 266 255 267 fn onDelete(did: []const u8, rkey: []const u8) void {
+21
bot/src/stats.zig
··· 34 34 bufo_mutex: Thread.Mutex = .{}, 35 35 // track last post time per bufo (persisted to survive restarts) 36 36 last_posted: std.StringHashMap(i64), 37 + // global cooldown: timestamp of most recent post (any bufo) 38 + last_global_post: ?i64 = null, 37 39 // track our quote-posts for cleanup on delete/block 38 40 tracked_posts: std.ArrayList(TrackedPost), 39 41 ··· 108 110 }; 109 111 if (root.get("cumulative_uptime")) |v| if (v == .integer) { 110 112 self.prior_uptime = @intCast(@max(0, v.integer)); 113 + }; 114 + if (root.get("last_global_post")) |v| if (v == .integer) { 115 + self.last_global_post = v.integer; 111 116 }; 112 117 113 118 // load bufo_matches (or legacy bufo_posts) ··· 274 279 std.fmt.format(writer, "\"blocks_respected\":{},", .{self.blocks_respected.load(.monotonic)}) catch return; 275 280 std.fmt.format(writer, "\"errors\":{},", .{self.errors.load(.monotonic)}) catch return; 276 281 std.fmt.format(writer, "\"cumulative_uptime\":{},", .{total_uptime}) catch return; 282 + if (self.last_global_post) |lgp| { 283 + std.fmt.format(writer, "\"last_global_post\":{},", .{lgp}) catch return; 284 + } 277 285 writer.writeAll("\"bufo_matches\":{") catch return; 278 286 279 287 var first = true; ··· 348 356 self.allocator.free(key); 349 357 }; 350 358 } 359 + self.saveUnlocked(); 360 + } 361 + 362 + pub fn getLastGlobalPost(self: *Stats) ?i64 { 363 + self.bufo_mutex.lock(); 364 + defer self.bufo_mutex.unlock(); 365 + return self.last_global_post; 366 + } 367 + 368 + pub fn setLastGlobalPost(self: *Stats, timestamp: i64) void { 369 + self.bufo_mutex.lock(); 370 + defer self.bufo_mutex.unlock(); 371 + self.last_global_post = timestamp; 351 372 self.saveUnlocked(); 352 373 } 353 374