A Bluesky Playdate client
at main 316 lines 11 kB view raw
1const std = @import("std"); 2const pdapi = @import("playdate_api_definitions.zig"); 3 4pub const NetworkState = enum { 5 idle, 6 connecting, 7 authenticating, 8 requesting, 9 fetching_posts, 10 success, 11 network_error, 12}; 13 14// HTTP method types 15pub const HttpMethod = enum { 16 GET, 17 POST, 18 PUT, 19 DELETE, 20 PATCH, 21}; 22 23// HTTP status code ranges 24pub const HttpStatusCode = u16; 25 26// Callback function types 27pub const SuccessCallback = *const fn (status_code: HttpStatusCode, response_data: []const u8) void; 28pub const FailureCallback = *const fn (status_code: HttpStatusCode, error_message: []const u8) void; 29 30// Request configuration 31pub const HttpRequest = struct { 32 method: HttpMethod, 33 url: []const u8, 34 server: []const u8, 35 port: u16, 36 path: []const u8, 37 use_https: bool, 38 bearer_token: ?[]const u8, 39 body: ?[]const u8, 40 response_buffer: []u8, 41 success_callback: ?SuccessCallback, 42 failure_callback: ?FailureCallback, 43}; 44 45// Network request state 46pub const NetworkRequestState = struct { 47 state: NetworkState, 48 connection: ?*pdapi.HTTPConnection, 49 request: ?HttpRequest, 50 bytes_read: usize, 51 is_reading_chunks: bool, 52 total_bytes_read: usize, 53 expected_content_length: usize, 54 network_access_requested: bool, 55}; 56 57// Global network state 58var g_network_state = NetworkRequestState{ 59 .state = .idle, 60 .connection = null, 61 .request = null, 62 .bytes_read = 0, 63 .is_reading_chunks = false, 64 .total_bytes_read = 0, 65 .expected_content_length = 0, 66 .network_access_requested = false, 67}; 68 69// Global buffers to avoid large stack allocations 70var g_headers_buffer: [512]u8 = undefined; 71 72/// Convert HttpMethod enum to string 73fn httpMethodToString(method: HttpMethod) []const u8 { 74 return switch (method) { 75 .GET => "GET", 76 .POST => "POST", 77 .PUT => "PUT", 78 .DELETE => "DELETE", 79 .PATCH => "PATCH", 80 }; 81} 82 83/// Create HTTP connection with proper configuration 84pub fn createConnection(playdate: *pdapi.PlaydateAPI, server: []const u8, port: u16, use_https: bool) ?*pdapi.HTTPConnection { 85 // Create a null-terminated buffer for the server name 86 var server_buffer: [256]u8 = undefined; 87 if (server.len >= server_buffer.len) { 88 playdate.system.logToConsole("ERROR: Server name too long: %d", @as(c_int, @intCast(server.len))); 89 return null; 90 } 91 @memcpy(server_buffer[0..server.len], server); 92 server_buffer[server.len] = 0; 93 94 playdate.system.logToConsole("Attempting to create connection to %s:%d", @as([*:0]const u8, @ptrCast(&server_buffer)), port); 95 if (use_https) { 96 playdate.system.logToConsole("Using HTTPS connection"); 97 } else { 98 playdate.system.logToConsole("Using HTTP connection"); 99 } 100 const connection = playdate.network.playdate_http.newConnection(@as([*:0]const u8, @ptrCast(&server_buffer)), port, use_https); 101 102 // Set connection timeout - increase to 30 seconds for more reliability 103 playdate.network.playdate_http.setConnectTimeout(connection, 30000); // 30 seconds 104 playdate.network.playdate_http.setReadTimeout(connection, 30000); // 30 seconds 105 playdate.system.logToConsole("HTTP connection created with 30s timeouts"); 106 107 return connection; 108} 109 110/// Start an HTTP request with optional bearer token 111pub fn makeHttpRequest(playdate: *pdapi.PlaydateAPI, request: HttpRequest) bool { 112 // Check if already processing a request 113 if (g_network_state.state != .idle) { 114 playdate.system.logToConsole("ERROR: Network request already in progress"); 115 return false; 116 } 117 118 // Create connection 119 const connection = createConnection(playdate, request.server, request.port, request.use_https); 120 if (connection == null) { 121 return false; 122 } 123 124 // Store request info 125 g_network_state.connection = connection; 126 g_network_state.request = request; 127 g_network_state.state = .connecting; 128 g_network_state.bytes_read = 0; 129 g_network_state.total_bytes_read = 0; 130 g_network_state.is_reading_chunks = false; 131 132 // Build headers 133 var headers_len: usize = 0; 134 135 // Add Authorization header if bearer token provided 136 if (request.bearer_token) |token| { 137 const auth_header = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Authorization: Bearer {s}\r\n", .{token}) catch { 138 playdate.system.logToConsole("ERROR: Failed to format authorization header"); 139 return false; 140 }; 141 headers_len += auth_header.len; 142 } 143 144 // Add Content-Type header for POST/PUT/PATCH requests 145 if (request.method == .POST or request.method == .PUT or request.method == .PATCH) { 146 const content_type = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Content-Type: application/json\r\n", .{}) catch { 147 playdate.system.logToConsole("ERROR: Failed to format content-type header"); 148 return false; 149 }; 150 headers_len += content_type.len; 151 } 152 153 // Null terminate headers 154 if (headers_len < g_headers_buffer.len) { 155 g_headers_buffer[headers_len] = 0; 156 } 157 158 // Send request using query method 159 const method_str = httpMethodToString(request.method); 160 const headers_ptr = if (headers_len > 0) @as([*:0]const u8, @ptrCast(&g_headers_buffer)) else null; 161 const body_ptr = if (request.body) |body| @as([*:0]const u8, @ptrCast(body.ptr)) else null; 162 const body_len = if (request.body) |body| body.len else 0; 163 164 const result = playdate.network.playdate_http.query(connection, @as([*:0]const u8, @ptrCast(method_str.ptr)), @as([*:0]const u8, @ptrCast(request.path.ptr)), headers_ptr, if (headers_len > 0) headers_len else 0, body_ptr, body_len); 165 166 if (result != .NET_OK) { 167 playdate.system.logToConsole("ERROR: Failed to send HTTP request"); 168 g_network_state.state = .network_error; 169 return false; 170 } 171 172 g_network_state.state = .requesting; 173 playdate.system.logToConsole("HTTP %s request sent to %s%s", @as([*:0]const u8, @ptrCast(method_str.ptr)), @as([*:0]const u8, @ptrCast(request.server.ptr)), @as([*:0]const u8, @ptrCast(request.path.ptr))); 174 175 return true; 176} 177 178/// Process network responses and handle callbacks 179pub fn updateNetwork(playdate: *pdapi.PlaydateAPI) void { 180 if (g_network_state.state == .idle or g_network_state.connection == null) { 181 return; 182 } 183 184 const connection = g_network_state.connection.?; 185 _ = g_network_state.request.?; 186 187 // Check for errors 188 const net_error = playdate.network.playdate_http.getError(connection); 189 if (net_error != .NET_OK) { 190 playdate.system.logToConsole("ERROR: HTTP network error: %d", @intFromEnum(net_error)); 191 handleNetworkError(playdate, "Network error"); 192 return; 193 } 194 195 // Read available data if we're in requesting state 196 if (g_network_state.state == .requesting) { 197 const bytes_available = playdate.network.playdate_http.getBytesAvailable(connection); 198 if (bytes_available > 0) { 199 readResponseData(playdate); 200 } else { 201 // Check if request is complete by trying to get response status 202 const status_code = playdate.network.playdate_http.getResponseStatus(connection); 203 if (status_code > 0) { 204 // We have a response, process it 205 processResponse(playdate); 206 } 207 } 208 } 209} 210 211/// Read response data into the provided buffer 212fn readResponseData(playdate: *pdapi.PlaydateAPI) void { 213 const connection = g_network_state.connection.?; 214 const request = g_network_state.request.?; 215 const available_space = request.response_buffer.len - g_network_state.bytes_read; 216 217 if (available_space == 0) { 218 playdate.system.logToConsole("WARNING: Response buffer full, stopping read"); 219 processResponse(playdate); 220 return; 221 } 222 223 const bytes_to_read = @min(available_space, 1024); // Read in chunks of 1KB or less 224 const buffer_ptr = request.response_buffer[g_network_state.bytes_read .. g_network_state.bytes_read + bytes_to_read]; 225 226 const bytes_read = playdate.network.playdate_http.read(connection, @as([*]u8, @ptrCast(buffer_ptr.ptr)), @intCast(bytes_to_read)); 227 228 if (bytes_read > 0) { 229 g_network_state.bytes_read += @intCast(bytes_read); 230 g_network_state.total_bytes_read += @intCast(bytes_read); 231 playdate.system.logToConsole("Read %d bytes, total: %d", bytes_read, g_network_state.total_bytes_read); 232 } 233} 234 235/// Process the completed response and call appropriate callback 236fn processResponse(playdate: *pdapi.PlaydateAPI) void { 237 const connection = g_network_state.connection.?; 238 const request = g_network_state.request.?; 239 240 // Get HTTP status code 241 const status_code = playdate.network.playdate_http.getResponseStatus(connection); 242 playdate.system.logToConsole("HTTP response status: %d", status_code); 243 244 // Null-terminate response data 245 if (g_network_state.bytes_read < request.response_buffer.len) { 246 request.response_buffer[g_network_state.bytes_read] = 0; 247 } 248 249 const response_data = request.response_buffer[0..g_network_state.bytes_read]; 250 251 // Determine if this is a success (2xx) or failure (4xx/5xx) 252 if (status_code >= 200 and status_code < 300) { 253 // Success (2xx) 254 g_network_state.state = .success; 255 if (request.success_callback) |callback| { 256 callback(@intCast(status_code), response_data); 257 } 258 } else if (status_code >= 400) { 259 // Client/Server error (4xx/5xx) 260 g_network_state.state = .network_error; 261 if (request.failure_callback) |callback| { 262 callback(@intCast(status_code), response_data); 263 } 264 } else { 265 // Other status codes (1xx, 3xx) - treat as success for now 266 g_network_state.state = .success; 267 if (request.success_callback) |callback| { 268 callback(@intCast(status_code), response_data); 269 } 270 } 271 272 // Cleanup 273 cleanup(playdate); 274} 275 276/// Handle network errors 277fn handleNetworkError(playdate: *pdapi.PlaydateAPI, error_message: []const u8) void { 278 g_network_state.state = .network_error; 279 280 if (g_network_state.request) |request| { 281 if (request.failure_callback) |callback| { 282 callback(0, error_message); // Status code 0 indicates network error 283 } 284 } 285 286 cleanup(playdate); 287} 288 289/// Clean up network resources 290fn cleanup(playdate: *pdapi.PlaydateAPI) void { 291 if (g_network_state.connection) |connection| { 292 playdate.network.playdate_http.close(connection); 293 g_network_state.connection = null; 294 } 295 296 g_network_state.request = null; 297 g_network_state.bytes_read = 0; 298 g_network_state.total_bytes_read = 0; 299 g_network_state.is_reading_chunks = false; 300 g_network_state.state = .idle; 301} 302 303/// Get current network state 304pub fn getNetworkState() NetworkState { 305 return g_network_state.state; 306} 307 308/// Check if network is idle 309pub fn isNetworkIdle() bool { 310 return g_network_state.state == .idle; 311} 312 313/// Force cleanup network resources 314pub fn forceCleanup(playdate: *pdapi.PlaydateAPI) void { 315 cleanup(playdate); 316}