semantic bufo search find-bufo.com
bufo

fix: handle expired auth token by re-logging in

detect ExpiredToken errors from bsky API and automatically
re-login with stored app password, then retry the operation.

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

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

+32 -28
+8
bot/src/bsky.zig
··· 163 163 if (result.status != .ok) { 164 164 const err_response = aw.toArrayList(); 165 165 std.debug.print("upload blob failed with status: {} - {s}\n", .{ result.status, err_response.items }); 166 + // check for expired token 167 + if (mem.indexOf(u8, err_response.items, "ExpiredToken") != null) { 168 + return error.ExpiredToken; 169 + } 166 170 return error.UploadFailed; 167 171 } 168 172 ··· 316 320 if (result.status != .ok) { 317 321 const err_response = aw.toArrayList(); 318 322 std.debug.print("get service auth failed with status: {} - {s}\n", .{ result.status, err_response.items }); 323 + // check for expired token 324 + if (mem.indexOf(u8, err_response.items, "ExpiredToken") != null) { 325 + return error.ExpiredToken; 326 + } 319 327 return error.ServiceAuthFailed; 320 328 } 321 329
+24 -28
bot/src/main.zig
··· 93 93 } 94 94 } 95 95 96 - // fetch bufo image 97 - const img_data = state.bsky_client.fetchImage(match.url) catch |err| { 98 - std.debug.print("failed to fetch bufo image: {}\n", .{err}); 99 - return; 96 + // try to post, with one retry on token expiration 97 + tryPost(state, post, match, now) catch |err| { 98 + if (err == error.ExpiredToken) { 99 + std.debug.print("token expired, re-logging in...\n", .{}); 100 + state.bsky_client.login() catch |login_err| { 101 + std.debug.print("failed to re-login: {}\n", .{login_err}); 102 + return; 103 + }; 104 + std.debug.print("re-login successful, retrying post...\n", .{}); 105 + tryPost(state, post, match, now) catch |retry_err| { 106 + std.debug.print("retry failed: {}\n", .{retry_err}); 107 + }; 108 + } 100 109 }; 110 + } 111 + 112 + fn tryPost(state: *BotState, post: jetstream.Post, match: matcher.Match, now: i64) !void { 113 + // fetch bufo image 114 + const img_data = try state.bsky_client.fetchImage(match.url); 101 115 defer state.allocator.free(img_data); 102 116 103 117 const is_gif = mem.endsWith(u8, match.url, ".gif"); ··· 119 133 const alt_text = alt_buf[0..alt_len]; 120 134 121 135 // get post CID for quote 122 - const cid = state.bsky_client.getPostCid(post.uri) catch |err| { 123 - std.debug.print("failed to get post CID: {}\n", .{err}); 124 - return; 125 - }; 136 + const cid = try state.bsky_client.getPostCid(post.uri); 126 137 defer state.allocator.free(cid); 127 138 128 139 if (is_gif) { 129 140 // upload as video for animated GIFs 130 141 std.debug.print("uploading {d} bytes as video\n", .{img_data.len}); 131 - const job_id = state.bsky_client.uploadVideo(img_data, match.name) catch |err| { 132 - std.debug.print("failed to upload video: {}\n", .{err}); 133 - return; 134 - }; 142 + const job_id = try state.bsky_client.uploadVideo(img_data, match.name); 135 143 defer state.allocator.free(job_id); 136 144 137 145 std.debug.print("waiting for video processing (job: {s})...\n", .{job_id}); 138 - const blob_json = state.bsky_client.waitForVideo(job_id) catch |err| { 139 - std.debug.print("failed to wait for video: {}\n", .{err}); 140 - return; 141 - }; 146 + const blob_json = try state.bsky_client.waitForVideo(job_id); 142 147 defer state.allocator.free(blob_json); 143 148 144 - state.bsky_client.createVideoQuotePost(post.uri, cid, blob_json, alt_text) catch |err| { 145 - std.debug.print("failed to create video quote post: {}\n", .{err}); 146 - return; 147 - }; 149 + try state.bsky_client.createVideoQuotePost(post.uri, cid, blob_json, alt_text); 148 150 } else { 149 151 // upload as image 150 152 const content_type = if (mem.endsWith(u8, match.url, ".png")) ··· 153 155 "image/jpeg"; 154 156 155 157 std.debug.print("uploading {d} bytes as {s}\n", .{ img_data.len, content_type }); 156 - const blob_json = state.bsky_client.uploadBlob(img_data, content_type) catch |err| { 157 - std.debug.print("failed to upload blob: {}\n", .{err}); 158 - return; 159 - }; 158 + const blob_json = try state.bsky_client.uploadBlob(img_data, content_type); 160 159 defer state.allocator.free(blob_json); 161 160 162 - state.bsky_client.createQuotePost(post.uri, cid, blob_json, alt_text) catch |err| { 163 - std.debug.print("failed to create quote post: {}\n", .{err}); 164 - return; 165 - }; 161 + try state.bsky_client.createQuotePost(post.uri, cid, blob_json, alt_text); 166 162 } 167 163 std.debug.print("posted bufo quote: {s}\n", .{match.name}); 168 164