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 rate limit (MAX_POSTS_PER_HOUR, default 3)

ring buffer of recent post timestamps, checked before posting. set to
0 to disable. configurable via env var without redeploy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

zzstoatzz 9295f451 232133ab

+24
+2
bot/src/config.zig
··· 9 9 cooldown_minutes: u32, 10 10 cooldown_scale_factor: u32, 11 11 length_multiplier: u32, 12 + max_posts_per_hour: u32, 12 13 exclude_patterns: []const u8, 13 14 stats_port: u16, 14 15 backend_url: []const u8, ··· 23 24 .cooldown_minutes = parseU32(getenv("COOLDOWN_MINUTES"), 120), 24 25 .cooldown_scale_factor = parseU32(getenv("COOLDOWN_SCALE_FACTOR"), 400), 25 26 .length_multiplier = parseU32(getenv("LENGTH_MULTIPLIER"), 6), 27 + .max_posts_per_hour = parseU32(getenv("MAX_POSTS_PER_HOUR"), 3), 26 28 .exclude_patterns = getenv("EXCLUDE_PATTERNS") orelse "what-have-you-done,what-have-i-done,sad,crying,cant-take,knife,what-are-you-doing-with-that,\\brip\\b,fire,ventilator,carnage,riot,grenade,pitchfork,sobbing,breakdown,meltdown,panic,pleading,cry,call-for-help", 27 29 .stats_port = parseU16(getenv("STATS_PORT"), 8080), 28 30 .backend_url = getenv("BACKEND_URL") orelse "https://find-bufo.com",
+22
bot/src/main.zig
··· 20 20 21 21 var global_state: ?*BotState = null; 22 22 23 + const MAX_RATE_LIMIT_SLOTS = 64; 24 + 23 25 const BotState = struct { 24 26 allocator: Allocator, 25 27 config: config.Config, ··· 27 29 bsky_client: bsky.BskyClient, 28 30 mutex: Io.Mutex = Io.Mutex.init, 29 31 stats: stats.Stats, 32 + // ring buffer of recent post timestamps for global rate limiting 33 + recent_posts: [MAX_RATE_LIMIT_SLOTS]i64 = [_]i64{0} ** MAX_RATE_LIMIT_SLOTS, 34 + recent_post_idx: usize = 0, 30 35 }; 31 36 32 37 pub fn main() !void { ··· 173 178 } 174 179 } 175 180 181 + // global rate limit (0 = disabled) 182 + if (state.config.max_posts_per_hour > 0) { 183 + const one_hour_ago = now - 3600; 184 + var recent_count: u32 = 0; 185 + for (state.recent_posts) |ts| { 186 + if (ts > one_hour_ago) recent_count += 1; 187 + } 188 + if (recent_count >= state.config.max_posts_per_hour) { 189 + std.debug.print("rate limit: {} posts in last hour, skipping {s}\n", .{ recent_count, match.name }); 190 + return; 191 + } 192 + } 193 + 176 194 // check if poster blocks us 177 195 const is_blocked = state.bsky_client.isBlockedBy(post.did) catch |err| blk: { 178 196 std.debug.print("block check failed: {}, proceeding with post\n", .{err}); ··· 267 285 268 286 std.debug.print("posted bufo quote: {s} (rkey: {s})\n", .{ match.name, our_rkey }); 269 287 state.stats.incPostsCreated(); 288 + 289 + // record for global rate limiting 290 + state.recent_posts[state.recent_post_idx] = now; 291 + state.recent_post_idx = (state.recent_post_idx + 1) % MAX_RATE_LIMIT_SLOTS; 270 292 271 293 // track our post for cleanup on delete/block 272 294 state.stats.addTrackedPost(our_rkey, post.uri, post.did, now);