package ChatApp::HTTP; use strict; use warnings; use Future::AsyncAwait; use JSON::MaybeXS; use File::Spec; use File::Basename qw(dirname); use PAGI::App::Router; use ChatApp::State qw( get_all_rooms get_room get_room_messages get_room_users get_stats ); my $JSON = JSON::MaybeXS->new->utf8->canonical; # Get the public directory path my $PUBLIC_DIR = File::Spec->catdir(dirname(__FILE__), '..', '..', 'public'); # MIME types for static files my %MIME_TYPES = ( html => 'text/html; charset=utf-8', css => 'text/css; charset=utf-8', js => 'application/javascript; charset=utf-8', json => 'application/json; charset=utf-8', png => 'image/png', jpg => 'image/jpeg', jpeg => 'image/jpeg', gif => 'image/gif', svg => 'image/svg+xml', ico => 'image/x-icon', woff => 'font/woff', woff2=> 'font/woff2', ); # API Handlers sub _rooms_handler { return async sub { my ($scope, $receive, $send) = @_; my $rooms = get_all_rooms(); my $data = [ map { { name => $_->{name}, users => scalar(keys %{$_->{users}}), created_at => $_->{created_at}, } } sort { $a->{name} cmp $b->{name} } values %$rooms ]; await _send_json($send, 200, $data); }; } sub _room_history_handler { return async sub { my ($scope, $receive, $send) = @_; my $room_name = $scope->{path_params}{name}; my $room = get_room($room_name); if ($room) { my $data = get_room_messages($room_name, 100); await _send_json($send, 200, $data); } else { await _send_json($send, 404, { error => 'Room not found' }); } }; } sub _room_users_handler { return async sub { my ($scope, $receive, $send) = @_; my $room_name = $scope->{path_params}{name}; my $room = get_room($room_name); if ($room) { my $data = get_room_users($room_name); await _send_json($send, 200, $data); } else { await _send_json($send, 404, { error => 'Room not found' }); } }; } sub _stats_handler { return async sub { my ($scope, $receive, $send) = @_; my $data = get_stats(); await _send_json($send, 200, $data); }; } async sub _send_json { my ($send, $status, $data) = @_; my $body = $JSON->encode($data); await $send->({ type => 'http.response.start', status => $status, headers => [ ['content-type', 'application/json; charset=utf-8'], ['content-length', length($body)], ['cache-control', 'no-cache'], ], }); await $send->({ type => 'http.response.body', body => $body, more => 0, }); } sub handler { my $router = PAGI::App::Router->new; # API routes $router->get('/api/rooms' => _rooms_handler()); $router->get('/api/room/:name/history' => _room_history_handler()); $router->get('/api/room/:name/users' => _room_users_handler()); $router->get('/api/stats' => _stats_handler()); my $api_app = $router->to_app; return async sub { my ($scope, $receive, $send) = @_; my $path = $scope->{path} // '/'; # Route API requests through router if ($path =~ m{^/api/}) { return await $api_app->($scope, $receive, $send); } # Serve static files return await _serve_static($scope, $receive, $send, $path); }; } async sub _serve_static { my ($scope, $receive, $send, $path) = @_; # Default to index.html $path = '/index.html' if $path eq '/'; # Security: prevent directory traversal $path =~ s/\.\.//g; $path =~ s|//+|/|g; my $file_path = File::Spec->catfile($PUBLIC_DIR, $path); # Check if file exists and is readable unless (-f $file_path && -r $file_path) { return await _send_404($send); } # Get file extension and MIME type my ($ext) = $file_path =~ /\.(\w+)$/; my $content_type = $MIME_TYPES{lc($ext // '')} // 'application/octet-stream'; # Read file content my $content; { open my $fh, '<:raw', $file_path or return await _send_500($send); local $/; $content = <$fh>; close $fh; } # Send response await $send->({ type => 'http.response.start', status => 200, headers => [ ['content-type', $content_type], ['content-length', length($content)], ['cache-control', 'public, max-age=3600'], ], }); await $send->({ type => 'http.response.body', body => $content, more => 0, }); } async sub _send_404 { my ($send) = @_; my $body = '{"error":"Not found"}'; await $send->({ type => 'http.response.start', status => 404, headers => [ ['content-type', 'application/json'], ['content-length', length($body)], ], }); await $send->({ type => 'http.response.body', body => $body, more => 0, }); } async sub _send_500 { my ($send) = @_; my $body = '{"error":"Internal server error"}'; await $send->({ type => 'http.response.start', status => 500, headers => [ ['content-type', 'application/json'], ['content-length', length($body)], ], }); await $send->({ type => 'http.response.body', body => $body, more => 0, }); } 1; __END__ # NAME ChatApp::HTTP - HTTP request handler for the chat application # DESCRIPTION Handles HTTP requests including static file serving and API endpoints. Uses PAGI::App::Router for declarative API routing with parameter capture. ## API Endpoints - **GET /api/rooms** - Returns list of all rooms with user counts. - **GET /api/room/:name/history** - Returns message history for a room. The `:name` parameter is captured by the router and available in `$scope->{path_params}{name}`. - **GET /api/room/:name/users** - Returns list of users in a room. - **GET /api/stats** - Returns server statistics. # SEE ALSO PAGI::App::Router