semantic bufo search find-bufo.com
bufo
1
fork

Configure Feed

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

fix timestamp generation, remove quote_chance

- fix getIsoTimestamp to generate real timestamps instead of hardcoded placeholder
- remove quote_chance config and always quote-post on match
- update README to reflect removed quote_chance option

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

zzstoatzz 6fcd5932 1ab45154

+34 -43
+1 -2
bot/README.md
··· 6 6 7 7 1. connects to bluesky jetstream (firehose) 8 8 2. for each post, checks if text contains an exact phrase matching a bufo name 9 - 3. if matched, quote-posts with the corresponding bufo image (or posts without quote based on quote_chance) 9 + 3. if matched, quote-posts with the corresponding bufo image 10 10 11 11 ## matching logic 12 12 ··· 23 23 | `MIN_PHRASE_WORDS` | `4` | minimum words in phrase to match | 24 24 | `POSTING_ENABLED` | `false` | must be `true` to actually post | 25 25 | `COOLDOWN_MINUTES` | `120` | don't repost same bufo within this time | 26 - | `QUOTE_CHANCE` | `0.5` | probability of quoting vs just posting with rkey | 27 26 | `EXCLUDE_PATTERNS` | `...` | exclude bufos matching these patterns | 28 27 | `JETSTREAM_ENDPOINT` | `jetstream2.us-east.bsky.network` | jetstream server | 29 28
+22 -4
bot/src/bsky.zig
··· 117 117 var body_buf: std.ArrayList(u8) = .{}; 118 118 defer body_buf.deinit(self.allocator); 119 119 120 + var ts_buf: [30]u8 = undefined; 120 121 try body_buf.print(self.allocator, 121 122 \\{{"repo":"{s}","collection":"app.bsky.feed.post","record":{{"$type":"app.bsky.feed.post","text":"","createdAt":"{s}","embed":{{"$type":"app.bsky.embed.recordWithMedia","record":{{"$type":"app.bsky.embed.record","record":{{"uri":"{s}","cid":"{s}"}}}},"media":{{"$type":"app.bsky.embed.images","images":[{{"image":{s},"alt":"{s}"}}]}}}}}}}} 122 - , .{ self.did.?, getIsoTimestamp(), quote_uri, quote_cid, blob_json, alt_text }); 123 + , .{ self.did.?, getIsoTimestamp(&ts_buf), quote_uri, quote_cid, blob_json, alt_text }); 123 124 124 125 var auth_buf: [512]u8 = undefined; 125 126 const auth_header = std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{self.access_jwt.?}) catch return error.AuthTooLong; ··· 156 157 var body_buf: std.ArrayList(u8) = .{}; 157 158 defer body_buf.deinit(self.allocator); 158 159 160 + var ts_buf: [30]u8 = undefined; 159 161 try body_buf.print(self.allocator, 160 162 \\{{"repo":"{s}","collection":"app.bsky.feed.post","record":{{"$type":"app.bsky.feed.post","text":"{s}","createdAt":"{s}","embed":{{"$type":"app.bsky.embed.images","images":[{{"image":{s},"alt":"{s}"}}]}}}}}} 161 - , .{ self.did.?, text, getIsoTimestamp(), blob_json, alt_text }); 163 + , .{ self.did.?, text, getIsoTimestamp(&ts_buf), blob_json, alt_text }); 162 164 163 165 var auth_buf: [512]u8 = undefined; 164 166 const auth_header = std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{self.access_jwt.?}) catch return error.AuthTooLong; ··· 252 254 } 253 255 }; 254 256 255 - fn getIsoTimestamp() []const u8 { 256 - return "2025-01-01T00:00:00.000Z"; 257 + fn getIsoTimestamp(buf: *[30]u8) []const u8 { 258 + const ts = std.time.timestamp(); 259 + const epoch_secs: u64 = @intCast(ts); 260 + const epoch = std.time.epoch.EpochSeconds{ .secs = epoch_secs }; 261 + const day = epoch.getEpochDay(); 262 + const year_day = day.calculateYearDay(); 263 + const month_day = year_day.calculateMonthDay(); 264 + const day_secs = epoch.getDaySeconds(); 265 + 266 + const len = std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.000Z", .{ 267 + year_day.year, 268 + month_day.month.numeric(), 269 + month_day.day_index + 1, 270 + day_secs.getHoursIntoDay(), 271 + day_secs.getMinutesIntoHour(), 272 + day_secs.getSecondsIntoMinute(), 273 + }) catch return "2025-01-01T00:00:00.000Z"; 274 + return buf[0..len.len]; 257 275 }
-9
bot/src/config.zig
··· 8 8 min_phrase_words: u32, 9 9 posting_enabled: bool, 10 10 cooldown_minutes: u32, 11 - quote_chance: f32, 12 11 exclude_patterns: []const u8, 13 12 14 13 pub fn fromEnv() Config { ··· 19 18 .min_phrase_words = parseU32(posix.getenv("MIN_PHRASE_WORDS"), 4), 20 19 .posting_enabled = parseBool(posix.getenv("POSTING_ENABLED")), 21 20 .cooldown_minutes = parseU32(posix.getenv("COOLDOWN_MINUTES"), 120), 22 - .quote_chance = parseF32(posix.getenv("QUOTE_CHANCE"), 0.5), 23 21 .exclude_patterns = posix.getenv("EXCLUDE_PATTERNS") orelse "what-have-you-done,what-have-i-done,sad,crying,cant-take", 24 22 }; 25 23 } ··· 28 26 fn parseU32(str: ?[]const u8, default: u32) u32 { 29 27 if (str) |s| { 30 28 return std.fmt.parseInt(u32, s, 10) catch default; 31 - } 32 - return default; 33 - } 34 - 35 - fn parseF32(str: ?[]const u8, default: f32) f32 { 36 - if (str) |s| { 37 - return std.fmt.parseFloat(f32, s) catch default; 38 29 } 39 30 return default; 40 31 }
+11 -28
bot/src/main.zig
··· 18 18 bsky_client: bsky.BskyClient, 19 19 recent_bufos: std.StringHashMap(i64), // name -> timestamp 20 20 mutex: Thread.Mutex = .{}, 21 - rng: std.Random.DefaultPrng, 22 21 }; 23 22 24 23 pub fn main() !void { ··· 57 56 .matcher = m, 58 57 .bsky_client = bsky_client, 59 58 .recent_bufos = std.StringHashMap(i64).init(allocator), 60 - .rng = std.Random.DefaultPrng.init(@intCast(std.time.timestamp())), 61 59 }; 62 60 defer state.recent_bufos.deinit(); 63 61 ··· 94 92 return; 95 93 } 96 94 } 97 - 98 - // random quote chance 99 - const should_quote = state.rng.random().float(f32) < state.config.quote_chance; 100 95 101 96 // fetch bufo image 102 97 const img_data = state.bsky_client.fetchImage(match.url) catch |err| { ··· 136 131 } 137 132 const alt_text = alt_buf[0..alt_len]; 138 133 139 - if (should_quote) { 140 - // get post CID for quote 141 - const cid = state.bsky_client.getPostCid(post.uri) catch |err| { 142 - std.debug.print("failed to get post CID: {}\n", .{err}); 143 - return; 144 - }; 145 - defer state.allocator.free(cid); 134 + // get post CID for quote 135 + const cid = state.bsky_client.getPostCid(post.uri) catch |err| { 136 + std.debug.print("failed to get post CID: {}\n", .{err}); 137 + return; 138 + }; 139 + defer state.allocator.free(cid); 146 140 147 - state.bsky_client.createQuotePost(post.uri, cid, blob_json, alt_text) catch |err| { 148 - std.debug.print("failed to create quote post: {}\n", .{err}); 149 - return; 150 - }; 151 - std.debug.print("posted bufo quote: {s}\n", .{match.name}); 152 - } else { 153 - // post without quote 154 - var text_buf: [256]u8 = undefined; 155 - const text = std.fmt.bufPrint(&text_buf, "matched {s} (not quoting to reduce spam)", .{post.rkey}) catch "matched a post"; 156 - 157 - state.bsky_client.createSimplePost(text, blob_json, alt_text) catch |err| { 158 - std.debug.print("failed to create simple post: {}\n", .{err}); 159 - return; 160 - }; 161 - std.debug.print("posted bufo (no quote): {s} for {s}\n", .{ match.name, post.rkey }); 162 - } 141 + state.bsky_client.createQuotePost(post.uri, cid, blob_json, alt_text) catch |err| { 142 + std.debug.print("failed to create quote post: {}\n", .{err}); 143 + return; 144 + }; 145 + std.debug.print("posted bufo quote: {s}\n", .{match.name}); 163 146 164 147 // update cooldown cache 165 148 state.recent_bufos.put(match.name, now) catch {};