diff options
Diffstat (limited to 'lib/Travelynx/Controller/Traveling.pm')
-rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 1829 |
1 files changed, 1324 insertions, 505 deletions
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index ffc4211..89385e1 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -1,338 +1,465 @@ package Travelynx::Controller::Traveling; -# Copyright (C) 2020 Daniel Friesel +# Copyright (C) 2020-2023 Birte Kristina Friesel # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Controller'; use DateTime; use DateTime::Format::Strptime; -use JSON; -use List::Util qw(uniq min max); -use List::UtilsBy qw(max_by uniq_by); +use List::Util qw(uniq min max); +use List::UtilsBy qw(max_by uniq_by); use List::MoreUtils qw(first_index); +use Mojo::Promise; use Text::CSV; use Travel::Status::DE::IRIS::Stations; -sub homepage { - my ($self) = @_; - if ( $self->is_user_authenticated ) { - $self->render( - 'landingpage', - version => $self->app->config->{version} // 'UNKNOWN', - with_autocomplete => 1, - with_geolocation => 1 - ); - $self->users->mark_seen( uid => $self->current_user->{id} ); - } - else { - $self->render( - 'landingpage', - version => $self->app->config->{version} // 'UNKNOWN', - intro => 1 - ); +# Internal Helpers + +sub has_str_in_list { + my ( $str, @strs ) = @_; + if ( List::Util::any { $str eq $_ } @strs ) { + return 1; } + return; } -sub user_status { - my ($self) = @_; - - my $name = $self->stash('name'); - my $ts = $self->stash('ts') // 0; - my $user = $self->users->get_privacy_by_name( name => $name ); +sub get_connecting_trains_p { + my ( $self, %opt ) = @_; - if ( not $user or not $user->{public_level} & 0x03 ) { - $self->render('not_found'); - return; - } + my $uid = $opt{uid} //= $self->current_user->{id}; + my $use_history = $self->users->use_history( uid => $uid ); - if ( $user->{public_level} & 0x01 and not $self->is_user_authenticated ) { - $self->render( 'login', redirect_to => $self->req->url ); - return; - } + my ( $eva, $exclude_via, $exclude_train_id, $exclude_before ); + my $now = $self->now->epoch; + my ( $stationinfo, $arr_epoch, $arr_platform, $arr_countdown ); - my $status = $self->get_user_status( $user->{id} ); - my $journey; + my $promise = Mojo::Promise->new; - if ( - $ts - and ( not $status->{checked_in} - or $status->{sched_departure}->epoch != $ts ) - and ( $user->{public_level} & 0x20 - or - ( $user->{public_level} & 0x10 and $self->is_user_authenticated ) ) - ) - { - for my $candidate ( - $self->journeys->get( - uid => $user->{id}, - limit => 10, - ) - ) - { - if ( $candidate->{sched_dep_ts} eq $ts ) { - $journey = $self->journeys->get_single( - uid => $user->{id}, - journey_id => $candidate->{id}, - verbose => 1, - with_datetime => 1, - with_polyline => 1, - ); - } + if ( $opt{eva} ) { + if ( $use_history & 0x01 ) { + $eva = $opt{eva}; } - } - - my %tw_data = ( - card => 'summary', - site => '@derfnull', - image => $self->url_for('/static/icons/icon-512x512.png') - ->to_abs->scheme('https'), - ); - my %og_data = ( - type => 'article', - image => $tw_data{image}, - url => $self->url_for("/status/${name}")->to_abs->scheme('https'), - site_name => 'travelynx', - ); - - if ($journey) { - $og_data{title} = $tw_data{title} = sprintf( 'Fahrt von %s nach %s', - $journey->{from_name}, $journey->{to_name} ); - $og_data{description} = $tw_data{description} - = $journey->{rt_arrival}->strftime('Ankunft am %d.%m.%Y um %H:%M'); - $og_data{url} .= "/${ts}"; - } - elsif ( - $ts - and ( not $status->{checked_in} - or $status->{sched_departure}->epoch != $ts ) - ) - { - $og_data{title} = $tw_data{title} = "Bahnfahrt beendet"; - $og_data{description} = $tw_data{description} - = "${name} hat das Ziel erreicht"; - } - elsif ( $status->{checked_in} ) { - $og_data{url} .= '/' . $status->{sched_departure}->epoch; - $og_data{title} = $tw_data{title} = "${name} ist unterwegs"; - $og_data{description} = $tw_data{description} = sprintf( - '%s %s von %s nach %s', - $status->{train_type}, $status->{train_line} // $status->{train_no}, - $status->{dep_name}, $status->{arr_name} // 'irgendwo' - ); - if ( $status->{real_arrival}->epoch ) { - $tw_data{description} .= $status->{real_arrival} - ->strftime(' – Ankunft gegen %H:%M Uhr'); - $og_data{description} .= $status->{real_arrival} - ->strftime(' – Ankunft gegen %H:%M Uhr'); + elsif ( $opt{destination_name} ) { + $eva = $opt{eva}; } } else { - $og_data{title} = $tw_data{title} - = "${name} ist gerade nicht eingecheckt"; - $og_data{description} = $tw_data{description} - = "Letztes Fahrtziel: $status->{arr_name}"; - } - - if ($journey) { - if ( not $user->{public_level} & 0x04 ) { - delete $journey->{user_data}{comment}; + if ( $use_history & 0x02 ) { + my $status = $self->get_user_status; + $eva = $status->{arr_eva}; + $exclude_via = $status->{dep_name}; + $exclude_train_id = $status->{train_id}; + $arr_platform = $status->{arr_platform}; + $stationinfo = $status->{extra_data}{stationinfo_arr}; + if ( $status->{real_arrival} ) { + $exclude_before = $arr_epoch = $status->{real_arrival}->epoch; + $arr_countdown = $status->{arrival_countdown}; + } } - my $map_data = $self->journeys_to_map_data( - journeys => [$journey], - include_manual => 1, - ); - $self->render( - 'journey', - error => undef, - with_map => 1, - readonly => 1, - journey => $journey, - twitter => \%tw_data, - opengraph => \%og_data, - %{$map_data}, - ); } - else { - $self->render( - 'user_status', - name => $name, - public_level => $user->{public_level}, - journey => $status, - twitter => \%tw_data, - opengraph => \%og_data, - ); + + $exclude_before //= $now - 300; + + if ( not $eva ) { + return $promise->reject; } -} -sub public_profile { - my ($self) = @_; + my ( $dest_ids, $destinations ) + = $self->journeys->get_connection_targets(%opt); - my $name = $self->stash('name'); - my $user = $self->users->get_privacy_by_name( name => $name ); + my @destinations = uniq_by { $_->{name} } @{$destinations}; - if ( - $user - and ( $user->{public_level} & 0x22 - or - ( $user->{public_level} & 0x11 and $self->is_user_authenticated ) ) - ) - { - my $status = $self->get_user_status( $user->{id} ); - my @journeys; - if ( $user->{public_level} & 0x40 ) { - @journeys = $self->journeys->get( - uid => $user->{id}, - limit => 10, - with_datetime => 1 - ); - } - else { - my $now = DateTime->now( time_zone => 'Europe/Berlin' ); - my $month_ago = $now->clone->subtract( weeks => 4 ); - @journeys = $self->journeys->get( - uid => $user->{id}, - limit => 10, - with_datetime => 1, - after => $month_ago, - before => $now - ); - } - $self->render( - 'profile', - name => $name, - uid => $user->{id}, - public_level => $user->{public_level}, - journey => $status, - journeys => [@journeys], - version => $self->app->config->{version} // 'UNKNOWN', - ); + if ($exclude_via) { + @destinations = grep { $_->{name} ne $exclude_via } @destinations; } - else { - $self->render('not_found'); + + if ( not @destinations ) { + return $promise->reject; } -} -sub public_journey_details { - my ($self) = @_; - my $name = $self->stash('name'); - my $journey_id = $self->stash('id'); - my $user = $self->users->get_privacy_by_name( name => $name ); + my $iris_eva = $eva; + if ( $eva < 8000000 ) { + $iris_eva = ( List::Util::first { $_ >= 8000000 } @{$dest_ids} ) + // $eva; + } - $self->param( journey_id => $journey_id ); + my $can_check_in = not $arr_epoch || ( $arr_countdown // 1 ) < 0; + my $lookahead + = $can_check_in ? 40 : ( ( ${arr_countdown} // 0 ) / 60 + 40 ); - if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) { - $self->render( - 'journey', - status => 404, - error => 'notfound', - journey => {} - ); - return; - } + my $iris_promise = Mojo::Promise->new; + my %via_count = map { $_->{name} => 0 } @destinations; - if ( - $user - and ( $user->{public_level} & 0x20 - or - ( $user->{public_level} & 0x10 and $self->is_user_authenticated ) ) - ) + if ( $iris_eva >= 8000000 + and List::Util::any { $_->{eva} >= 8000000 } @destinations ) { - my $journey = $self->journeys->get_single( - uid => $user->{id}, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, - with_polyline => 1, - ); + $self->iris->get_departures_p( + station => $iris_eva, + lookbehind => 10, + lookahead => $lookahead, + with_related => 1 + )->then( + sub { + my ($stationboard) = @_; + if ( $stationboard->{errstr} ) { + $iris_promise->resolve( [] ); + return; + } - if ( not( $user->{public_level} & 0x40 ) ) { - my $month_ago = DateTime->now( time_zone => 'Europe/Berlin' ) - ->subtract( weeks => 4 )->epoch; - if ( $journey and $journey->{rt_dep_ts} < $month_ago ) { - $journey = undef; - } - } + @{ $stationboard->{results} } = map { $_->[0] } + sort { $a->[1] <=> $b->[1] } + map { [ $_, $_->departure ? $_->departure->epoch : 0 ] } + @{ $stationboard->{results} }; + my @results; + my @cancellations; + my $excluded_train; + for my $train ( @{ $stationboard->{results} } ) { + if ( not $train->departure ) { + next; + } + if ( $exclude_before + and $train->departure + and $train->departure->epoch < $exclude_before ) + { + next; + } + if ( $exclude_train_id + and $train->train_id eq $exclude_train_id ) + { + $excluded_train = $train; + next; + } + + # In general, this function is meant to return feasible + # connections. However, cancelled connections may also be of + # interest and are also useful for logging cancellations. + # To satisfy both demands with (hopefully) little confusion and + # UI clutter, this function returns two concatenated arrays: + # actual connections (ordered by actual departure time) followed + # by cancelled connections (ordered by scheduled departure time). + # This is easiest to achieve in two separate loops. + # + # Note that a cancelled train may still have a matching destination + # in its route_post, e.g. if it leaves out $eva due to + # unscheduled route changes but continues on schedule afterwards + # -- so it is only cancelled at $eva, not on the remainder of + # the route. Also note that this specific case is not yet handled + # properly by the cancellation logic etc. + + if ( $train->departure_is_cancelled ) { + my @via = ( + $train->sched_route_post, $train->sched_route_end + ); + for my $dest (@destinations) { + if ( has_str_in_list( $dest->{name}, @via ) ) { + push( @cancellations, [ $train, $dest ] ); + next; + } + } + } + else { + my @via = ( $train->route_post, $train->route_end ); + for my $dest (@destinations) { + if ( $via_count{ $dest->{name} } < 2 + and has_str_in_list( $dest->{name}, @via ) ) + { + push( @results, [ $train, $dest ] ); + + # Show all past and up to two future departures per destination + if ( not $train->departure + or $train->departure->epoch >= $now ) + { + $via_count{ $dest->{name} }++; + } + next; + } + } + } + } - if ($journey) { - my $title = sprintf( 'Fahrt von %s nach %s am %s', - $journey->{from_name}, $journey->{to_name}, - $journey->{rt_arrival}->strftime('%d.%m.%Y') ); - my $description = sprintf( 'Ankunft mit %s %s %s', - $journey->{type}, $journey->{no}, - $journey->{rt_arrival}->strftime('um %H:%M') ); - my %tw_data = ( - card => 'summary', - site => '@derfnull', - image => $self->url_for('/static/icons/icon-512x512.png') - ->to_abs->scheme('https'), - title => $title, - description => $description, - ); - my %og_data = ( - type => 'article', - image => $tw_data{image}, - url => $self->url_for->to_abs, - site_name => 'travelynx', - title => $title, - description => $description, - ); + @results = map { $_->[0] } + sort { $a->[1] <=> $b->[1] } + map { + [ + $_, + $_->[0]->departure->epoch + // $_->[0]->sched_departure->epoch + ] + } @results; + @cancellations = map { $_->[0] } + sort { $a->[1] <=> $b->[1] } + map { [ $_, $_->[0]->sched_departure->epoch ] } + @cancellations; + + # remove trains whose route matches the excluded one's + if ($excluded_train) { + my $route_pre + = join( '|', reverse $excluded_train->route_pre ); + @results + = grep { join( '|', $_->[0]->route_post ) ne $route_pre } + @results; + my $route_post = join( '|', $excluded_train->route_post ); + @results + = grep { join( '|', $_->[0]->route_post ) ne $route_post } + @results; + } - my $map_data = $self->journeys_to_map_data( - journeys => [$journey], - include_manual => 1, - ); - if ( $journey->{user_data}{comment} - and not $user->{public_level} & 0x04 ) - { - delete $journey->{user_data}{comment}; + # add message IDs and 'transfer short' hints + for my $result (@results) { + my $train = $result->[0]; + my @message_ids + = List::Util::uniq map { $_->[1] } $train->raw_messages; + $train->{message_id} = { map { $_ => 1 } @message_ids }; + my $interchange_duration; + if ( exists $stationinfo->{i} ) { + if ( defined $arr_platform + and defined $train->platform ) + { + $interchange_duration + = $stationinfo->{i}{$arr_platform} + { $train->platform }; + } + $interchange_duration //= $stationinfo->{i}{"*"}; + } + if ( defined $interchange_duration ) { + my $interchange_time + = ( $train->departure->epoch - $arr_epoch ) / 60; + if ( $interchange_time < $interchange_duration ) { + $train->{interchange_text} = 'Anschluss knapp'; + $train->{interchange_icon} = 'directions_run'; + } + elsif ( $interchange_time == $interchange_duration ) { + $train->{interchange_text} + = 'Anschluss könnte knapp werden'; + $train->{interchange_icon} = 'directions_run'; + } + } + } + + $iris_promise->resolve( [ @results, @cancellations ] ); + return; } - $self->render( - 'journey', - error => undef, - journey => $journey, - with_map => 1, - username => $name, - readonly => 1, - twitter => \%tw_data, - opengraph => \%og_data, - %{$map_data}, - ); - } - else { - $self->render('not_found'); - } + )->catch( + sub { + $iris_promise->resolve( [] ); + return; + } + )->wait; } else { - $self->render('not_found'); + $iris_promise->resolve( [] ); } -} -sub public_status_card { - my ($self) = @_; + my $hafas_promise = Mojo::Promise->new; + $self->hafas->get_departures_p( + eva => $eva, + lookbehind => 10, + lookahead => $lookahead + )->then( + sub { + my ($status) = @_; + $hafas_promise->resolve( [ $status->results ] ); + return; + } + )->catch( + sub { + # HAFAS data is optional. + # Errors are logged by get_json_p and can be silently ignored here. + $hafas_promise->resolve( [] ); + return; + } + )->wait; + + Mojo::Promise->all( $iris_promise, $hafas_promise )->then( + sub { + my ( $iris, $hafas ) = @_; + my @iris_trains = @{ $iris->[0] }; + my @all_hafas_trains = @{ $hafas->[0] }; + my @hafas_trains; + + # We've already got a list of connecting trains; this function + # only adds further information to them. We ignore errors, as + # partial data is better than no data. + eval { + for my $iris_train (@iris_trains) { + if ( $iris_train->[0]->departure_is_cancelled ) { + for my $hafas_train (@all_hafas_trains) { + if ( $hafas_train->number + and $hafas_train->number + == $iris_train->[0]->train_no ) + { + $hafas_train->{iris_seen} = 1; + next; + } + } + next; + } + for my $hafas_train (@all_hafas_trains) { + if ( $hafas_train->number + and $hafas_train->number + == $iris_train->[0]->train_no ) + { + $hafas_train->{iris_seen} = 1; + if ( $hafas_train->load + and $hafas_train->load->{SECOND} ) + { + $iris_train->[3] = $hafas_train->load; + } + for my $stop ( $hafas_train->route ) { + if ( $stop->loc->name + and $stop->loc->name eq + $iris_train->[1]->{name} + and $stop->arr ) + { + $iris_train->[2] = $stop->arr; + if ( $iris_train->[0]->departure_delay + and not $stop->arr_delay ) + { + $iris_train->[2] + ->add( minutes => $iris_train->[0] + ->departure_delay ); + } + last; + } + } + last; + } + } + } + for my $hafas_train (@all_hafas_trains) { + if ( $hafas_train->{iris_seen} ) { + next; + } + if ( $hafas_train->station_eva >= 8000000 ) { + + # better safe than sorry, for now + next; + } + for my $stop ( $hafas_train->route ) { + for my $dest (@destinations) { + if ( $stop->loc->name + and $stop->loc->name eq $dest->{name} + and $via_count{ $dest->{name} } < 2 + and $hafas_train->datetime ) + { + my $departure = $hafas_train->datetime; + my $arrival = $stop->arr; + my $delay = $hafas_train->delay; + if ( $delay + and $stop->arr == $stop->sched_arr ) + { + $arrival->add( minutes => $delay ); + } + if ( $departure->epoch >= $exclude_before ) { + $via_count{ $dest->{name} }++; + push( @hafas_trains, + [ $hafas_train, $dest, $arrival ] ); + } + } + } + } + } + }; + if ($@) { + $self->app->log->error( + "get_connecting_trains_p($uid): IRIS/HAFAS merge failed: $@" + ); + } - my $name = $self->stash('name'); - my $user = $self->users->get_privacy_by_name( name => $name ); + $promise->resolve( \@iris_trains, \@hafas_trains ); + return; + } + )->catch( + sub { + my ($err) = @_; + $promise->reject($err); + return; + } + )->wait; - delete $self->stash->{layout}; + return $promise; +} - if ( - $user - and ( $user->{public_level} & 0x02 - or - ( $user->{public_level} & 0x01 and $self->is_user_authenticated ) ) - ) - { - my $status = $self->get_user_status( $user->{id} ); +sub compute_effective_visibility { + my ( $self, $default_visibility, $journey_visibility ) = @_; + if ( $journey_visibility eq 'default' ) { + return $default_visibility; + } + return $journey_visibility; +} + +# Controllers + +sub homepage { + my ($self) = @_; + if ( $self->is_user_authenticated ) { + my $uid = $self->current_user->{id}; + my $status = $self->get_user_status; + my @timeline = $self->in_transit->get_timeline( + uid => $uid, + short => 1 + ); + $self->stash( timeline => [@timeline] ); + my @recent_targets; + if ( $status->{checked_in} ) { + my $journey_visibility + = $self->compute_effective_visibility( + $self->current_user->{default_visibility_str}, + $status->{visibility_str} ); + if ( defined $status->{arrival_countdown} + and $status->{arrival_countdown} < ( 40 * 60 ) ) + { + $self->render_later; + $self->get_connecting_trains_p->then( + sub { + my ( $connections_iris, $connections_hafas ) = @_; + $self->render( + 'landingpage', + user_status => $status, + journey_visibility => $journey_visibility, + connections_iris => $connections_iris, + connections_hafas => $connections_hafas, + ); + $self->users->mark_seen( uid => $uid ); + } + )->catch( + sub { + $self->render( + 'landingpage', + user_status => $status, + journey_visibility => $journey_visibility, + ); + $self->users->mark_seen( uid => $uid ); + } + )->wait; + return; + } + else { + $self->render( + 'landingpage', + user_status => $status, + journey_visibility => $journey_visibility, + ); + $self->users->mark_seen( uid => $uid ); + return; + } + } + else { + @recent_targets = uniq_by { $_->{eva} } + $self->journeys->get_latest_checkout_stations( uid => $uid ); + } $self->render( - '_public_status_card', - name => $name, - public_level => $user->{public_level}, - journey => $status + 'landingpage', + user_status => $status, + recent_targets => \@recent_targets, + with_autocomplete => 1, + with_geolocation => 1 ); + $self->users->mark_seen( uid => $uid ); } else { - $self->render('not_found'); + $self->render( 'landingpage', intro => 1 ); } } @@ -342,14 +469,93 @@ sub status_card { delete $self->stash->{layout}; + my @timeline = $self->in_transit->get_timeline( + uid => $self->current_user->{id}, + short => 1 + ); + $self->stash( timeline => [@timeline] ); + if ( $status->{checked_in} ) { - $self->render( '_checked_in', journey => $status ); + my $journey_visibility + = $self->compute_effective_visibility( + $self->current_user->{default_visibility_str}, + $status->{visibility_str} ); + if ( defined $status->{arrival_countdown} + and $status->{arrival_countdown} < ( 40 * 60 ) ) + { + $self->render_later; + $self->get_connecting_trains_p->then( + sub { + my ( $connections_iris, $connections_hafas ) = @_; + $self->render( + '_checked_in', + journey => $status, + journey_visibility => $journey_visibility, + connections_iris => $connections_iris, + connections_hafas => $connections_hafas, + ); + } + )->catch( + sub { + $self->render( + '_checked_in', + journey => $status, + journey_visibility => $journey_visibility, + ); + } + )->wait; + return; + } + $self->render( + '_checked_in', + journey => $status, + journey_visibility => $journey_visibility, + ); } elsif ( $status->{cancellation} ) { - $self->render( '_cancelled_departure', - journey => $status->{cancellation} ); + $self->render_later; + $self->get_connecting_trains_p( + eva => $status->{cancellation}{dep_eva}, + destination_name => $status->{cancellation}{arr_name} + )->then( + sub { + my ($connecting_trains) = @_; + $self->render( + '_cancelled_departure', + journey => $status->{cancellation}, + connections_iris => $connecting_trains + ); + } + )->catch( + sub { + $self->render( '_cancelled_departure', + journey => $status->{cancellation} ); + } + )->wait; + return; } else { + my @connecting_trains; + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + if ( $now->epoch - $status->{timestamp}->epoch < ( 30 * 60 ) ) { + $self->render_later; + $self->get_connecting_trains_p->then( + sub { + my ( $connections_iris, $connections_hafas ) = @_; + $self->render( + '_checked_out', + journey => $status, + connections_iris => $connections_iris, + connections_hafas => $connections_hafas, + ); + } + )->catch( + sub { + $self->render( '_checked_out', journey => $status ); + } + )->wait; + return; + } $self->render( '_checked_out', journey => $status ); } } @@ -362,38 +568,71 @@ sub geolocation { if ( not $lon or not $lat ) { $self->render( json => { error => 'Invalid lon/lat received' } ); + return; } - else { - my @candidates = map { - { - ds100 => $_->[0][0], - name => $_->[0][1], - eva => $_->[0][2], - lon => $_->[0][3], - lat => $_->[0][4], - distance => $_->[1], + $self->render_later; + + my @iris = map { + { + ds100 => $_->[0][0], + name => $_->[0][1], + eva => $_->[0][2], + lon => $_->[0][3], + lat => $_->[0][4], + distance => $_->[1], + hafas => 0, + } + } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon, + $lat, 10 ); + @iris = uniq_by { $_->{name} } @iris; + if ( @iris > 5 ) { + @iris = @iris[ 0 .. 4 ]; + } + + Travel::Status::DE::HAFAS->new_p( + promise => 'Mojo::Promise', + user_agent => $self->ua, + geoSearch => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($hafas) = @_; + my @hafas = map { + { + name => $_->name, + eva => $_->eva, + distance => $_->distance_m / 1000, + hafas => 1 + } + } $hafas->results; + if ( @hafas > 10 ) { + @hafas = @hafas[ 0 .. 9 ]; } - } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon, - $lat, 10 ); - @candidates = uniq_by { $_->{name} } @candidates; - if ( @candidates > 5 ) { + my @results = map { $_->[0] } + sort { $a->[1] <=> $b->[1] } + map { [ $_, $_->{distance} ] } ( @iris, @hafas ); $self->render( json => { - candidates => [ @candidates[ 0 .. 4 ] ], + candidates => [@results], } ); } - else { + )->catch( + sub { + my ($err) = @_; $self->render( json => { - candidates => [@candidates], + candidates => [@iris], + warning => $err, } ); } - } + )->wait; } -sub log_action { +sub travel_action { my ($self) = @_; my $params = $self->req->json; @@ -428,67 +667,124 @@ sub log_action { if ( $params->{action} eq 'checkin' ) { - my ( $train, $error ) = $self->checkin( - station => $params->{station}, - train_id => $params->{train} - ); - my $destination = $params->{dest}; + my $status = $self->get_user_status; + my $promise; - if ($error) { - $self->render( - json => { - success => 0, - error => $error, - }, - ); - } - elsif ( not $destination ) { - $self->render( - json => { - success => 1, - redirect_to => '/', - }, - ); + if ( $status->{checked_in} + and $status->{arr_eva} + and $status->{arrival_countdown} <= 0 ) + { + $promise = $self->checkout_p( station => $status->{arr_eva} ); } else { - # Silently ignore errors -- if they are permanent, the user will see - # them when selecting the destination manually. - my ( $still_checked_in, undef ) = $self->checkout( - station => $destination, - force => 0 - ); - my $station_link = '/s/' . $destination; - $self->render( - json => { - success => 1, - redirect_to => $still_checked_in ? '/' : $station_link, - }, - ); + $promise = Mojo::Promise->resolve; } + + $self->render_later; + $promise->then( + sub { + return $self->checkin_p( + station => $params->{station}, + train_id => $params->{train} + ); + } + )->then( + sub { + my $destination = $params->{dest}; + if ( not $destination ) { + $self->render( + json => { + success => 1, + redirect_to => '/', + }, + ); + return; + } + + # Silently ignore errors -- if they are permanent, the user will see + # them when selecting the destination manually. + return $self->checkout_p( + station => $destination, + force => 0 + ); + } + )->then( + sub { + my ( $still_checked_in, undef ) = @_; + if ( my $destination = $params->{dest} ) { + my $station_link = '/s/' . $destination; + if ( $status->{train_id} =~ m{[|]} ) { + $station_link .= '?hafas=1'; + } + $self->render( + json => { + success => 1, + redirect_to => $still_checked_in + ? '/' + : $station_link, + }, + ); + } + return; + } + )->catch( + sub { + my ($error) = @_; + $self->render( + json => { + success => 0, + error => $error, + }, + ); + } + )->wait; } elsif ( $params->{action} eq 'checkout' ) { - my ( $still_checked_in, $error ) = $self->checkout( + $self->render_later; + my $status = $self->get_user_status; + $self->checkout_p( station => $params->{station}, force => $params->{force} - ); - my $station_link = '/s/' . $params->{station}; + )->then( + sub { + my ( $still_checked_in, $error ) = @_; + my $station_link = '/s/' . $params->{station}; + if ( $status->{train_id} =~ m{[|]} ) { + $station_link .= '?hafas=1'; + } - if ($error) { - $self->render( - json => { - success => 0, - error => $error, - }, - ); - } - else { - $self->render( - json => { - success => 1, - redirect_to => $still_checked_in ? '/' : $station_link, - }, - ); - } + if ($error) { + $self->render( + json => { + success => 0, + error => $error, + }, + ); + } + else { + $self->render( + json => { + success => 1, + redirect_to => $still_checked_in + ? '/' + : $station_link, + }, + ); + } + return; + } + )->catch( + sub { + my ($error) = @_; + $self->render( + json => { + success => 0, + error => $error, + }, + ); + return; + } + )->wait; } elsif ( $params->{action} eq 'undo' ) { my $status = $self->get_user_status; @@ -504,7 +800,12 @@ sub log_action { else { my $redir = '/'; if ( $status->{checked_in} or $status->{cancelled} ) { - $redir = '/s/' . $status->{dep_ds100}; + if ( $status->{train_id} =~ m{[|]} ) { + $redir = '/s/' . $status->{dep_eva} . '?hafas=1'; + } + else { + $redir = '/s/' . $status->{dep_ds100}; + } } $self->render( json => { @@ -515,50 +816,69 @@ sub log_action { } } elsif ( $params->{action} eq 'cancelled_from' ) { - my ( undef, $error ) = $self->checkin( + $self->render_later; + $self->checkin_p( station => $params->{station}, train_id => $params->{train} - ); - - if ($error) { - $self->render( - json => { - success => 0, - error => $error, - }, - ); - } - else { - $self->render( - json => { - success => 1, - redirect_to => '/', - }, - ); - } + )->then( + sub { + $self->render( + json => { + success => 1, + redirect_to => '/', + }, + ); + } + )->catch( + sub { + my ($error) = @_; + $self->render( + json => { + success => 0, + error => $error, + }, + ); + } + )->wait; } elsif ( $params->{action} eq 'cancelled_to' ) { - my ( undef, $error ) = $self->checkout( + $self->render_later; + $self->checkout_p( station => $params->{station}, force => 1 - ); - - if ($error) { - $self->render( - json => { - success => 0, - error => $error, - }, - ); - } - else { - $self->render( - json => { - success => 1, - redirect_to => '/', - }, - ); - } + )->then( + sub { + my ( undef, $error ) = @_; + if ($error) { + $self->render( + json => { + success => 0, + error => $error, + }, + ); + } + else { + $self->render( + json => { + success => 1, + redirect_to => '/', + }, + ); + } + return; + } + )->catch( + sub { + my ($error) = @_; + $self->render( + json => { + success => 0, + error => $error, + }, + ); + return; + } + )->wait; } elsif ( $params->{action} eq 'delete' ) { my $error = $self->journeys->delete( @@ -595,57 +915,271 @@ sub log_action { } sub station { - my ($self) = @_; - my $station = $self->stash('station'); - my $train = $self->param('train'); - - my $status = $self->iris->get_departures( - station => $station, - lookbehind => 120, - lookahead => 30, - with_related => 1 + my ($self) = @_; + my $station = $self->stash('station'); + my $train = $self->param('train'); + my $trip_id = $self->param('trip_id'); + my $timestamp = $self->param('timestamp'); + my $uid = $self->current_user->{id}; + + my @timeline = $self->in_transit->get_timeline( + uid => $uid, + short => 1 ); + my %checkin_by_train; + for my $checkin (@timeline) { + say $checkin->{train_id}; + push( @{ $checkin_by_train{ $checkin->{train_id} } }, $checkin ); + } + $self->stash( checkin_by_train => \%checkin_by_train ); - if ( $status->{errstr} ) { - $self->render( - 'landingpage', - version => $self->app->config->{version} // 'UNKNOWN', - with_autocomplete => 1, - with_geolocation => 1, - error => $status->{errstr} + $self->render_later; + + if ( $timestamp and $timestamp =~ m{ ^ \d+ $ }x ) { + $timestamp = DateTime->from_epoch( + epoch => $timestamp, + time_zone => 'Europe/Berlin' ); } else { - # You can't check into a train which terminates here - my @results = grep { $_->departure } @{ $status->{results} }; - - @results = map { $_->[0] } - sort { $b->[1] <=> $a->[1] } - map { [ $_, $_->departure->epoch // $_->sched_departure->epoch ] } - @results; - - if ($train) { - @results - = grep { $_->type . ' ' . $_->train_no eq $train } @results; - } + $timestamp = DateTime->now( time_zone => 'Europe/Berlin' ); + } - $self->render( - 'departures', - eva => $status->{station_eva}, - results => \@results, - station => $status->{station_name}, - related_stations => $status->{related_stations}, - title => "travelynx: $status->{station_name}", + my $use_hafas = $self->param('hafas'); + my $promise; + if ($use_hafas) { + $promise = $self->hafas->get_departures_p( + eva => $station, + timestamp => $timestamp, + lookbehind => 30, + lookahead => 30, + ); + } + else { + $promise = $self->iris->get_departures_p( + station => $station, + lookbehind => 120, + lookahead => 30, + with_related => 1, ); } - $self->users->mark_seen( uid => $self->current_user->{id} ); + $promise->then( + sub { + my ($status) = @_; + my $api_link; + my @results; + + my $now = $self->now->epoch; + my $now_within_range + = abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0; + + if ($use_hafas) { + + my $iris_eva = List::Util::min grep { $_ >= 1000000 } + @{ $status->station->{evas} // [] }; + if ($iris_eva) { + $api_link = '/s/' . $iris_eva; + } + + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->datetime->epoch ] } $status->results; + $self->stations->add_meta( + eva => $status->station->{eva}, + meta => $status->station->{evas} // [] + ); + $status = { + station_eva => $status->station->{eva}, + station_name => ( + List::Util::reduce { length($a) < length($b) ? $a : $b } + @{ $status->station->{names} } + ), + related_stations => [], + }; + } + else { + + $api_link = '/s/' . $status->{station_eva} . '?hafas=1'; + + # You can't check into a train which terminates here + @results = grep { $_->departure } @{ $status->{results} }; + + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { + [ $_, $_->departure->epoch // $_->sched_departure->epoch ] + } @results; + } + + my $user_status = $self->get_user_status; + + my $can_check_out = 0; + if ( $user_status->{checked_in} ) { + for my $stop ( @{ $user_status->{route_after} } ) { + if ( + $stop->[1] eq $status->{station_eva} + or List::Util::any { $stop->[1] eq $_->{uic} } + @{ $status->{related_stations} } + ) + { + $can_check_out = 1; + last; + } + } + } + + my $connections_p; + if ( $trip_id and $use_hafas ) { + @results = grep { $_->id eq $trip_id } @results; + } + elsif ( $train and not $use_hafas ) { + @results + = grep { $_->type . ' ' . $_->train_no eq $train } @results; + } + else { + if ( $user_status->{cancellation} + and $status->{station_eva} eq + $user_status->{cancellation}{dep_eva} ) + { + $connections_p = $self->get_connecting_trains_p( + eva => $user_status->{cancellation}{dep_eva}, + destination_name => + $user_status->{cancellation}{arr_name} + ); + } + else { + $connections_p = $self->get_connecting_trains_p( + eva => $status->{station_eva} ); + } + } + + if ($connections_p) { + $connections_p->then( + sub { + my ( $connections_iris, $connections_hafas ) = @_; + $self->render( + 'departures', + eva => $status->{station_eva}, + datetime => $timestamp, + now_in_range => $now_within_range, + results => \@results, + hafas => $use_hafas, + station => $status->{station_name}, + related_stations => $status->{related_stations}, + user_status => $user_status, + can_check_out => $can_check_out, + connections_iris => $connections_iris, + connections_hafas => $connections_hafas, + api_link => $api_link, + title => "travelynx: $status->{station_name}", + ); + } + )->catch( + sub { + $self->render( + 'departures', + eva => $status->{station_eva}, + datetime => $timestamp, + now_in_range => $now_within_range, + results => \@results, + hafas => $use_hafas, + station => $status->{station_name}, + related_stations => $status->{related_stations}, + user_status => $user_status, + can_check_out => $can_check_out, + api_link => $api_link, + title => "travelynx: $status->{station_name}", + ); + } + )->wait; + } + else { + $self->render( + 'departures', + eva => $status->{station_eva}, + datetime => $timestamp, + now_in_range => $now_within_range, + results => \@results, + hafas => $use_hafas, + station => $status->{station_name}, + related_stations => $status->{related_stations}, + user_status => $user_status, + can_check_out => $can_check_out, + api_link => $api_link, + title => "travelynx: $status->{station_name}", + ); + } + } + )->catch( + sub { + my ( $err, $status ) = @_; + if ( $status and $status->{suggestions} ) { + $self->render( + 'disambiguation', + suggestions => $status->{suggestions}, + status => 300, + ); + } + elsif ( $use_hafas and $status and $status->errcode eq 'LOCATION' ) + { + $self->hafas->search_location_p( query => $station )->then( + sub { + my ($hafas2) = @_; + my @suggestions = $hafas2->results; + if ( @suggestions == 1 ) { + $self->redirect_to( + '/s/' . $suggestions[0]->eva . '?hafas=1' ); + } + else { + $self->render( + 'disambiguation', + suggestions => [ + map { { name => $_->name, eva => $_->eva } } + @suggestions + ], + status => 300, + ); + } + } + )->catch( + sub { + my ($err2) = @_; + $self->render( + 'exception', + exception => +"locationSearch threw '$err2' when handling '$err'", + status => 502 + ); + } + )->wait; + } + else { + $self->render( + 'exception', + exception => $err, + status => 502 + ); + } + } + )->wait; + $self->users->mark_seen( uid => $uid ); } sub redirect_to_station { my ($self) = @_; my $station = $self->param('station'); - $self->redirect_to("/s/${station}"); + if ( my $s = $self->app->stations->search($station) ) { + if ( $s->{source} == 1 ) { + $self->redirect_to("/s/${station}?hafas=1"); + } + else { + $self->redirect_to("/s/${station}"); + } + } + else { + $self->redirect_to("/s/${station}?hafas=1"); + } } sub cancelled { @@ -668,7 +1202,10 @@ sub cancelled { sub history { my ($self) = @_; - $self->render( template => 'history' ); + $self->render( + template => 'history', + title => 'travelynx: History' + ); } sub commute { @@ -719,10 +1256,10 @@ sub commute { $candidate_count{ $journey->{from_name} }++; } else { - # Avoid selecting an intermediate station for multi-leg commutes. - # Assumption: The intermediate station is also used for private - # travels -> penalize stations which are used on weekends or at - # unexpected times. + # Avoid selecting an intermediate station for multi-leg commutes. + # Assumption: The intermediate station is also used for private + # travels -> penalize stations which are used on weekends or at + # unexpected times. $candidate_count{ $journey->{from_name} }--; $candidate_count{ $journey->{to_name} }--; } @@ -776,6 +1313,7 @@ sub commute { journeys_by_month => \%journeys_by_month, count_by_month => \%count_by_month, total_journeys => $total, + title => 'travelynx: Reisen nach Station', months => [ qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember) ], @@ -792,13 +1330,64 @@ sub map_history { } my $route_type = $self->param('route_type'); + my $filter_from = $self->param('filter_from'); + my $filter_until = $self->param('filter_to'); + my $filter_type = $self->param('filter_type'); my $with_polyline = $route_type eq 'beeline' ? 0 : 1; + my $parser = DateTime::Format::Strptime->new( + pattern => '%d.%m.%Y', + locale => 'de_DE', + time_zone => 'Europe/Berlin' + ); + + if ( $filter_from + and $filter_from =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x ) + { + $filter_from = $parser->parse_datetime($filter_from); + } + else { + $filter_from = undef; + } + + if ( $filter_until + and $filter_until =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x ) + { + $filter_until = $parser->parse_datetime($filter_until)->set( + hour => 23, + minute => 59, + second => 58 + ); + } + else { + $filter_until = undef; + } + + my $year; + if ( $filter_from + and $filter_from->day == 1 + and $filter_from->month == 1 + and $filter_until + and $filter_until->day == 31 + and $filter_until->month == 12 + and $filter_from->year == $filter_until->year ) + { + $year = $filter_from->year; + } + my @journeys = $self->journeys->get( uid => $self->current_user->{id}, - with_polyline => $with_polyline + with_polyline => $with_polyline, + after => $filter_from, + before => $filter_until, ); + if ($filter_type) { + my @filter = split( qr{, *}, $filter_type ); + @journeys + = grep { has_str_in_list( $_->{type}, @filter ) } @journeys; + } + if ( not @journeys ) { $self->render( template => 'history_map', @@ -820,7 +1409,9 @@ sub map_history { $self->render( template => 'history_map', + year => $year, with_map => 1, + title => 'travelynx: Karte', %{$res} ); } @@ -886,42 +1477,126 @@ sub csv_history { ); } -sub yearly_history { +sub year_in_review { my ($self) = @_; my $year = $self->stash('year'); my @journeys; - my $stats; # DateTime is very slow when looking far into the future due to DST changes # -> Limit time range to avoid accidental DoS. if ( not( $year =~ m{ ^ [0-9]{4} $ }x and $year > 1990 and $year < 2100 ) ) { - @journeys = $self->journeys->get( - uid => $self->current_user->{id}, - with_datetime => 1 - ); + $self->render( 'not_found', status => 404 ); + return; } - else { - my $interval_start = DateTime->new( - time_zone => 'Europe/Berlin', - year => $year, - month => 1, - day => 1, - hour => 0, - minute => 0, - second => 0, + + my $interval_start = DateTime->new( + time_zone => 'Europe/Berlin', + year => $year, + month => 1, + day => 1, + hour => 0, + minute => 0, + second => 0, + ); + my $interval_end = $interval_start->clone->add( years => 1 ); + @journeys = $self->journeys->get( + uid => $self->current_user->{id}, + after => $interval_start, + before => $interval_end, + with_datetime => 1 + ); + + if ( not @journeys ) { + $self->render( + 'not_found', + message => 'Keine Zugfahrten im angefragten Jahr gefunden.', + status => 404 ); - my $interval_end = $interval_start->clone->add( years => 1 ); - @journeys = $self->journeys->get( - uid => $self->current_user->{id}, - after => $interval_start, - before => $interval_end, - with_datetime => 1 + return; + } + + my $now = $self->now; + if ( + not( $year < $now->year or ( $now->month == 12 and $now->day == 31 ) ) ) + { + $self->render( + 'not_found', + message => +'Der aktuelle Jahresrückblick wird erst zum Jahresende (am 31.12.) freigeschaltet', + status => 404 ); - $stats = $self->journeys->get_stats( - uid => $self->current_user->{id}, - year => $year + return; + } + + my ( $stats, $review ) = $self->journeys->get_stats( + uid => $self->current_user->{id}, + year => $year, + review => 1 + ); + + $self->render( + 'year_in_review', + title => "travelynx: Jahresrückblick $year", + year => $year, + stats => $stats, + review => $review, + ); + +} + +sub yearly_history { + my ($self) = @_; + my $year = $self->stash('year'); + my $filter = $self->param('filter'); + my @journeys; + + # DateTime is very slow when looking far into the future due to DST changes + # -> Limit time range to avoid accidental DoS. + if ( not( $year =~ m{ ^ [0-9]{4} $ }x and $year > 1990 and $year < 2100 ) ) + { + $self->render( 'not_found', status => 404 ); + return; + } + my $interval_start = DateTime->new( + time_zone => 'Europe/Berlin', + year => $year, + month => 1, + day => 1, + hour => 0, + minute => 0, + second => 0, + ); + my $interval_end = $interval_start->clone->add( years => 1 ); + @journeys = $self->journeys->get( + uid => $self->current_user->{id}, + after => $interval_start, + before => $interval_end, + with_datetime => 1 + ); + + if ( $filter and $filter eq 'single' ) { + @journeys = $self->journeys->grep_single(@journeys); + } + + if ( not @journeys ) { + $self->render( + 'not_found', + status => 404, + message => 'Keine Zugfahrten im angefragten Jahr gefunden.' ); + return; + } + + my $stats = $self->journeys->get_stats( + uid => $self->current_user->{id}, + year => $year + ); + + my $with_review; + my $now = $self->now; + if ( $year < $now->year or ( $now->month == 12 and $now->day == 31 ) ) { + $with_review = 1; } $self->respond_to( @@ -932,10 +1607,12 @@ sub yearly_history { } }, any => { - template => 'history_by_year', - journeys => [@journeys], - year => $year, - statistics => $stats + template => 'history_by_year', + title => "travelynx: $year", + journeys => [@journeys], + year => $year, + have_review => $with_review, + statistics => $stats } ); @@ -946,7 +1623,6 @@ sub monthly_history { my $year = $self->stash('year'); my $month = $self->stash('month'); my @journeys; - my $stats; my @months = ( qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember) @@ -961,35 +1637,43 @@ sub monthly_history { and $month < 13 ) ) { - @journeys = $self->journeys->get( - uid => $self->current_user->{id}, - with_datetime => 1 - ); + $self->render( 'not_found', status => 404 ); + return; } - else { - my $interval_start = DateTime->new( - time_zone => 'Europe/Berlin', - year => $year, - month => $month, - day => 1, - hour => 0, - minute => 0, - second => 0, - ); - my $interval_end = $interval_start->clone->add( months => 1 ); - @journeys = $self->journeys->get( - uid => $self->current_user->{id}, - after => $interval_start, - before => $interval_end, - with_datetime => 1 - ); - $stats = $self->journeys->get_stats( - uid => $self->current_user->{id}, - year => $year, - month => $month + my $interval_start = DateTime->new( + time_zone => 'Europe/Berlin', + year => $year, + month => $month, + day => 1, + hour => 0, + minute => 0, + second => 0, + ); + my $interval_end = $interval_start->clone->add( months => 1 ); + @journeys = $self->journeys->get( + uid => $self->current_user->{id}, + after => $interval_start, + before => $interval_end, + with_datetime => 1 + ); + + if ( not @journeys ) { + $self->render( + 'not_found', + message => 'Keine Zugfahrten im angefragten Monat gefunden.', + status => 404 ); + return; } + my $stats = $self->journeys->get_stats( + uid => $self->current_user->{id}, + year => $year, + month => $month + ); + + my $month_name = $months[ $month - 1 ]; + $self->respond_to( json => { json => { @@ -999,10 +1683,11 @@ sub monthly_history { }, any => { template => 'history_by_month', + title => "travelynx: $month_name $year", journeys => [@journeys], year => $year, month => $month, - month_name => $months[ $month - 1 ], + month_name => $month_name, statistics => $stats } ); @@ -1013,7 +1698,8 @@ sub journey_details { my ($self) = @_; my $journey_id = $self->stash('id'); - my $uid = $self->current_user->{id}; + my $user = $self->current_user; + my $uid = $user->{id}; $self->param( journey_id => $journey_id ); @@ -1028,11 +1714,12 @@ sub journey_details { } my $journey = $self->journeys->get_single( - uid => $uid, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, - with_polyline => 1, + uid => $uid, + journey_id => $journey_id, + verbose => 1, + with_datetime => 1, + with_polyline => 1, + with_visibility => 1, ); if ($journey) { @@ -1040,11 +1727,53 @@ sub journey_details { journeys => [$journey], include_manual => 1, ); + my $with_share; + my $share_text; + + my $visibility + = $self->compute_effective_visibility( + $user->{default_visibility_str}, + $journey->{visibility_str} ); + + if ( $visibility eq 'public' + or $visibility eq 'travelynx' + or $visibility eq 'followers' + or $visibility eq 'unlisted' ) + { + my $delay = 'pünktlich '; + if ( $journey->{rt_arrival} != $journey->{sched_arrival} ) { + $delay = sprintf( + 'mit %+d ', + ( + $journey->{rt_arrival}->epoch + - $journey->{sched_arrival}->epoch + ) / 60 + ); + } + $with_share = 1; + $share_text + = $journey->{km_route} + ? sprintf( '%.0f km', $journey->{km_route} ) + : 'Fahrt'; + $share_text .= sprintf( ' mit %s %s – Ankunft %sum %s', + $journey->{type}, $journey->{no}, + $delay, $journey->{rt_arrival}->strftime('%H:%M') ); + } + $self->render( 'journey', - error => undef, - journey => $journey, - with_map => 1, + title => sprintf( + 'travelynx: Fahrt %s %s %s am %s', + $journey->{type}, $journey->{line} // '', + $journey->{no}, + $journey->{sched_departure}->strftime('%d.%m.%Y um %H:%M') + ), + error => undef, + journey => $journey, + journey_visibility => $visibility, + with_map => 1, + with_share => $with_share, + share_text => $share_text, %{$map_data}, ); } @@ -1059,6 +1788,94 @@ sub journey_details { } +sub visibility_form { + my ($self) = @_; + my $dep_ts = $self->param('dep_ts'); + my $journey_id = $self->param('id'); + my $action = $self->param('action') // 'none'; + my $user = $self->current_user; + my $user_level = $user->{default_visibility_str}; + my $uid = $user->{id}; + my $status = $self->get_user_status; + my $visibility = $status->{visibility_str}; + my $journey; + + if ($journey_id) { + $journey = $self->journeys->get_single( + uid => $uid, + journey_id => $journey_id, + with_datetime => 1, + with_visibility => 1, + ); + $visibility = $journey->{visibility_str}; + } + + if ( $action eq 'save' ) { + if ( $self->validation->csrf_protect->has_error('csrf_token') ) { + $self->render( + 'bad_request', + csrf => 1, + status => 400 + ); + } + elsif ( $dep_ts and $dep_ts != $status->{sched_departure}->epoch ) { + $self->render( + 'edit_visibility', + error => 'old', + user_level => $user_level, + journey => {} + ); + } + else { + if ($dep_ts) { + $self->in_transit->update_visibility( + uid => $uid, + visibility => $self->param('status_level'), + ); + $self->redirect_to('/'); + $self->run_hook( $uid, 'update' ); + } + elsif ($journey_id) { + $self->journeys->update_visibility( + uid => $uid, + id => $journey_id, + visibility => $self->param('status_level'), + ); + $self->redirect_to( '/journey/' . $journey_id ); + } + } + return; + } + + $self->param( status_level => $visibility ); + + if ($journey_id) { + $self->render( + 'edit_visibility', + error => undef, + user_level => $user_level, + journey => $journey + ); + } + elsif ( $status->{checked_in} ) { + $self->param( dep_ts => $status->{sched_departure}->epoch ); + $self->render( + 'edit_visibility', + error => undef, + user_level => $user_level, + journey => $status + ); + } + else { + $self->render( + 'edit_visibility', + error => 'notfound', + user_level => $user_level, + journey => {} + ); + } +} + sub comment_form { my ($self) = @_; my $dep_ts = $self->param('dep_ts'); @@ -1099,11 +1916,13 @@ sub comment_form { } else { $self->app->log->debug("set comment"); + my $uid = $self->current_user->{id}; $self->in_transit->update_user_data( - uid => $self->current_user->{id}, + uid => $uid, user_data => { comment => $self->param('comment') } ); $self->redirect_to('/'); + $self->run_hook( $uid, 'update' ); } } @@ -1293,7 +2112,7 @@ sub add_journey_form { $self->render( 'add_journey', with_autocomplete => 1, - error => + error => 'Zug muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.' ); return; |