summaryrefslogtreecommitdiff
path: root/lib/Travelynx.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx.pm')
-rwxr-xr-xlib/Travelynx.pm4128
1 files changed, 1621 insertions, 2507 deletions
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 3cfc675..4749d65 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -1,4 +1,9 @@
package Travelynx;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
use Mojo::Base 'Mojolicious';
use Mojo::Pg;
@@ -8,21 +13,29 @@ use Cache::File;
use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
use DateTime;
use DateTime::Format::Strptime;
-use Encode qw(decode encode);
-use Geo::Distance;
+use Encode qw(decode encode);
+use File::Slurp qw(read_file);
use JSON;
use List::Util;
-use List::MoreUtils qw(after_incl before_incl);
+use List::UtilsBy qw(uniq_by);
+use List::MoreUtils qw(first_index);
use Travel::Status::DE::DBWagenreihung;
-use Travel::Status::DE::IRIS;
-use Travel::Status::DE::IRIS::Stations;
+use Travelynx::Helper::DBDB;
+use Travelynx::Helper::HAFAS;
+use Travelynx::Helper::IRIS;
use Travelynx::Helper::Sendmail;
-use XML::LibXML;
+use Travelynx::Helper::Traewelling;
+use Travelynx::Model::InTransit;
+use Travelynx::Model::Journeys;
+use Travelynx::Model::JourneyStatsCache;
+use Travelynx::Model::Stations;
+use Travelynx::Model::Traewelling;
+use Travelynx::Model::Users;
sub check_password {
my ( $password, $hash ) = @_;
- if ( bcrypt( $password, $hash ) eq $hash ) {
+ if ( bcrypt( substr( $password, 0, 10000 ), $hash ) eq $hash ) {
return 1;
}
return 0;
@@ -37,22 +50,11 @@ sub epoch_to_dt {
return DateTime->from_epoch(
epoch => $epoch,
- time_zone => 'Europe/Berlin'
+ time_zone => 'Europe/Berlin',
+ locale => 'de-DE',
);
}
-sub get_station {
- my ($station_name) = @_;
-
- my @candidates
- = Travel::Status::DE::IRIS::Stations::get_station($station_name);
-
- if ( @candidates == 1 ) {
- return $candidates[0];
- }
- return undef;
-}
-
sub startup {
my ($self) = @_;
@@ -60,6 +62,7 @@ sub startup {
$self->defaults( layout => 'default' );
+ $self->types->type( csv => 'text/csv; charset=utf-8' );
$self->types->type( json => 'application/json; charset=utf-8' );
$self->plugin('Config');
@@ -68,6 +71,9 @@ sub startup {
$self->secrets( $self->config->{secrets} );
}
+ chomp $self->config->{version};
+ $self->defaults( version => $self->config->{version} // 'UNKNOWN' );
+
$self->plugin(
authentication => {
autoload_user => 1,
@@ -78,7 +84,8 @@ sub startup {
},
validate_user => sub {
my ( $self, $username, $password, $extradata ) = @_;
- my $user_info = $self->get_user_password($username);
+ my $user_info
+ = $self->users->get_login_data( name => $username );
if ( not $user_info ) {
return undef;
}
@@ -93,6 +100,23 @@ sub startup {
},
}
);
+
+ if ( my $oa = $self->config->{traewelling}{oauth} ) {
+ $self->plugin(
+ OAuth2 => {
+ providers => {
+ traewelling => {
+ key => $oa->{id},
+ secret => $oa->{secret},
+ authorize_url =>
+'https://traewelling.de/oauth/authorize?response_type=code',
+ token_url => 'https://traewelling.de/oauth/token',
+ }
+ }
+ }
+ );
+ }
+
$self->sessions->default_expiration( 60 * 60 * 24 * 180 );
# Starting with v8.11, Mojolicious sends SameSite=Lax Cookies by default.
@@ -100,7 +124,7 @@ sub startup {
# security and usability for websites that want to maintain user's logged-in
# session after the user arrives from an external link". In practice,
# Safari (both iOS and macOS) does not send a SameSite=lax cookie when
- # following a link from an external site. So, marudor.de providing a
+ # following a link from an external site. So, bahn.expert providing a
# checkin link to travelynx.de/s/whatever does not work because the user
# is not logged in due to Safari not sending the cookie.
#
@@ -116,10 +140,10 @@ sub startup {
before_dispatch => sub {
my ($self) = @_;
- # The "theme" cookie is set client-side if the theme we delivered was
- # changed by dark mode detection or by using the theme switcher. It's
- # not part of Mojolicious' session data (and can't be, due to
- # signing and HTTPOnly), so we need to add it here.
+ # The "theme" cookie is set client-side if the theme we delivered was
+ # changed by dark mode detection or by using the theme switcher. It's
+ # not part of Mojolicious' session data (and can't be, due to
+ # signing and HTTPOnly), so we need to add it here.
for my $cookie ( @{ $self->req->cookies } ) {
if ( $cookie->name eq 'theme' ) {
$self->session( theme => $cookie->value );
@@ -154,76 +178,132 @@ sub startup {
);
$self->attr(
- token_type => sub {
- return {
- status => 1,
- history => 2,
- travel => 3,
- import => 4,
- };
- }
- );
- $self->attr(
- token_types => sub {
- return [qw(status history travel import)];
+ coordinates_by_station => sub {
+ my $legacy_names = $self->app->renamed_station;
+ my $location = $self->stations->get_latlon_by_name;
+ while ( my ( $old_name, $new_name ) = each %{$legacy_names} ) {
+ $location->{$old_name} = $location->{$new_name};
+ }
+ return $location;
}
);
+ # https://de.wikipedia.org/wiki/Liste_nach_Gemeinden_und_Regionen_benannter_IC/ICE-Fahrzeuge#Namensgebung_ICE-Triebz%C3%BCge_nach_Gemeinden
+ # via https://github.com/marudor/bahn.expert/blob/main/src/server/coachSequence/TrainNames.ts
$self->attr(
- account_public_mask => sub {
- return {
- status_intern => 0x01,
- status_extern => 0x02,
- status_comment => 0x04,
- };
+ ice_name => sub {
+ my $id_to_name = JSON->new->utf8->decode(
+ scalar read_file('share/ice_names.json') );
+ return $id_to_name;
}
);
$self->attr(
- journey_edit_mask => sub {
- return {
- sched_departure => 0x0001,
- real_departure => 0x0002,
- route => 0x0010,
- is_cancelled => 0x0020,
- sched_arrival => 0x0100,
- real_arrival => 0x0200,
- };
+ renamed_station => sub {
+ my $legacy_to_new = JSON->new->utf8->decode(
+ scalar read_file('share/old_station_names.json') );
+ return $legacy_to_new;
}
);
- $self->attr(
- coordinates_by_station => sub {
- my %location;
- for
- my $station ( Travel::Status::DE::IRIS::Stations::get_stations() )
+ if ( not $self->app->config->{base_url} ) {
+ $self->app->log->error(
+"travelynx.conf: 'base_url' is missing. Links in maintenance/work/worker-generated E-Mails will be incorrect. This variable was introduced in travelynx 1.22; see examples/travelynx.conf for documentation."
+ );
+ }
+
+ $self->helper(
+ base_url_for => sub {
+ my ( $self, $path ) = @_;
+ if ( ( my $url = $self->url_for($path) )->base ne q{}
+ or not $self->app->config->{base_url} )
{
- if ( $station->[3] ) {
- $location{ $station->[1] }
- = [ $station->[4], $station->[3] ];
- }
+ return $url;
}
- return \%location;
+ return $self->url_for($path)
+ ->base( $self->app->config->{base_url} );
}
);
- $self->attr(
- station_by_eva => sub {
- my %map;
- for
- my $station ( Travel::Status::DE::IRIS::Stations::get_stations() )
- {
- $map{ $station->[2] } = $station;
- }
- return \%map;
+ $self->helper(
+ hafas => sub {
+ my ($self) = @_;
+ state $hafas = Travelynx::Helper::HAFAS->new(
+ log => $self->app->log,
+ main_cache => $self->app->cache_iris_main,
+ realtime_cache => $self->app->cache_iris_rt,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
+ );
}
);
$self->helper(
- sendmail => sub {
- state $sendmail = Travelynx::Helper::Sendmail->new(
- config => ( $self->config->{mail} // {} ),
- log => $self->log
+ iris => sub {
+ my ($self) = @_;
+ state $iris = Travelynx::Helper::IRIS->new(
+ log => $self->app->log,
+ main_cache => $self->app->cache_iris_main,
+ realtime_cache => $self->app->cache_iris_rt,
+ root_url => $self->base_url_for('/')->to_abs,
+ version => $self->app->config->{version},
+ );
+ }
+ );
+
+ $self->helper(
+ traewelling => sub {
+ my ($self) = @_;
+ state $trwl = Travelynx::Model::Traewelling->new( pg => $self->pg );
+ }
+ );
+
+ $self->helper(
+ traewelling_api => sub {
+ my ($self) = @_;
+ state $trwl_api = Travelynx::Helper::Traewelling->new(
+ log => $self->app->log,
+ model => $self->traewelling,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
+ );
+ }
+ );
+
+ $self->helper(
+ in_transit => sub {
+ my ($self) = @_;
+ state $in_transit = Travelynx::Model::InTransit->new(
+ log => $self->app->log,
+ pg => $self->pg,
+ );
+ }
+ );
+
+ $self->helper(
+ journey_stats_cache => sub {
+ my ($self) = @_;
+ state $journey_stats_cache
+ = Travelynx::Model::JourneyStatsCache->new(
+ log => $self->app->log,
+ pg => $self->pg,
+ );
+ }
+ );
+
+ $self->helper(
+ journeys => sub {
+ my ($self) = @_;
+ state $journeys = Travelynx::Model::Journeys->new(
+ log => $self->app->log,
+ pg => $self->pg,
+ in_transit => $self->in_transit,
+ stats_cache => $self->journey_stats_cache,
+ renamed_station => $self->app->renamed_station,
+ latlon_by_station => $self->app->coordinates_by_station,
+ stations => $self->stations,
);
}
);
@@ -242,6 +322,52 @@ sub startup {
state $pg
= Mojo::Pg->new("postgresql://${user}\@${host}:${port}/${dbname}")
->password($pw);
+
+ $pg->on(
+ connection => sub {
+ my ( $pg, $dbh ) = @_;
+ $dbh->do("set time zone 'Europe/Berlin'");
+ }
+ );
+
+ return $pg;
+ }
+ );
+
+ $self->helper(
+ sendmail => sub {
+ state $sendmail = Travelynx::Helper::Sendmail->new(
+ config => ( $self->config->{mail} // {} ),
+ log => $self->log
+ );
+ }
+ );
+
+ $self->helper(
+ stations => sub {
+ my ($self) = @_;
+ state $stations
+ = Travelynx::Model::Stations->new( pg => $self->pg );
+ }
+ );
+
+ $self->helper(
+ users => sub {
+ my ($self) = @_;
+ state $users = Travelynx::Model::Users->new( pg => $self->pg );
+ }
+ );
+
+ $self->helper(
+ dbdb => sub {
+ my ($self) = @_;
+ state $dbdb = Travelynx::Helper::DBDB->new(
+ log => $self->app->log,
+ cache => $self->app->cache_iris_main,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
+ );
}
);
@@ -268,248 +394,256 @@ sub startup {
);
$self->helper(
- 'get_departures' => sub {
- my ( $self, $station, $lookbehind, $lookahead, $with_related ) = @_;
-
- $lookbehind //= 180;
- $lookahead //= 30;
- $with_related //= 0;
-
- my @station_matches
- = Travel::Status::DE::IRIS::Stations::get_station($station);
-
- if ( @station_matches == 1 ) {
- $station = $station_matches[0][0];
- my $status = Travel::Status::DE::IRIS->new(
- station => $station,
- main_cache => $self->app->cache_iris_main,
- realtime_cache => $self->app->cache_iris_rt,
- keep_transfers => 1,
- lookbehind => 20,
- datetime => DateTime->now( time_zone => 'Europe/Berlin' )
- ->subtract( minutes => $lookbehind ),
- lookahead => $lookbehind + $lookahead,
- lwp_options => {
- timeout => 10,
- agent => 'travelynx/' . $self->app->config->{version},
- },
- with_related => $with_related,
- );
- return {
- results => [ $status->results ],
- errstr => $status->errstr,
- station_ds100 =>
- ( $status->station ? $status->station->{ds100} : undef ),
- station_eva =>
- ( $status->station ? $status->station->{uic} : undef ),
- station_name =>
- ( $status->station ? $status->station->{name} : undef ),
- related_stations => [ $status->related_stations ],
- };
- }
- elsif ( @station_matches > 1 ) {
- return {
- results => [],
- errstr => 'Mehrdeutiger Stationsname. Mögliche Eingaben: '
- . join( q{, }, map { $_->[1] } @station_matches ),
- };
+ 'sprintf_km' => sub {
+ my ( $self, $km ) = @_;
+
+ if ( $km < 1 ) {
+ return sprintf( '%.f m', $km * 1000 );
}
- else {
- return {
- results => [],
- errstr => 'Unbekannte Station',
- };
+ if ( $km < 10 ) {
+ return sprintf( '%.1f km', $km );
}
+ return sprintf( '%.f km', $km );
}
);
$self->helper(
- 'grep_unknown_stations' => sub {
- my ( $self, @stations ) = @_;
-
- my @unknown_stations;
- for my $station (@stations) {
- my $station_info = get_station($station);
- if ( not $station_info ) {
- push( @unknown_stations, $station );
- }
+ 'load_icon' => sub {
+ my ( $self, $load ) = @_;
+ my $first = $load->{FIRST} // 0;
+ my $second = $load->{SECOND} // 0;
+
+ my @symbols
+ = (
+ qw(help_outline person_outline people priority_high not_interested)
+ );
+
+ return ( $symbols[$first], $symbols[$second] );
+ }
+ );
+
+ $self->helper(
+ 'visibility_icon' => sub {
+ my ( $self, $visibility ) = @_;
+ if ( $visibility eq 'public' ) {
+ return 'language';
+ }
+ if ( $visibility eq 'travelynx' ) {
+ return 'lock_open';
+ }
+ if ( $visibility eq 'followers' ) {
+ return 'group';
+ }
+ if ( $visibility eq 'unlisted' ) {
+ return 'lock_outline';
}
- return @unknown_stations;
+ if ( $visibility eq 'private' ) {
+ return 'lock';
+ }
+ return 'help_outline';
}
);
- # Returns (journey id, error)
- # Must be called during a transaction.
- # Must perform a rollback on error.
$self->helper(
- 'add_journey' => sub {
+ 'checkin_p' => sub {
my ( $self, %opt ) = @_;
- my $db = $opt{db};
- my $uid = $opt{uid} // $self->current_user->{id};
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $dep_station = get_station( $opt{dep_station} );
- my $arr_station = get_station( $opt{arr_station} );
+ my $station = $opt{station};
+ my $train_id = $opt{train_id};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+ my $hafas;
- if ( not $dep_station ) {
- return ( undef, 'Unbekannter Startbahnhof' );
+ my $user = $self->get_user_status( $uid, $db );
+ if ( $user->{checked_in} or $user->{cancelled} ) {
+ return Mojo::Promise->reject('You are already checked in');
}
- if ( not $arr_station ) {
- return ( undef, 'Unbekannter Zielbahnhof' );
+
+ if ( $train_id =~ m{[|]} ) {
+ return $self->_checkin_hafas_p(%opt);
}
- my @route = ( [ $dep_station->[1], {}, undef ] );
+ my $promise = Mojo::Promise->new;
- if ( $opt{route} ) {
- my @unknown_stations;
- for my $station ( @{ $opt{route} } ) {
- my $station_info = get_station($station);
- if ($station_info) {
- push( @route, [ $station_info->[1], {}, undef ] );
- }
- else {
- push( @route, [ $station, {}, undef ] );
- push( @unknown_stations, $station );
- }
- }
+ $self->iris->get_departures_p(
+ station => $station,
+ lookbehind => 140,
+ lookahead => 40
+ )->then(
+ sub {
+ my ($status) = @_;
- if ( not $opt{lax} ) {
- if ( @unknown_stations == 1 ) {
- return ( undef,
- "Unbekannter Unterwegshalt: $unknown_stations[0]" );
- }
- elsif (@unknown_stations) {
- return ( undef,
- 'Unbekannte Unterwegshalte: '
- . join( ', ', @unknown_stations ) );
+ if ( $status->{errstr} ) {
+ $promise->reject( $status->{errstr} );
+ return;
}
- }
- }
-
- push( @route, [ $arr_station->[1], {}, undef ] );
-
- if ( $route[0][0] eq $route[1][0] ) {
- shift(@route);
- }
- if ( $route[-2][0] eq $route[-1][0] ) {
- pop(@route);
- }
+ my $eva = $status->{station_eva};
+ my $train = List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- my $entry = {
- user_id => $uid,
- train_type => $opt{train_type},
- train_line => $opt{train_line},
- train_no => $opt{train_no},
- train_id => 'manual',
- checkin_station_id => $dep_station->[2],
- checkin_time => $now,
- sched_departure => $opt{sched_departure},
- real_departure => $opt{rt_departure},
- checkout_station_id => $arr_station->[2],
- sched_arrival => $opt{sched_arrival},
- real_arrival => $opt{rt_arrival},
- checkout_time => $now,
- edited => 0x3fff,
- cancelled => $opt{cancelled} ? 1 : 0,
- route => JSON->new->encode( \@route ),
- };
+ if ( not defined $train ) {
+ $promise->reject("Train ${train_id} not found");
+ return;
+ }
- if ( $opt{comment} ) {
- $entry->{user_data}
- = JSON->new->encode( { comment => $opt{comment} } );
- }
+ eval {
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ departure_eva => $eva,
+ train => $train,
+ route => [ $self->iris->route_diff($train) ],
+ );
+ };
+ if ($@) {
+ $self->app->log->error(
+ "Checkin($uid): INSERT failed: $@");
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
+ }
- my $journey_id = undef;
- eval {
- $journey_id
- = $db->insert( 'journeys', $entry, { returning => 'id' } )
- ->hash->{id};
- $self->invalidate_stats_cache( $opt{rt_departure}, $db, $uid );
- };
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->add_route_timestamps( $uid, $train, 1 );
+ $self->run_hook( $uid, 'checkin' );
+ }
- if ($@) {
- $self->app->log->error("add_journey($uid): $@");
- return ( undef, 'add_journey failed: ' . $@ );
- }
+ $promise->resolve($train);
+ return;
+ }
+ )->catch(
+ sub {
+ my ( $err, $status ) = @_;
+ $promise->reject( $status->{errstr} );
+ return;
+ }
+ )->wait;
- return ( $journey_id, undef );
+ return $promise;
}
);
$self->helper(
- 'checkin' => sub {
- my ( $self, $station, $train_id, $uid ) = @_;
+ '_checkin_hafas_p' => sub {
+ my ( $self, %opt ) = @_;
- $uid //= $self->current_user->{id};
+ my $station = $opt{station};
+ my $train_id = $opt{train_id};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+ my $hafas;
- my $status = $self->get_departures( $station, 140, 40, 0 );
- if ( $status->{errstr} ) {
- return ( undef, $status->{errstr} );
- }
- else {
- my ($train) = List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
- if ( not defined $train ) {
- return ( undef, "Train ${train_id} not found" );
- }
- else {
-
- my $user = $self->get_user_status($uid);
- if ( $user->{checked_in} or $user->{cancelled} ) {
+ my $promise = Mojo::Promise->new;
- if ( $user->{train_id} eq $train_id
- and $user->{dep_eva} eq $status->{station_eva} )
+ $self->hafas->get_journey_p(
+ trip_id => $train_id,
+ with_polyline => 1
+ )->then(
+ sub {
+ my ($journey) = @_;
+ my $found;
+ for my $stop ( $journey->route ) {
+ if ( $stop->loc->name eq $station
+ or $stop->loc->eva == $station )
{
- # checking in twice is harmless
- return ( $train, undef );
+ $found = $stop;
+ last;
}
-
- # Otherwise, someone forgot to check out first
- $self->checkout( $station, 1, $uid );
}
-
+ if ( not $found ) {
+ $promise->reject(
+ "Did not find journey $train_id at $station");
+ return;
+ }
+ for my $stop ( $journey->route ) {
+ $self->stations->add_or_update(
+ stop => $stop,
+ db => $db,
+ );
+ }
eval {
- my $json = JSON->new;
- $self->pg->db->insert(
- 'in_transit',
- {
- user_id => $uid,
- cancelled => $train->departure_is_cancelled
- ? 1
- : 0,
- checkin_station_id => $status->{station_eva},
- checkin_time =>
- DateTime->now( time_zone => 'Europe/Berlin' ),
- dep_platform => $train->platform,
- train_type => $train->type,
- train_line => $train->line_no,
- train_no => $train->train_no,
- train_id => $train->train_id,
- sched_departure => $train->sched_departure,
- real_departure => $train->departure,
- route => $json->encode(
- [ $self->route_diff($train) ]
- ),
- messages => $json->encode(
- [
- map { [ $_->[0]->epoch, $_->[1] ] }
- $train->messages
- ]
- )
- }
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ journey => $journey,
+ stop => $found,
);
};
if ($@) {
$self->app->log->error(
"Checkin($uid): INSERT failed: $@");
- return ( undef, 'INSERT failed: ' . $@ );
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
+ }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => { trip_id => $journey->id }
+ );
+
+ my $polyline;
+ if ( $journey->polyline ) {
+ my @station_list;
+ my @coordinate_list;
+ for my $coord ( $journey->polyline ) {
+ if ( $coord->{name} ) {
+ push(
+ @coordinate_list,
+ [
+ $coord->{lon}, $coord->{lat},
+ $coord->{eva}
+ ]
+ );
+ push( @station_list, $coord->{name} );
+ }
+ else {
+ push( @coordinate_list,
+ [ $coord->{lon}, $coord->{lat} ] );
+ }
+ }
+
+ # equal length → polyline only consists of straight
+ # lines between stops. that's not helpful.
+ if ( @station_list == @coordinate_list ) {
+ $self->log->debug( 'Ignoring polyline for '
+ . $journey->line
+ . ' as it only consists of straight lines between stops.'
+ );
+ }
+ else {
+ $polyline = {
+ from_eva => ( $journey->route )[0]->loc->eva,
+ to_eva => ( $journey->route )[-1]->loc->eva,
+ coords => \@coordinate_list,
+ };
+ }
}
- $self->add_route_timestamps( $uid, $train, 1 );
- $self->run_hook( $uid, 'checkin' );
- return ( $train, undef );
+
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ );
+ }
+
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkin' );
+ }
+
+ $promise->resolve($journey);
}
- }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
}
);
@@ -519,9 +653,7 @@ sub startup {
$uid //= $self->current_user->{id};
if ( $journey_id eq 'in_transit' ) {
- eval {
- $self->pg->db->delete( 'in_transit', { user_id => $uid } );
- };
+ eval { $self->in_transit->delete( uid => $uid ); };
if ($@) {
$self->app->log->error("Undo($uid, $journey_id): $@");
return "Undo($journey_id): $@";
@@ -537,20 +669,10 @@ sub startup {
my $db = $self->pg->db;
my $tx = $db->begin;
- my $journey = $db->select(
- 'journeys',
- '*',
- {
- user_id => $uid,
- id => $journey_id
- }
- )->hash;
- $db->delete(
- 'journeys',
- {
- user_id => $uid,
- id => $journey_id
- }
+ my $journey = $self->journeys->pop(
+ uid => $uid,
+ db => $db,
+ journey_id => $journey_id
);
if ( $journey->{edited} ) {
@@ -562,7 +684,30 @@ sub startup {
delete $journey->{edited};
delete $journey->{id};
- $db->insert( 'in_transit', $journey );
+ # users may force checkouts at stations that are not part of
+ # the train's scheduled (or real-time) route. re-adding those
+ # to in-transit violates the assumption that each train has
+ # a valid destination. Remove the target in this case.
+ my $route = JSON->new->decode( $journey->{route} );
+ my $found_checkout_id;
+ for my $stop ( @{$route} ) {
+ if ( $stop->[1] == $journey->{checkout_station_id} ) {
+ $found_checkout_id = 1;
+ last;
+ }
+ }
+ if ( not $found_checkout_id ) {
+ $journey->{checkout_station_id} = undef;
+ $journey->{checkout_time} = undef;
+ $journey->{arr_platform} = undef;
+ $journey->{sched_arrival} = undef;
+ $journey->{real_arrival} = undef;
+ }
+
+ $self->in_transit->add_from_journey(
+ db => $db,
+ journey => $journey
+ );
my $cache_ts = DateTime->now( time_zone => 'Europe/Berlin' );
if ( $journey->{real_departure}
@@ -574,7 +719,11 @@ sub startup {
);
}
- $self->invalidate_stats_cache( $cache_ts, $db, $uid );
+ $self->journey_stats_cache->invalidate(
+ ts => $cache_ts,
+ db => $db,
+ uid => $uid
+ );
$tx->commit;
};
@@ -587,628 +736,395 @@ sub startup {
}
);
- # Statistics are partitioned by real_departure, which must be provided
- # when calling this function e.g. after journey deletion or editing.
- # If a joureny's real_departure has been edited, this function must be
- # called twice: once with the old and once with the new value.
$self->helper(
- 'invalidate_stats_cache' => sub {
- my ( $self, $ts, $db, $uid ) = @_;
-
- $uid //= $self->current_user->{id};
- $db //= $self->pg->db;
+ 'checkout_p' => sub {
+ my ( $self, %opt ) = @_;
- $self->pg->db->delete(
- 'journey_stats',
- {
- user_id => $uid,
- year => $ts->year,
- month => $ts->month,
- }
- );
- $self->pg->db->delete(
- 'journey_stats',
- {
- user_id => $uid,
- year => $ts->year,
- month => 0,
- }
- );
- }
- );
+ my $station = $opt{station};
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $with_related = $opt{with_related} // 0;
+ my $force = $opt{force};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+ my $user = $self->get_user_status( $uid, $db );
+ my $train_id = $user->{train_id};
- $self->helper(
- 'checkout' => sub {
- my ( $self, $station, $force, $uid ) = @_;
+ my $promise = Mojo::Promise->new;
- my $db = $self->pg->db;
- my $status = $self->get_departures( $station, 120, 120, 0 );
- $uid //= $self->current_user->{id};
- my $user = $self->get_user_status($uid);
- my $train_id = $user->{train_id};
+ if ( not $station ) {
+ $self->app->log->error("Checkout($uid): station is empty");
+ return $promise->resolve( 1,
+ 'BUG: Checkout station is empty.' );
+ }
if ( not $user->{checked_in} and not $user->{cancelled} ) {
- return ( 0, 'You are not checked into any train' );
- }
- if ( $status->{errstr} and not $force ) {
- return ( 1, $status->{errstr} );
+ return $promise->resolve( 0,
+ 'You are not checked into any train' );
}
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $journey
- = $db->select( 'in_transit', '*', { user_id => $uid } )
- ->expand->hash;
-
- # Note that a train may pass the same station several times.
- # Notable example: S41 / S42 ("Ringbahn") both starts and
- # terminates at Berlin Südkreuz
- my ($train) = List::Util::first {
- $_->train_id eq $train_id
- and $_->sched_arrival
- and $_->sched_arrival->epoch > $user->{sched_departure}->epoch
+ if ( $dep_eva and $dep_eva != $user->{dep_eva} ) {
+ return $promise->resolve( 0, 'race condition' );
}
- @{ $status->{results} };
-
- $train //= List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
-
- my $new_checkout_station_id = $status->{station_eva};
-
- # When a checkout is triggered by a checkin, there is an edge case
- # with related stations.
- # Assume a user travels from A to B1, then from B2 to C. B1 and B2 are
- # relatd stations (e.g. "Frankfurt Hbf" and "Frankfurt Hbf(tief)").
- # Now, if they check in for the journey from B2 to C, and have not yet
- # checked out of the previous train, $train is undef as B2 is not B1.
- # Redo the request with with_related => 1 to avoid this case.
- # While at it, we increase the lookahead to handle long journeys as
- # well.
- if ( not $train ) {
- $status = $self->get_departures( $station, 120, 180, 1 );
- ($train) = List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
- if ( $train
- and $self->app->station_by_eva->{ $train->station_uic } )
- {
- $new_checkout_station_id = $train->station_uic;
- }
+ if ( $arr_eva and $arr_eva != $user->{arr_eva} ) {
+ return $promise->resolve( 0, 'race condition' );
}
- # Store the intended checkout station regardless of this operation's
- # success.
- $db->update(
- 'in_transit',
- {
- checkout_station_id => $new_checkout_station_id,
- },
- { user_id => $uid }
- );
-
- # If in_transit already contains arrival data for another estimated
- # destination, we must invalidate it.
- if ( defined $journey->{checkout_station_id}
- and $journey->{checkout_station_id}
- != $new_checkout_station_id )
- {
- $db->update(
- 'in_transit',
- {
- checkout_time => undef,
- arr_platform => undef,
- sched_arrival => undef,
- real_arrival => undef,
- },
- { user_id => $uid }
- );
+ if ( $train_id =~ m{[|]} ) {
+ return $self->_checkout_hafas_p(%opt);
}
- if ( not defined $train ) {
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $journey = $self->in_transit->get(
+ uid => $uid,
+ with_data => 1
+ );
- # Arrival time via IRIS is unknown, so the train probably has not
- # arrived yet. Fall back to HAFAS.
- # TODO support cases where $station is EVA or DS100 code
- if (
- my $station_data
- = List::Util::first { $_->[0] eq $station }
- @{ $journey->{route} }
- )
- {
- $station_data = $station_data->[1];
- if ( $station_data->{sched_arr} ) {
- my $sched_arr
- = epoch_to_dt( $station_data->{sched_arr} );
- my $rt_arr = $sched_arr->clone;
- if ( $station_data->{adelay}
- and $station_data->{adelay} =~ m{^\d+$} )
- {
- $rt_arr->add( minutes => $station_data->{adelay} );
- }
- $db->update(
- 'in_transit',
- {
- sched_arrival => $sched_arr,
- real_arrival => $rt_arr
- },
- { user_id => $uid }
+ $self->iris->get_departures_p(
+ station => $station,
+ lookbehind => 120,
+ lookahead => 180,
+ with_related => $with_related,
+ )->then(
+ sub {
+ my ($status) = @_;
+
+ my $new_checkout_station_id = $status->{station_eva};
+
+ # Store the intended checkout station regardless of this operation's
+ # success.
+ # TODO for with_related == 1, the correct EVA may be different
+ # and should be fetched from $train later on
+ $self->in_transit->set_arrival_eva(
+ uid => $uid,
+ db => $db,
+ arrival_eva => $new_checkout_station_id
+ );
+
+ # If in_transit already contains arrival data for another estimated
+ # destination, we must invalidate it.
+ if ( defined $journey->{checkout_station_id}
+ and $journey->{checkout_station_id}
+ != $new_checkout_station_id )
+ {
+ $self->in_transit->unset_arrival_data(
+ uid => $uid,
+ db => $db
);
}
- }
- if ( not $force ) {
- $self->run_hook( $uid, 'update' );
- return ( 1, undef );
- }
- }
-
- my $has_arrived = 0;
- eval {
-
- my $tx = $db->begin;
+ # Note that a train may pass the same station several times.
+ # Notable example: S41 / S42 ("Ringbahn") both starts and
+ # terminates at Berlin Südkreuz
+ my $train = List::Util::first {
+ $_->train_id eq $train_id
+ and $_->sched_arrival
+ and $_->sched_arrival->epoch
+ > $user->{sched_departure}->epoch
+ }
+ @{ $status->{results} };
- if ( defined $train ) {
+ $train //= List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- if ( not $train->arrival ) {
- die("Train has no arrival timestamp\n");
- }
+ if ( not defined $train ) {
- $has_arrived = $train->arrival->epoch < $now->epoch ? 1 : 0;
- my $json = JSON->new;
- $db->update(
- 'in_transit',
+ # Arrival time via IRIS is unknown, so the train probably
+ # has not arrived yet. Fall back to HAFAS.
+ # TODO support cases where $station is EVA or DS100 code
+ if (
+ my $station_data
+ = List::Util::first { $_->[0] eq $station }
+ @{ $journey->{route} }
+ )
{
- checkout_time => $now,
- arr_platform => $train->platform,
- sched_arrival => $train->sched_arrival,
- real_arrival => $train->arrival,
- cancelled => $train->arrival_is_cancelled ? 1 : 0,
- route =>
- $json->encode( [ $self->route_diff($train) ] ),
- messages => $json->encode(
- [
- map { [ $_->[0]->epoch, $_->[1] ] }
- $train->messages
- ]
- )
- },
- { user_id => $uid }
- );
- if ($has_arrived) {
- my @unknown_stations
- = $self->grep_unknown_stations( $train->route );
- if (@unknown_stations) {
- $self->app->log->warn(
- 'Encountered unknown stations: '
- . join( ', ', @unknown_stations ) );
+ $station_data = $station_data->[2];
+ if ( $station_data->{sched_arr} ) {
+ my $sched_arr
+ = epoch_to_dt( $station_data->{sched_arr} );
+ my $rt_arr
+ = epoch_to_dt( $station_data->{rt_arr} );
+ if ( $rt_arr->epoch == 0 ) {
+ $rt_arr = $sched_arr->clone;
+ if ( $station_data->{arr_delay}
+ and $station_data->{arr_delay}
+ =~ m{^\d+$} )
+ {
+ $rt_arr->add( minutes =>
+ $station_data->{arr_delay} );
+ }
+ }
+ $self->in_transit->set_arrival_times(
+ uid => $uid,
+ db => $db,
+ sched_arrival => $sched_arr,
+ rt_arrival => $rt_arr
+ );
+ }
}
- }
- }
+ if ( not $force ) {
- $journey
- = $db->select( 'in_transit', '*', { user_id => $uid } )->hash;
-
- if ( $has_arrived or $force ) {
- delete $journey->{data};
- $journey->{edited} = 0;
- $journey->{checkout_time} = $now;
- $db->insert( 'journeys', $journey );
- $db->delete( 'in_transit', { user_id => $uid } );
-
- my $cache_ts = $now->clone;
- if ( $journey->{real_departure}
- =~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x )
- {
- $cache_ts->set(
- year => $+{year},
- month => $+{month}
- );
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'update' );
+ }
+ $promise->resolve( 1, undef );
+ return;
+ }
}
- $self->invalidate_stats_cache( $cache_ts, $db, $uid );
- }
-
- $tx->commit;
- };
-
- if ($@) {
- $self->app->log->error("Checkout($uid): $@");
- return ( 1, 'Checkout error: ' . $@ );
- }
+ my $has_arrived = 0;
- if ( $has_arrived or $force ) {
- $self->run_hook( $uid, 'checkout' );
- return ( 0, undef );
- }
- $self->run_hook( $uid, 'update' );
- $self->add_route_timestamps( $uid, $train, 0 );
- return ( 1, undef );
- }
- );
+ eval {
- $self->helper(
- 'mark_seen' => sub {
- my ( $self, $uid ) = @_;
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
+ }
- $self->pg->db->update(
- 'users',
- { last_seen => DateTime->now( time_zone => 'Europe/Berlin' ) },
- { id => $uid }
- );
- }
- );
+ if ( defined $train
+ and not $train->arrival
+ and not $force )
+ {
+ my $train_no = $train->train_no;
+ die("Train ${train_no} has no arrival timestamp\n");
+ }
+ elsif ( defined $train and $train->arrival ) {
+ $self->in_transit->set_arrival(
+ uid => $uid,
+ db => $db,
+ train => $train,
+ route => [ $self->iris->route_diff($train) ]
+ );
- $self->helper(
- 'update_in_transit_comment' => sub {
- my ( $self, $comment, $uid ) = @_;
- $uid //= $self->current_user->{id};
+ $has_arrived
+ = $train->arrival->epoch < $now->epoch ? 1 : 0;
+ if ($has_arrived) {
+ my @unknown_stations
+ = $self->stations->grep_unknown(
+ $train->route );
+ if (@unknown_stations) {
+ $self->app->log->warn(
+ sprintf(
+'Route of %s %s (%s -> %s) contains unknown stations: %s',
+ $train->type,
+ $train->train_no,
+ $train->origin,
+ $train->destination,
+ join( ', ', @unknown_stations )
+ )
+ );
+ }
+ }
+ }
- my $status = $self->pg->db->select( 'in_transit', ['user_data'],
- { user_id => $uid } )->expand->hash;
- if ( not $status ) {
- return;
- }
- $status->{user_data}{comment} = $comment;
- $self->pg->db->update(
- 'in_transit',
- { user_data => JSON->new->encode( $status->{user_data} ) },
- { user_id => $uid }
- );
- }
- );
+ $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db
+ );
- $self->helper(
- 'update_journey_part' => sub {
- my ( $self, $db, $journey_id, $key, $value ) = @_;
- my $rows;
-
- my $journey = $self->get_journey(
- db => $db,
- journey_id => $journey_id,
- with_datetime => 1,
- );
+ if ( $has_arrived or $force ) {
+ $self->journeys->add_from_in_transit(
+ db => $db,
+ journey => $journey
+ );
+ $self->in_transit->delete(
+ uid => $uid,
+ db => $db
+ );
- eval {
- if ( $key eq 'sched_departure' ) {
- $rows = $db->update(
- 'journeys',
- {
- sched_departure => $value,
- edited => $journey->{edited} | 0x0001,
- },
- {
- id => $journey_id,
+ my $cache_ts = $now->clone;
+ if ( $journey->{real_departure}
+ =~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x
+ )
+ {
+ $cache_ts->set(
+ year => $+{year},
+ month => $+{month}
+ );
+ }
+ $self->journey_stats_cache->invalidate(
+ ts => $cache_ts,
+ db => $db,
+ uid => $uid
+ );
}
- )->rows;
- }
- elsif ( $key eq 'rt_departure' ) {
- $rows = $db->update(
- 'journeys',
- {
- real_departure => $value,
- edited => $journey->{edited} | 0x0002,
- },
+ elsif ( defined $train
+ and $train->arrival_is_cancelled )
{
- id => $journey_id,
- }
- )->rows;
- # stats are partitioned by rt_departure -> both the cache for
- # the old value (see bottom of this function) and the new value
- # (here) must be invalidated.
- $self->invalidate_stats_cache( $value, $db );
- }
- elsif ( $key eq 'sched_arrival' ) {
- $rows = $db->update(
- 'journeys',
- {
- sched_arrival => $value,
- edited => $journey->{edited} | 0x0100,
- },
- {
- id => $journey_id,
- }
- )->rows;
- }
- elsif ( $key eq 'rt_arrival' ) {
- $rows = $db->update(
- 'journeys',
- {
- real_arrival => $value,
- edited => $journey->{edited} | 0x0200,
- },
- {
- id => $journey_id,
- }
- )->rows;
- }
- elsif ( $key eq 'route' ) {
- my @new_route = map { [ $_, {}, undef ] } @{$value};
- $rows = $db->update(
- 'journeys',
- {
- route => JSON->new->encode( \@new_route ),
- edited => $journey->{edited} | 0x0010,
- },
- {
- id => $journey_id,
- }
- )->rows;
- }
- elsif ( $key eq 'cancelled' ) {
- $rows = $db->update(
- 'journeys',
- {
- cancelled => $value,
- edited => $journey->{edited} | 0x0020,
- },
- {
- id => $journey_id,
+ # This branch is only taken if the deparure was not cancelled,
+ # i.e., if the train was supposed to go here but got
+ # redirected or cancelled on the way and not from the start on.
+ # If the departure itself was cancelled, the user route is
+ # cancelled_from action -> 'cancelled journey' panel on main page
+ # -> cancelled_to action -> force checkout (causing the
+ # previous branch to be taken due to $force)
+ $journey->{cancelled} = 1;
+ $self->journeys->add_from_in_transit(
+ db => $db,
+ journey => $journey
+ );
+ $self->in_transit->set_cancelled_destination(
+ uid => $uid,
+ db => $db,
+ cancelled_destination => $train->station,
+ );
}
- )->rows;
- }
- elsif ( $key eq 'comment' ) {
- $journey->{user_data}{comment} = $value;
- $rows = $db->update(
- 'journeys',
- {
- user_data =>
- JSON->new->encode( $journey->{user_data} ),
- },
- {
- id => $journey_id,
+
+ if ( not $opt{in_transaction} ) {
+ $tx->commit;
}
- )->rows;
- }
- else {
- die("Invalid key $key\n");
- }
- };
+ };
- if ($@) {
- $self->app->log->error(
- "update_journey_part($journey_id, $key): $@");
- return "update_journey_part($key): $@";
- }
- if ( $rows == 1 ) {
- $self->invalidate_stats_cache( $journey->{rt_departure}, $db );
- return undef;
- }
- return 'UPDATE failed: did not match any journey part';
- }
- );
+ if ($@) {
+ $self->app->log->error("Checkout($uid): $@");
+ $promise->resolve( 1, 'Checkout error: ' . $@ );
+ return;
+ }
- $self->helper(
- 'journey_sanity_check' => sub {
- my ( $self, $journey, $lax ) = @_;
+ if ( $has_arrived or $force ) {
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkout' );
+ }
+ $promise->resolve( 0, undef );
+ return;
+ }
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'update' );
+ $self->add_route_timestamps( $uid, $train, 0, 1 );
+ }
+ $promise->resolve( 1, undef );
+ return;
- if ( $journey->{sched_duration} and $journey->{sched_duration} < 0 )
- {
- return
-'Die geplante Dauer dieser Zugfahrt ist negativ. Zeitreisen werden aktuell nicht unterstützt.';
- }
- if ( $journey->{rt_duration} and $journey->{rt_duration} < 0 ) {
- return
-'Die Dauer dieser Zugfahrt ist negativ. Zeitreisen werden aktuell nicht unterstützt.';
- }
- if ( $journey->{sched_duration}
- and $journey->{sched_duration} > 60 * 60 * 24 )
- {
- return 'Die Zugfahrt ist länger als 24 Stunden.';
- }
- if ( $journey->{rt_duration}
- and $journey->{rt_duration} > 60 * 60 * 24 )
- {
- return 'Die Zugfahrt ist länger als 24 Stunden.';
- }
- if ( $journey->{kmh_route} > 500 or $journey->{kmh_beeline} > 500 )
- {
- return 'Zugfahrten mit über 500 km/h? Schön wär\'s.';
- }
- if ( $journey->{route} and @{ $journey->{route} } > 99 ) {
- my $stop_count = @{ $journey->{route} };
- return
-"Die Zugfahrt hat $stop_count Unterwegshalte. Also ich weiß ja nicht so recht.";
- }
- if ( $journey->{edited} & 0x0010 and not $lax ) {
- my @unknown_stations
- = $self->grep_unknown_stations( map { $_->[0] }
- @{ $journey->{route} } );
- if (@unknown_stations) {
- return 'Unbekannte Station(en): '
- . join( ', ', @unknown_stations );
}
- }
-
- return undef;
- }
- );
-
- $self->helper(
- 'verify_registration_token' => sub {
- my ( $self, $uid, $token ) = @_;
-
- my $db = $self->pg->db;
- my $tx = $db->begin;
-
- my $res = $db->select(
- 'pending_registrations',
- 'count(*) as count',
- {
- user_id => $uid,
- token => $token
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->resolve( 1, $err );
+ return;
}
- );
+ )->wait;
- if ( $res->hash->{count} ) {
- $db->update( 'users', { status => 1 }, { id => $uid } );
- $db->delete( 'pending_registrations', { user_id => $uid } );
- $tx->commit;
- return 1;
- }
- return;
+ return $promise;
}
);
$self->helper(
- 'get_uid_by_name_and_mail' => sub {
- my ( $self, $name, $email ) = @_;
+ '_checkout_hafas_p' => sub {
+ my ( $self, %opt ) = @_;
- my $res = $self->pg->db->select(
- 'users',
- ['id'],
- {
- name => $name,
- email => $email,
- status => 1
- }
- );
+ my $station = $opt{station};
+ my $force = $opt{force};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
- if ( my $user = $res->hash ) {
- return $user->{id};
- }
- return;
- }
- );
-
- $self->helper(
- 'get_privacy_by_name' => sub {
- my ( $self, $name ) = @_;
+ my $promise = Mojo::Promise->new;
- my $res = $self->pg->db->select(
- 'users',
- [ 'id', 'public_level' ],
- {
- name => $name,
- status => 1
- }
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db,
+ with_data => 1,
+ with_timestamps => 1,
+ with_visibility => 1,
+ postprocess => 1,
);
- if ( my $user = $res->hash ) {
- return $user;
+ # with_visibility needed due to postprocess
+
+ my $found;
+ my $has_arrived;
+ for my $stop ( @{ $journey->{route_after} } ) {
+ if ( $station eq $stop->[0] or $station eq $stop->[1] ) {
+ $found = 1;
+ $self->in_transit->set_arrival_eva(
+ uid => $uid,
+ db => $db,
+ arrival_eva => $stop->[1],
+ );
+ if ( defined $journey->{checkout_station_id}
+ and $journey->{checkout_station_id} != $stop->{eva} )
+ {
+ $self->in_transit->unset_arrival_data(
+ uid => $uid,
+ db => $db
+ );
+ }
+ $self->in_transit->set_arrival_times(
+ uid => $uid,
+ db => $db,
+ sched_arrival => $stop->[2]{sched_arr},
+ rt_arrival =>
+ ( $stop->[2]{rt_arr} || $stop->[2]{sched_arr} )
+ );
+ if (
+ $now > ( $stop->[2]{rt_arr} || $stop->[2]{sched_arr} ) )
+ {
+ $has_arrived = 1;
+ }
+ last;
+ }
}
- return;
- }
- );
-
- $self->helper(
- 'set_privacy' => sub {
- my ( $self, $uid, $public_level ) = @_;
-
- $self->pg->db->update(
- 'users',
- { public_level => $public_level },
- { id => $uid }
- );
- }
- );
-
- $self->helper(
- 'mark_for_password_reset' => sub {
- my ( $self, $db, $uid, $token ) = @_;
-
- my $res = $db->select(
- 'pending_passwords',
- 'count(*) as count',
- { user_id => $uid }
- );
- if ( $res->hash->{count} ) {
- return 'in progress';
+ if ( not $found ) {
+ return $promise->resolve( 1, 'station not found in route' );
}
- $db->insert(
- 'pending_passwords',
- {
- user_id => $uid,
- token => $token,
- requested_at =>
- DateTime->now( time_zone => 'Europe/Berlin' )
+ eval {
+ my $tx;
+ if ( not $opt{in_transaction} ) {
+ $tx = $db->begin;
}
- );
-
- return undef;
- }
- );
- $self->helper(
- 'verify_password_token' => sub {
- my ( $self, $uid, $token ) = @_;
+ if ( $has_arrived or $force ) {
+ $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db
+ );
+ $self->journeys->add_from_in_transit(
+ db => $db,
+ journey => $journey
+ );
+ $self->in_transit->delete(
+ uid => $uid,
+ db => $db
+ );
- my $res = $self->pg->db->select(
- 'pending_passwords',
- 'count(*) as count',
- {
- user_id => $uid,
- token => $token
+ my $cache_ts = $now->clone;
+ if ( $journey->{real_departure}
+ =~ m{ ^ (?<year> \d{4} ) - (?<month> \d{2} ) }x )
+ {
+ $cache_ts->set(
+ year => $+{year},
+ month => $+{month}
+ );
+ }
+ $self->journey_stats_cache->invalidate(
+ ts => $cache_ts,
+ db => $db,
+ uid => $uid
+ );
}
- );
- if ( $res->hash->{count} ) {
- return 1;
- }
- return;
- }
- );
-
- $self->helper(
- 'mark_for_mail_change' => sub {
- my ( $self, $db, $uid, $email, $token ) = @_;
-
- $db->insert(
- 'pending_mails',
- {
- user_id => $uid,
- email => $email,
- token => $token,
- requested_at =>
- DateTime->now( time_zone => 'Europe/Berlin' )
- },
- {
- on_conflict => \
-'(user_id) do update set email = EXCLUDED.email, token = EXCLUDED.token, requested_at = EXCLUDED.requested_at'
- },
- );
- }
- );
-
- $self->helper(
- 'change_mail_with_token' => sub {
- my ( $self, $uid, $token ) = @_;
-
- my $db = $self->pg->db;
- my $tx = $db->begin;
-
- my $res_h = $db->select(
- 'pending_mails',
- ['email'],
- {
- user_id => $uid,
- token => $token
+ if ($tx) {
+ $tx->commit;
}
- )->hash;
+ };
- if ($res_h) {
- $db->update(
- 'users',
- { email => $res_h->{email} },
- { id => $uid }
- );
- $db->delete( 'pending_mails', { user_id => $uid } );
- $tx->commit;
- return 1;
+ if ($@) {
+ $self->app->log->error("Checkout($uid): $@");
+ return $promise->resolve( 1, 'Checkout error: ' . $@ );
}
- return;
- }
- );
- $self->helper(
- 'remove_password_token' => sub {
- my ( $self, $uid, $token ) = @_;
-
- $self->pg->db->delete(
- 'pending_passwords',
- {
- user_id => $uid,
- token => $token
+ if ( $has_arrived or $force ) {
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkout' );
}
- );
+ return $promise->resolve( 0, undef );
+ }
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'update' );
+ }
+ return $promise->resolve( 1, undef );
}
);
@@ -1221,124 +1137,7 @@ sub startup {
$uid //= $self->current_user->{id};
- my $user_data = $self->pg->db->select(
- 'users',
- 'id, name, status, public_level, email, '
- . 'extract(epoch from registered_at) as registered_at_ts, '
- . 'extract(epoch from last_seen) as last_seen_ts, '
- . 'extract(epoch from deletion_requested) as deletion_requested_ts',
- { id => $uid }
- )->hash;
-
- if ($user_data) {
- return {
- id => $user_data->{id},
- name => $user_data->{name},
- status => $user_data->{status},
- is_public => $user_data->{public_level},
- email => $user_data->{email},
- registered_at => DateTime->from_epoch(
- epoch => $user_data->{registered_at_ts},
- time_zone => 'Europe/Berlin'
- ),
- last_seen => DateTime->from_epoch(
- epoch => $user_data->{last_seen_ts},
- time_zone => 'Europe/Berlin'
- ),
- deletion_requested => $user_data->{deletion_requested_ts}
- ? DateTime->from_epoch(
- epoch => $user_data->{deletion_requested_ts},
- time_zone => 'Europe/Berlin'
- )
- : undef,
- };
- }
- return undef;
- }
- );
-
- $self->helper(
- 'get_api_token' => sub {
- my ( $self, $uid ) = @_;
- $uid //= $self->current_user->{id};
-
- my $token = {};
- my $res = $self->pg->db->select(
- 'tokens',
- [ 'type', 'token' ],
- { user_id => $uid }
- );
-
- for my $entry ( $res->hashes->each ) {
- $token->{ $self->app->token_types->[ $entry->{type} - 1 ] }
- = $entry->{token};
- }
-
- return $token;
- }
- );
-
- $self->helper(
- 'get_webhook' => sub {
- my ( $self, $uid ) = @_;
- $uid //= $self->current_user->{id};
-
- my $res_h
- = $self->pg->db->select( 'webhooks_str', '*',
- { user_id => $uid } )->hash;
-
- $res_h->{latest_run} = epoch_to_dt( $res_h->{latest_run_ts} );
-
- return $res_h;
- }
- );
-
- $self->helper(
- 'set_webhook' => sub {
- my ( $self, %opt ) = @_;
-
- $opt{uid} //= $self->current_user->{id};
-
- if ( $opt{token} ) {
- $opt{token} =~ tr{\r\n}{}d;
- }
-
- my $res = $self->pg->db->insert(
- 'webhooks',
- {
- user_id => $opt{uid},
- enabled => $opt{enabled},
- url => $opt{url},
- token => $opt{token}
- },
- {
- on_conflict => \
-'(user_id) do update set enabled = EXCLUDED.enabled, url = EXCLUDED.url, token = EXCLUDED.token, errored = null, latest_run = null, output = null'
- }
- );
- }
- );
-
- $self->helper(
- 'mark_hook_status' => sub {
- my ( $self, $uid, $url, $success, $text ) = @_;
-
- if ( length($text) > 1000 ) {
- $text = substr( $text, 0, 1000 ) . '…';
- }
-
- $self->pg->db->update(
- 'webhooks',
- {
- errored => $success ? 0 : 1,
- latest_run => DateTime->now( time_zone => 'Europe/Berlin' ),
- output => $text,
- },
- {
- user_id => $uid,
- url => $url
- }
- );
+ return $self->users->get( uid => $uid );
}
);
@@ -1346,7 +1145,7 @@ sub startup {
'run_hook' => sub {
my ( $self, $uid, $reason, $callback ) = @_;
- my $hook = $self->get_webhook($uid);
+ my $hook = $self->users->get_webhook( uid => $uid );
if ( not $hook->{enabled} or not $hook->{url} =~ m{^ https?:// }x )
{
@@ -1356,7 +1155,7 @@ sub startup {
return;
}
- my $status = $self->get_user_status_json_v1($uid);
+ my $status = $self->get_user_status_json_v1( uid => $uid );
my $header = {};
my $hook_body = {
reason => $reason,
@@ -1365,6 +1164,8 @@ sub startup {
if ( $hook->{token} ) {
$header->{Authorization} = "Bearer $hook->{token}";
+ $header->{'User-Agent'}
+ = 'travelynx/' . $self->app->config->{version};
}
my $ua = $self->ua;
@@ -1379,804 +1180,194 @@ sub startup {
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
- $self->mark_hook_status( $uid, $hook->{url}, 0,
- "HTTP $err->{code} $err->{message}" );
+ $self->users->update_webhook_status(
+ uid => $uid,
+ url => $hook->{url},
+ success => 0,
+ text => "HTTP $err->{code} $err->{message}"
+ );
}
else {
- $self->mark_hook_status( $uid, $hook->{url}, 1,
- $tx->result->body );
+ $self->users->update_webhook_status(
+ uid => $uid,
+ url => $hook->{url},
+ success => 1,
+ text => $tx->result->body
+ );
}
if ($callback) {
&$callback();
}
+ return;
}
)->catch(
sub {
my ($err) = @_;
- $self->mark_hook_status( $uid, $hook->{url}, 0, $err );
+ $self->users->update_webhook_status(
+ uid => $uid,
+ url => $hook->{url},
+ success => 0,
+ text => $err
+ );
if ($callback) {
&$callback();
}
+ return;
}
)->wait;
}
);
- $self->helper(
- 'get_user_password' => sub {
- my ( $self, $name ) = @_;
-
- my $res_h = $self->pg->db->select(
- 'users',
- 'id, name, status, password as password_hash',
- { name => $name }
- )->hash;
-
- return $res_h;
- }
- );
-
- $self->helper(
- 'add_user' => sub {
- my ( $self, $db, $user_name, $email, $token, $password ) = @_;
-
- # This helper must be called during a transaction, as user creation
- # may fail even after the database entry has been generated, e.g. if
- # the registration mail cannot be sent. We therefore use $db (the
- # database handle performing the transaction) instead of $self->pg->db
- # (which may be a new handle not belonging to the transaction).
-
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
-
- my $res = $db->insert(
- 'users',
- {
- name => $user_name,
- status => 0,
- public_level => 0,
- email => $email,
- password => $password,
- registered_at => $now,
- last_seen => $now,
- },
- { returning => 'id' }
- );
- my $uid = $res->hash->{id};
-
- $db->insert(
- 'pending_registrations',
- {
- user_id => $uid,
- token => $token
- }
- );
-
- return $uid;
- }
- );
-
- $self->helper(
- 'flag_user_deletion' => sub {
- my ( $self, $uid ) = @_;
-
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
-
- $self->pg->db->update(
- 'users',
- { deletion_requested => $now },
- {
- id => $uid,
- }
- );
- }
- );
-
- $self->helper(
- 'unflag_user_deletion' => sub {
- my ( $self, $uid ) = @_;
-
- $self->pg->db->update(
- 'users',
- {
- deletion_requested => undef,
- },
- {
- id => $uid,
- }
- );
- }
- );
-
- $self->helper(
- 'set_user_password' => sub {
- my ( $self, $uid, $password ) = @_;
-
- $self->pg->db->update(
- 'users',
- { password => $password },
- { id => $uid }
- );
- }
- );
-
- $self->helper(
- 'check_if_user_name_exists' => sub {
- my ( $self, $user_name ) = @_;
-
- my $count = $self->pg->db->select(
- 'users',
- 'count(*) as count',
- { name => $user_name }
- )->hash->{count};
-
- if ($count) {
- return 1;
- }
- return 0;
- }
- );
-
- $self->helper(
- 'check_if_mail_is_blacklisted' => sub {
- my ( $self, $mail ) = @_;
-
- my $count = $self->pg->db->select(
- 'users',
- 'count(*) as count',
- {
- email => $mail,
- status => 0,
- }
- )->hash->{count};
-
- if ($count) {
- return 1;
- }
-
- $count = $self->pg->db->select(
- 'mail_blacklist',
- 'count(*) as count',
- {
- email => $mail,
- num_tries => { '>', 1 },
- }
- )->hash->{count};
-
- if ($count) {
- return 1;
- }
- return 0;
- }
- );
-
- $self->helper(
- 'delete_journey' => sub {
- my ( $self, $journey_id, $checkin_epoch, $checkout_epoch ) = @_;
- my $uid = $self->current_user->{id};
-
- my @journeys = $self->get_user_travels(
- uid => $uid,
- journey_id => $journey_id
- );
- if ( @journeys == 0 ) {
- return 'Journey not found';
- }
- my $journey = $journeys[0];
-
- # Double-check (comparing both ID and action epoch) to make sure we
- # are really deleting the right journey and the user isn't just
- # playing around with POST requests.
- if ( $journey->{id} != $journey_id
- or $journey->{checkin_ts} != $checkin_epoch
- or $journey->{checkout_ts} != $checkout_epoch )
- {
- return 'Invalid journey data';
- }
-
- my $rows;
- eval {
- $rows = $self->pg->db->delete(
- 'journeys',
- {
- user_id => $uid,
- id => $journey_id,
- }
- )->rows;
- };
-
- if ($@) {
- $self->app->log->error("Delete($uid, $journey_id): $@");
- return 'DELETE failed: ' . $@;
- }
-
- if ( $rows == 1 ) {
- $self->invalidate_stats_cache(
- epoch_to_dt( $journey->{rt_dep_ts} ) );
- return undef;
- }
- return sprintf( 'Deleted %d rows, expected 1', $rows );
- }
- );
-
- $self->helper(
- 'get_journey_stats' => sub {
- my ( $self, %opt ) = @_;
-
- if ( $opt{cancelled} ) {
- $self->app->log->warning(
-'get_journey_stats called with illegal option cancelled => 1'
- );
- return {};
- }
-
- my $uid = $opt{uid} // $self->current_user->{id};
- my $year = $opt{year} // 0;
- my $month = $opt{month} // 0;
-
- # Assumption: If the stats cache contains an entry it is up-to-date.
- # -> Cache entries must be explicitly invalidated whenever the user
- # checks out of a train or manually edits/adds a journey.
-
- my $res = $self->pg->db->select(
- 'journey_stats',
- ['data'],
- {
- user_id => $uid,
- year => $year,
- month => $month
- }
- );
-
- my $res_h = $res->expand->hash;
-
- if ($res_h) {
- $res->finish;
- return $res_h->{data};
- }
-
- my $interval_start = DateTime->new(
- time_zone => 'Europe/Berlin',
- year => 2000,
- month => 1,
- day => 1,
- hour => 0,
- minute => 0,
- second => 0,
- );
-
- # I wonder if people will still be traveling by train in the year 3000
- my $interval_end = $interval_start->clone->add( years => 1000 );
-
- if ( $opt{year} and $opt{month} ) {
- $interval_start->set(
- year => $opt{year},
- month => $opt{month}
- );
- $interval_end = $interval_start->clone->add( months => 1 );
- }
- elsif ( $opt{year} ) {
- $interval_start->set( year => $opt{year} );
- $interval_end = $interval_start->clone->add( years => 1 );
- }
-
- my @journeys = $self->get_user_travels(
- uid => $uid,
- cancelled => $opt{cancelled} ? 1 : 0,
- verbose => 1,
- after => $interval_start,
- before => $interval_end
- );
- my $stats = $self->compute_journey_stats(@journeys);
-
- eval {
- $self->pg->db->insert(
- 'journey_stats',
- {
- user_id => $uid,
- year => $year,
- month => $month,
- data => JSON->new->encode($stats),
- }
- );
- };
- if ( my $err = $@ ) {
- if ( $err =~ m{duplicate key value violates unique constraint} )
- {
- # When a user opens the same history page several times in
- # short succession, there is a race condition where several
- # Mojolicious workers execute this helper, notice that there is
- # no up-to-date history, compute it, and insert it using the
- # statement above. This will lead to a uniqueness violation
- # in each successive insert. However, this is harmless, and
- # thus ignored.
- }
- else {
- # Otherwise we probably have a problem.
- die($@);
- }
- }
-
- return $stats;
- }
- );
-
- $self->helper(
- 'history_years' => sub {
- my ( $self, $uid ) = @_;
- $uid //= $self->current_user->{id},
-
- my $res = $self->pg->db->select(
- 'journeys',
- 'distinct extract(year from real_departure) as year',
- { user_id => $uid },
- { order_by => { -asc => 'year' } }
- );
-
- my @ret;
- for my $row ( $res->hashes->each ) {
- push( @ret, [ $row->{year}, $row->{year} ] );
- }
- return @ret;
- }
- );
-
- $self->helper(
- 'history_months' => sub {
- my ( $self, $uid ) = @_;
- $uid //= $self->current_user->{id},
-
- my $res = $self->pg->db->select(
- 'journeys',
- "distinct to_char(real_departure, 'YYYY.MM') as yearmonth",
- { user_id => $uid },
- { order_by => { -asc => 'yearmonth' } }
- );
-
- my @ret;
- for my $row ( $res->hashes->each ) {
- my ( $year, $month ) = split( qr{[.]}, $row->{yearmonth} );
- push( @ret, [ "${year}/${month}", "${month}.${year}" ] );
- }
- return @ret;
- }
- );
-
- $self->helper(
- 'route_diff' => sub {
- my ( $self, $train ) = @_;
- my @json_route;
- my @route = $train->route;
- my @sched_route = $train->sched_route;
-
- my $route_idx = 0;
- my $sched_idx = 0;
-
- while ( $route_idx <= $#route and $sched_idx <= $#sched_route ) {
- if ( $route[$route_idx] eq $sched_route[$sched_idx] ) {
- push( @json_route, [ $route[$route_idx], {}, undef ] );
- $route_idx++;
- $sched_idx++;
- }
-
- # this branch is inefficient, but won't be taken frequently
- elsif ( not( grep { $_ eq $route[$route_idx] } @sched_route ) )
- {
- push( @json_route,
- [ $route[$route_idx], {}, 'additional' ],
- );
- $route_idx++;
- }
- else {
- push( @json_route,
- [ $sched_route[$sched_idx], {}, 'cancelled' ],
- );
- $sched_idx++;
- }
- }
- while ( $route_idx <= $#route ) {
- push( @json_route, [ $route[$route_idx], {}, 'additional' ], );
- $route_idx++;
- }
- while ( $sched_idx <= $#sched_route ) {
- push( @json_route,
- [ $sched_route[$sched_idx], {}, 'cancelled' ],
- );
- $sched_idx++;
- }
- return @json_route;
- }
- );
-
- $self->helper(
- 'get_dbdb_station_p' => sub {
- my ( $self, $eva ) = @_;
-
- my $url = "https://lib.finalrewind.org/dbdb/s/${eva}.json";
-
- my $cache = $self->app->cache_iris_main;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- $promise->resolve($content);
- return $promise;
- }
-
- $self->ua->request_timeout(5)->get_p($url)->then(
- sub {
- my ($tx) = @_;
- my $body = decode( 'utf-8', $tx->res->body );
-
- my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
- $promise->resolve($json);
- }
- )->catch(
- sub {
- my ($err) = @_;
- $promise->reject($err);
- }
- )->wait;
- return $promise;
- }
- );
-
- $self->helper(
- 'has_wagonorder_p' => sub {
- my ( $self, $ts, $train_no ) = @_;
- my $api_ts = $ts->strftime('%Y%m%d%H%M');
- my $url
- = "https://lib.finalrewind.org/dbdb/has_wagonorder/${train_no}/${api_ts}";
- my $cache = $self->app->cache_iris_main;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->get($url) ) {
- if ( $content eq 'y' ) {
- $promise->resolve;
- return $promise;
- }
- elsif ( $content eq 'n' ) {
- $promise->reject;
- return $promise;
- }
- }
-
- $self->ua->request_timeout(5)->head_p($url)->then(
- sub {
- my ($tx) = @_;
- if ( $tx->result->is_success ) {
- $cache->set( $url, 'y' );
- $promise->resolve;
- }
- else {
- $cache->set( $url, 'n' );
- $promise->resolve;
- }
- }
- )->catch(
- sub {
- $cache->set( $url, 'n' );
- $promise->reject;
- }
- )->wait;
- return $promise;
- }
- );
-
- $self->helper(
- 'get_wagonorder_p' => sub {
- my ( $self, $ts, $train_no ) = @_;
- my $api_ts = $ts->strftime('%Y%m%d%H%M');
- my $url
- = "https://www.apps-bahn.de/wr/wagenreihung/1.0/${train_no}/${api_ts}";
-
- my $cache = $self->app->cache_iris_main;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- $promise->resolve($content);
- return $promise;
- }
-
- $self->ua->request_timeout(5)->get_p($url)->then(
- sub {
- my ($tx) = @_;
- my $body = decode( 'utf-8', $tx->res->body );
-
- my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
- $promise->resolve($json);
- }
- )->catch(
- sub {
- my ($err) = @_;
- $promise->reject($err);
- }
- )->wait;
- return $promise;
- }
- );
-
- $self->helper(
- 'get_hafas_json_p' => sub {
- my ( $self, $url ) = @_;
-
- my $cache = $self->app->cache_iris_main;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- $promise->resolve($content);
- return $promise;
- }
-
- $self->ua->request_timeout(5)->get_p($url)->then(
- sub {
- my ($tx) = @_;
- my $body = decode( 'ISO-8859-15', $tx->res->body );
-
- $body =~ s{^TSLs[.]sls = }{};
- $body =~ s{;$}{};
- $body =~ s{&#x0028;}{(}g;
- $body =~ s{&#x0029;}{)}g;
- my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
- $promise->resolve($json);
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->app->log->warning("get($url): $err");
- $promise->reject($err);
- }
- )->wait;
- return $promise;
- }
- );
-
- $self->helper(
- 'get_hafas_xml_p' => sub {
- my ( $self, $url ) = @_;
-
- my $cache = $self->app->cache_iris_rt;
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- $promise->resolve($content);
- return $promise;
- }
-
- $self->ua->request_timeout(5)->get_p($url)->then(
- sub {
- my ($tx) = @_;
- my $body = decode( 'ISO-8859-15', $tx->res->body );
- my $tree;
-
- my $traininfo = {
- station => {},
- messages => [],
- };
-
- # <SDay text="... &gt; ..."> is invalid HTML, but present in
- # regardless. As it is the last tag, we just throw it away.
- $body =~ s{<SDay [^>]*/>}{}s;
- eval { $tree = XML::LibXML->load_xml( string => $body ) };
- if ($@) {
- $self->app->log->warning("load_xml($url): $@");
- $cache->freeze( $url, $traininfo );
- $promise->resolve($traininfo);
- return;
- }
-
- for my $station ( $tree->findnodes('/Journey/St') ) {
- my $name = $station->getAttribute('name');
- my $adelay = $station->getAttribute('adelay');
- my $ddelay = $station->getAttribute('ddelay');
- $traininfo->{station}{$name} = {
- adelay => $adelay,
- ddelay => $ddelay,
- };
- }
-
- for my $message ( $tree->findnodes('/Journey/HIMMessage') )
- {
- my $header = $message->getAttribute('header');
- my $lead = $message->getAttribute('lead');
- my $display = $message->getAttribute('display');
- push(
- @{ $traininfo->{messages} },
- {
- header => $header,
- lead => $lead,
- display => $display
- }
- );
- }
-
- $cache->freeze( $url, $traininfo );
- $promise->resolve($traininfo);
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->app->log->warning("get($url): $err");
- $promise->reject($err);
- }
- )->wait;
- return $promise;
- }
- );
-
+ # This helper is only ever called from an IRIS context.
+ # HAFAS already has all relevant information.
$self->helper(
'add_route_timestamps' => sub {
- my ( $self, $uid, $train, $is_departure ) = @_;
+ my ( $self, $uid, $train, $is_departure, $update_polyline ) = @_;
$uid //= $self->current_user->{id};
my $db = $self->pg->db;
- my $journey = $db->select(
- 'in_transit_str',
- [ 'arr_eva', 'dep_eva', 'route' ],
- { user_id => $uid }
- )->expand->hash;
+ # TODO "with_timestamps" is misleading, there are more differences between in_transit and in_transit_str
+ # Here it's only needed because of dep_eva / arr_eva names
+ my $in_transit = $self->in_transit->get(
+ db => $db,
+ uid => $uid,
+ with_data => 1,
+ with_timestamps => 1
+ );
- if ( not $journey ) {
+ if ( not $in_transit ) {
return;
}
- my ($platform) = ( ( $train->platform // 0 ) =~ m{(\d+)} );
-
- my $route = $journey->{route};
-
- my $base
- = 'https://reiseauskunft.bahn.de/bin/trainsearch.exe/dn?L=vs_json.vs_hap&start=yes&rt=1';
- my $date_yy = $train->start->strftime('%d.%m.%y');
- my $date_yyyy = $train->start->strftime('%d.%m.%Y');
- my $train_no = $train->type . ' ' . $train->train_no;
-
- my ( $trainlink, $route_data );
-
- $self->get_hafas_json_p(
- "${base}&date=${date_yy}&trainname=${train_no}")->then(
- sub {
- my ($trainsearch) = @_;
-
- # Fallback: Take first result
- $trainlink = $trainsearch->{suggestions}[0]{trainLink};
+ my $route = $in_transit->{route};
- # Try finding a result for the current date
- for
- my $suggestion ( @{ $trainsearch->{suggestions} // [] } )
- {
-
- # Drunken API, sail with care. Both date formats are used interchangeably
- if ( $suggestion->{depDate} eq $date_yy
- or $suggestion->{depDate} eq $date_yyyy )
- {
- # Train numbers are not unique, e.g. IC 149 refers both to the
- # InterCity service Amsterdam -> Berlin and to the InterCity service
- # Koebenhavns Lufthavn st -> Aarhus. One workaround is making
- # requests with the stationFilter=80 parameter. Checking the origin
- # station seems to be the more generic solution, so we do that
- # instead.
- if ( $suggestion->{dep} eq $train->origin ) {
- $trainlink = $suggestion->{trainLink};
- last;
- }
- }
- }
-
- if ( not $trainlink ) {
- $self->app->log->debug("trainlink not found");
- return Mojo::Promise->reject("trainlink not found");
- }
- my $base2
- = 'https://reiseauskunft.bahn.de/bin/traininfo.exe/dn';
- return $self->get_hafas_json_p(
-"${base2}/${trainlink}?rt=1&date=${date_yy}&L=vs_json.vs_hap"
- );
- }
- )->then(
+ $self->hafas->get_tripid_p( train => $train )->then(
sub {
- my ($traininfo) = @_;
- if ( not $traininfo or $traininfo->{error} ) {
- $self->app->log->debug("traininfo error");
- return Mojo::Promise->reject("traininfo error");
- }
- my $routeinfo
- = $traininfo->{suggestions}[0]{locations};
+ my ($trip_id) = @_;
- my $strp = DateTime::Format::Strptime->new(
- pattern => '%d.%m.%y %H:%M',
- time_zone => 'Europe/Berlin',
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => { trip_id => $trip_id }
);
- $route_data = {};
-
- for my $station ( @{$routeinfo} ) {
- my $arr
- = $strp->parse_datetime(
- $station->{arrDate} . ' ' . $station->{arrTime} );
- my $dep
- = $strp->parse_datetime(
- $station->{depDate} . ' ' . $station->{depTime} );
- $route_data->{ $station->{name} } = {
- sched_arr => $arr ? $arr->epoch : 0,
- sched_dep => $dep ? $dep->epoch : 0,
- };
- }
-
- my $base2
- = 'https://reiseauskunft.bahn.de/bin/traininfo.exe/dn';
- return $self->get_hafas_xml_p(
- "${base2}/${trainlink}?rt=1&date=${date_yy}&L=vs_java3"
+ return $self->hafas->get_route_timestamps_p(
+ train => $train,
+ trip_id => $trip_id,
+ with_polyline => (
+ $update_polyline
+ or not $in_transit->{polyline}
+ ) ? 1 : 0,
);
}
)->then(
sub {
- my ($traininfo2) = @_;
+ my ( $route_data, $journey, $polyline ) = @_;
- for my $station ( keys %{$route_data} ) {
- for my $key (
- keys %{ $traininfo2->{station}{$station} // {} } )
+ for my $station ( @{$route} ) {
+ if ( $station->[0]
+ =~ m{^Betriebsstelle nicht bekannt (\d+)$} )
{
- $route_data->{$station}{$key}
- = $traininfo2->{station}{$station}{$key};
+ my $eva = $1;
+ if ( $route_data->{$eva} ) {
+ $station->[0] = $route_data->{$eva}{name};
+ $station->[1] = $route_data->{$eva}{eva};
+ }
}
- }
+ if ( my $sd = $route_data->{ $station->[0] } ) {
+ $station->[1] = $sd->{eva};
+ if ( $station->[2]{isAdditional} ) {
+ $sd->{isAdditional} = 1;
+ }
+ if ( $station->[2]{isCancelled} ) {
+ $sd->{isCancelled} = 1;
+ }
- for my $station ( @{$route} ) {
- $station->[1]
- = $route_data->{ $station->[0] };
+ # keep rt_dep / rt_arr if they are no longer present
+ my %old;
+ for my $k (qw(rt_arr rt_dep arr_delay dep_delay)) {
+ $old{$k} = $station->[2]{$k};
+ }
+ $station->[2] = $sd;
+ if ( not $station->[2]{rt_arr} ) {
+ $station->[2]{rt_arr} = $old{rt_arr};
+ $station->[2]{arr_delay} = $old{arr_delay};
+ }
+ if ( not $station->[2]{rt_dep} ) {
+ $station->[2]{rt_dep} = $old{rt_dep};
+ $station->[2]{dep_delay} = $old{dep_delay};
+ }
+ }
}
- my $res = $db->select( 'in_transit', ['data'],
- { user_id => $uid } );
- my $res_h = $res->expand->hash;
- my $data = $res_h->{data} // {};
+ my @messages;
+ for my $m ( $journey->messages ) {
+ if ( not $m->code ) {
+ push(
+ @messages,
+ {
+ header => $m->short,
+ lead => $m->text,
+ }
+ );
+ }
+ }
- $data->{delay_msg} = [ map { [ $_->[0]->epoch, $_->[1] ] }
- $train->delay_messages ];
- $data->{qos_msg} = [ map { [ $_->[0]->epoch, $_->[1] ] }
- $train->qos_messages ];
+ $self->in_transit->set_route_data(
+ uid => $uid,
+ db => $db,
+ route => $route,
+ delay_messages => [
+ map { [ $_->[0]->epoch, $_->[1] ] }
+ $train->delay_messages
+ ],
+ qos_messages => [
+ map { [ $_->[0]->epoch, $_->[1] ] }
+ $train->qos_messages
+ ],
+ him_messages => \@messages,
+ );
- $data->{him_msg} = $traininfo2->{messages};
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ old_id => $in_transit->{polyline_id},
+ );
+ }
- $db->update(
- 'in_transit',
- {
- route => JSON->new->encode($route),
- data => JSON->new->encode($data)
- },
- { user_id => $uid }
- );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->app->log->debug("add_route_timestamps: $err");
+ return;
}
)->wait;
if ( $train->sched_departure ) {
- $self->has_wagonorder_p( $train->sched_departure,
+ $self->dbdb->has_wagonorder_p( $train->sched_departure,
$train->train_no )->then(
sub {
- return $self->get_wagonorder_p( $train->sched_departure,
- $train->train_no );
+ my ($api) = @_;
+ return $self->dbdb->get_wagonorder_p( $api,
+ $train->sched_departure, $train->train_no );
}
)->then(
sub {
my ($wagonorder) = @_;
- my $res = $db->select(
- 'in_transit',
- [ 'data', 'user_data' ],
- { user_id => $uid }
- );
- my $res_h = $res->expand->hash;
- my $data = $res_h->{data} // {};
- my $user_data = $res_h->{user_data} // {};
-
- if ($is_departure) {
- $data->{wagonorder_dep} = $wagonorder;
- if ( exists $user_data->{wagongroups} ) {
- $user_data->{wagongroups} = [];
- }
+ my $data = {};
+ my $user_data = {};
+
+ if ( $is_departure and not exists $wagonorder->{error} )
+ {
+ $data->{wagonorder_dep} = $wagonorder;
+ $user_data->{wagongroups} = [];
for my $group (
@{
$wagonorder->{data}{istformation}
@@ -2191,7 +1382,7 @@ sub startup {
push(
@wagons,
{
- id => $wagon->{fahrzeugnummer},
+ id => $wagon->{fahrzeugnummer},
number =>
$wagon->{wagenordnungsnummer},
type => $wagon->{fahrzeugtyp},
@@ -2210,449 +1401,99 @@ sub startup {
wagons => [@wagons],
}
);
- }
- $db->update(
- 'in_transit',
+ if ( $group->{fahrzeuggruppebezeichnung}
+ and $group->{fahrzeuggruppebezeichnung} eq
+ 'ICE0304' )
{
- data => JSON->new->encode($data),
- user_data => JSON->new->encode($user_data)
- },
- { user_id => $uid }
+ $data->{wagonorder_pride} = 1;
+ }
+ }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data
+ );
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ db => $db,
+ user_data => $user_data
);
}
- else {
+ elsif ( not $is_departure
+ and not exists $wagonorder->{error} )
+ {
$data->{wagonorder_arr} = $wagonorder;
- $db->update(
- 'in_transit',
- { data => JSON->new->encode($data) },
- { user_id => $uid }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data
);
}
+ return;
+ }
+ )->catch(
+ sub {
+ # no wagonorder? no problem.
+ return;
}
)->wait;
}
if ($is_departure) {
- $self->get_dbdb_station_p( $journey->{dep_eva} )->then(
+ $self->dbdb->get_stationinfo_p( $in_transit->{dep_eva} )->then(
sub {
my ($station_info) = @_;
+ my $data = { stationinfo_dep => $station_info };
- my $res = $db->select( 'in_transit', ['data'],
- { user_id => $uid } );
- my $res_h = $res->expand->hash;
- my $data = $res_h->{data} // {};
-
- $data->{stationinfo_dep} = $station_info;
-
- $db->update(
- 'in_transit',
- { data => JSON->new->encode($data) },
- { user_id => $uid }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data
);
+ return;
+ }
+ )->catch(
+ sub {
+ # no stationinfo? no problem.
+ return;
}
)->wait;
}
- if ( $journey->{arr_eva} and not $is_departure ) {
- $self->get_dbdb_station_p( $journey->{arr_eva} )->then(
+ if ( $in_transit->{arr_eva} and not $is_departure ) {
+ $self->dbdb->get_stationinfo_p( $in_transit->{arr_eva} )->then(
sub {
my ($station_info) = @_;
+ my $data = { stationinfo_arr => $station_info };
- my $res = $db->select( 'in_transit', ['data'],
- { user_id => $uid } );
- my $res_h = $res->expand->hash;
- my $data = $res_h->{data} // {};
-
- $data->{stationinfo_arr} = $station_info;
-
- $db->update(
- 'in_transit',
- { data => JSON->new->encode($data) },
- { user_id => $uid }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data
);
+ return;
}
- )->wait;
- }
- }
- );
-
- $self->helper(
- 'get_oldest_journey_ts' => sub {
- my ($self) = @_;
-
- my $res_h = $self->pg->db->select(
- 'journeys_str',
- ['sched_dep_ts'],
- {
- user_id => $self->current_user->{id},
- },
- {
- limit => 1,
- order_by => {
- -asc => 'real_dep_ts',
- },
- }
- )->hash;
-
- if ($res_h) {
- return epoch_to_dt( $res_h->{sched_dep_ts} );
- }
- return undef;
- }
- );
-
- $self->helper(
- 'get_latest_dest_id' => sub {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid} // $self->current_user->{id};
- my $db = $opt{db} // $self->pg->db;
-
- my $journey = $db->select( 'in_transit', ['checkout_station_id'],
- { user_id => $uid } )->hash;
- if ( not $journey ) {
- $journey = $db->select(
- 'journeys',
- ['checkout_station_id'],
- {
- user_id => $uid,
- cancelled => 0
- },
- {
- limit => 1,
- order_by => { -desc => 'real_departure' }
- }
- )->hash;
- }
-
- if ( not $journey ) {
- return;
- }
-
- return $journey->{checkout_station_id};
- }
- );
-
- $self->helper(
- 'get_connection_targets' => sub {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid} //= $self->current_user->{id};
- my $threshold = $opt{threshold}
- // DateTime->now( time_zone => 'Europe/Berlin' )
- ->subtract( months => 4 );
- my $db = $opt{db} //= $self->pg->db;
- my $min_count = $opt{min_count} // 3;
-
- my $dest_id = $opt{eva} // $self->get_latest_dest_id(%opt);
-
- if ( not $dest_id ) {
- return;
- }
-
- my $res = $db->query(
- qq{
- select
- count(checkout_station_id) as count,
- checkout_station_id as dest
- from journeys
- where user_id = ?
- and checkin_station_id = ?
- and real_departure > ?
- group by checkout_station_id
- order by count desc;
- },
- $uid,
- $dest_id,
- $threshold
- );
- my @destinations
- = $res->hashes->grep( sub { shift->{count} >= $min_count } )
- ->map( sub { shift->{dest} } )->each;
- @destinations
- = grep { $self->app->station_by_eva->{$_} } @destinations;
- @destinations
- = map { $self->app->station_by_eva->{$_}->[1] } @destinations;
- return @destinations;
- }
- );
-
- $self->helper(
- 'get_connecting_trains' => sub {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid} //= $self->current_user->{id};
- my $use_history = $self->account_use_history($uid);
-
- my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );
-
- if ( $opt{eva} ) {
- if ( $use_history & 0x01 ) {
- $eva = $opt{eva};
- }
- }
- else {
- 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};
- $exclude_before = $status->{real_arrival}->epoch;
- }
- }
-
- if ( not $eva ) {
- return;
- }
-
- my @destinations = $self->get_connection_targets(%opt);
-
- if ($exclude_via) {
- @destinations = grep { $_ ne $exclude_via } @destinations;
- }
-
- if ( not @destinations ) {
- return;
- }
-
- my $stationboard = $self->get_departures( $eva, 0, 40, 1 );
- if ( $stationboard->{errstr} ) {
- return;
- }
- @{ $stationboard->{results} } = map { $_->[0] }
- sort { $a->[1] <=> $b->[1] }
- map { [ $_, $_->departure ? $_->departure->epoch : 0 ] }
- @{ $stationboard->{results} };
- my @results;
- my @cancellations;
- my %via_count = map { $_ => 0 } @destinations;
- for my $train ( @{ $stationboard->{results} } ) {
- if ( not $train->departure ) {
- next;
- }
- if ( $exclude_before
- and $train->departure->epoch < $exclude_before )
- {
- next;
- }
- if ( $exclude_train_id
- and $train->train_id eq $exclude_train_id )
- {
- 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 ( List::Util::any { $_ eq $dest } @via ) {
- push( @cancellations, [ $train, $dest ] );
- next;
- }
- }
- }
- else {
- my @via = ( $train->route_post, $train->route_end );
- for my $dest (@destinations) {
- if ( $via_count{$dest} < 2
- and List::Util::any { $_ eq $dest } @via )
- {
- push( @results, [ $train, $dest ] );
- $via_count{$dest}++;
- next;
- }
- }
- }
- }
-
- @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;
-
- return ( @results, @cancellations );
- }
- );
-
- $self->helper(
- 'account_use_history' => sub {
- my ( $self, $uid, $value ) = @_;
-
- if ($value) {
- $self->pg->db->update(
- 'users',
- { use_history => $value },
- { id => $uid }
- );
- }
- else {
- return $self->pg->db->select( 'users', ['use_history'],
- { id => $uid } )->hash->{use_history};
- }
- }
- );
-
- $self->helper(
- 'get_user_travels' => sub {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid} || $self->current_user->{id};
-
- # If get_user_travels is called from inside a transaction, db
- # specifies the database handle performing the transaction.
- # Otherwise, we grab a fresh one.
- my $db = $opt{db} // $self->pg->db;
-
- my %where = (
- user_id => $uid,
- cancelled => 0
- );
- my %order = (
- order_by => {
- -desc => 'real_dep_ts',
- }
- );
-
- if ( $opt{cancelled} ) {
- $where{cancelled} = 1;
- }
-
- if ( $opt{limit} ) {
- $order{limit} = $opt{limit};
- }
-
- if ( $opt{journey_id} ) {
- $where{journey_id} = $opt{journey_id};
- delete $where{cancelled};
- }
- elsif ( $opt{after} and $opt{before} ) {
- $where{real_dep_ts} = {
- -between => [ $opt{after}->epoch, $opt{before}->epoch, ] };
- }
-
- my @travels;
-
- my $res = $db->select( 'journeys_str', '*', \%where, \%order );
-
- for my $entry ( $res->expand->hashes->each ) {
-
- my $ref = {
- id => $entry->{journey_id},
- type => $entry->{train_type},
- line => $entry->{train_line},
- no => $entry->{train_no},
- from_eva => $entry->{dep_eva},
- checkin_ts => $entry->{checkin_ts},
- sched_dep_ts => $entry->{sched_dep_ts},
- rt_dep_ts => $entry->{real_dep_ts},
- to_eva => $entry->{arr_eva},
- checkout_ts => $entry->{checkout_ts},
- sched_arr_ts => $entry->{sched_arr_ts},
- rt_arr_ts => $entry->{real_arr_ts},
- messages => $entry->{messages},
- route => $entry->{route},
- edited => $entry->{edited},
- user_data => $entry->{user_data},
- };
-
- if ( my $station
- = $self->app->station_by_eva->{ $ref->{from_eva} } )
- {
- $ref->{from_ds100} = $station->[0];
- $ref->{from_name} = $station->[1];
- }
- if ( my $station
- = $self->app->station_by_eva->{ $ref->{to_eva} } )
- {
- $ref->{to_ds100} = $station->[0];
- $ref->{to_name} = $station->[1];
- }
-
- if ( $opt{with_datetime} ) {
- $ref->{checkin} = epoch_to_dt( $ref->{checkin_ts} );
- $ref->{sched_departure}
- = epoch_to_dt( $ref->{sched_dep_ts} );
- $ref->{rt_departure} = epoch_to_dt( $ref->{rt_dep_ts} );
- $ref->{checkout} = epoch_to_dt( $ref->{checkout_ts} );
- $ref->{sched_arrival} = epoch_to_dt( $ref->{sched_arr_ts} );
- $ref->{rt_arrival} = epoch_to_dt( $ref->{rt_arr_ts} );
- }
-
- if ( $opt{verbose} ) {
- $ref->{cancelled} = $entry->{cancelled};
- my @parsed_messages;
- for my $message ( @{ $ref->{messages} // [] } ) {
- my ( $ts, $msg ) = @{$message};
- push( @parsed_messages, [ epoch_to_dt($ts), $msg ] );
+ )->catch(
+ sub {
+ # no stationinfo? no problem.
+ return;
}
- $ref->{messages} = [ reverse @parsed_messages ];
- $ref->{sched_duration}
- = $ref->{sched_arr_ts}
- ? $ref->{sched_arr_ts} - $ref->{sched_dep_ts}
- : undef;
- $ref->{rt_duration}
- = $ref->{rt_arr_ts}
- ? $ref->{rt_arr_ts} - $ref->{rt_dep_ts}
- : undef;
- my ( $km_route, $km_beeline, $skip )
- = $self->get_travel_distance( $ref->{from_name},
- $ref->{to_name}, $ref->{route} );
- $ref->{km_route} = $km_route;
- $ref->{skip_route} = $skip;
- $ref->{km_beeline} = $km_beeline;
- $ref->{skip_beeline} = $skip;
- my $kmh_divisor
- = ( $ref->{rt_duration} // $ref->{sched_duration}
- // 999999 ) / 3600;
- $ref->{kmh_route}
- = $kmh_divisor ? $ref->{km_route} / $kmh_divisor : -1;
- $ref->{kmh_beeline}
- = $kmh_divisor
- ? $ref->{km_beeline} / $kmh_divisor
- : -1;
- }
-
- push( @travels, $ref );
+ )->wait;
}
-
- return @travels;
}
);
$self->helper(
- 'get_journey' => sub {
- my ( $self, %opt ) = @_;
-
- $opt{cancelled} = 'any';
- my @journeys = $self->get_user_travels(%opt);
- if ( @journeys == 0 ) {
- return undef;
- }
-
- return $journeys[0];
+ 'resolve_sb_template' => sub {
+ my ( $self, $template, %opt ) = @_;
+ my $ret = $template;
+ my $name = $opt{name} =~ s{/}{%2F}gr;
+ $ret =~ s{[{]eva[}]}{$opt{eva}}g;
+ $ret =~ s{[{]name[}]}{$name}g;
+ $ret =~ s{[{]tt[}]}{$opt{tt}}g;
+ $ret =~ s{[{]tn[}]}{$opt{tn}}g;
+ $ret =~ s{[{]id[}]}{$opt{id}}g;
+ return $ret;
}
);
@@ -2720,21 +1561,17 @@ sub startup {
for my $station ( @{ $journey->{route_after} } ) {
my $station_desc = $station->[0];
- if ( $station->[1]{rt_arr} ) {
- $station_desc .= $station->[1]{sched_arr}->strftime(';%s');
- $station_desc .= $station->[1]{rt_arr}->strftime(';%s');
- if ( $station->[1]{rt_dep} ) {
- $station_desc
- .= $station->[1]{sched_dep}->strftime(';%s');
- $station_desc .= $station->[1]{rt_dep}->strftime(';%s');
- }
- else {
- $station_desc .= ';0;0';
- }
- }
- else {
- $station_desc .= ';0;0;0;0';
- }
+
+ my $sa = $station->[2]{sched_arr};
+ my $ra = $station->[2]{rt_arr} || $station->[2]{sched_arr};
+ my $sd = $station->[2]{sched_dep};
+ my $rd = $station->[2]{rt_dep} || $station->[2]{sched_dep};
+
+ $station_desc .= $sa ? $sa->strftime(';%s') : ';0';
+ $station_desc .= $ra ? $ra->strftime(';%s') : ';0';
+ $station_desc .= $sd ? $sd->strftime(';%s') : ';0';
+ $station_desc .= $rd ? $rd->strftime(';%s') : ';0';
+
push( @route, $station_desc );
}
@@ -2744,166 +1581,87 @@ sub startup {
$self->helper(
'get_user_status' => sub {
- my ( $self, $uid ) = @_;
+ my ( $self, $uid, $db ) = @_;
$uid //= $self->current_user->{id};
+ $db //= $self->pg->db;
- my $db = $self->pg->db;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
my $epoch = $now->epoch;
- my $in_transit
- = $db->select( 'in_transit_str', '*', { user_id => $uid } )
- ->expand->hash;
+ my $in_transit = $self->in_transit->get(
+ uid => $uid,
+ db => $db,
+ with_data => 1,
+ with_timestamps => 1,
+ with_visibility => 1,
+ postprocess => 1,
+ );
if ($in_transit) {
+ my $ret = $in_transit;
- if ( my $station
- = $self->app->station_by_eva->{ $in_transit->{dep_eva} } )
- {
- $in_transit->{dep_ds100} = $station->[0];
- $in_transit->{dep_name} = $station->[1];
- }
- if ( $in_transit->{arr_eva}
- and my $station
- = $self->app->station_by_eva->{ $in_transit->{arr_eva} } )
+ my $traewelling = $self->traewelling->get(
+ uid => $uid,
+ db => $db
+ );
+ if ( $traewelling->{latest_run}
+ >= epoch_to_dt( $in_transit->{checkin_ts} ) )
{
- $in_transit->{arr_ds100} = $station->[0];
- $in_transit->{arr_name} = $station->[1];
- }
-
- my @route = @{ $in_transit->{route} // [] };
- my @route_after;
- my $dep_info;
- my $stop_before_dest;
- my $is_after = 0;
- for my $station (@route) {
-
- if ( $in_transit->{arr_name}
- and @route_after
- and $station->[0] eq $in_transit->{arr_name} )
+ $ret->{traewelling} = $traewelling;
+ if ( @{ $traewelling->{data}{log} // [] }
+ and ( my $log_entry = $traewelling->{data}{log}[0] ) )
{
- $stop_before_dest = $route_after[-1][0];
- }
- if ($is_after) {
- push( @route_after, $station );
- }
- if ( $in_transit->{dep_name}
- and $station->[0] eq $in_transit->{dep_name} )
- {
- $is_after = 1;
- if ( @{$station} > 1 ) {
- $dep_info = $station->[1];
+ if ( $log_entry->[2] ) {
+ $ret->{traewelling_status} = $log_entry->[2];
+ $ret->{traewelling_url}
+ = 'https://traewelling.de/status/'
+ . $log_entry->[2];
}
+ $ret->{traewelling_log_latest} = $log_entry->[1];
}
}
- my $stop_after_dep = @route_after ? $route_after[0][0] : undef;
-
- my $ts = $in_transit->{checkout_ts}
- // $in_transit->{checkin_ts};
- my $action_time = epoch_to_dt($ts);
-
- my $ret = {
- checked_in => !$in_transit->{cancelled},
- cancelled => $in_transit->{cancelled},
- timestamp => $action_time,
- timestamp_delta => $now->epoch - $action_time->epoch,
- train_type => $in_transit->{train_type},
- train_line => $in_transit->{train_line},
- train_no => $in_transit->{train_no},
- train_id => $in_transit->{train_id},
- boarding_countdown => -1,
- sched_departure =>
- epoch_to_dt( $in_transit->{sched_dep_ts} ),
- real_departure => epoch_to_dt( $in_transit->{real_dep_ts} ),
- dep_ds100 => $in_transit->{dep_ds100},
- dep_eva => $in_transit->{dep_eva},
- dep_name => $in_transit->{dep_name},
- dep_platform => $in_transit->{dep_platform},
- sched_arrival => epoch_to_dt( $in_transit->{sched_arr_ts} ),
- real_arrival => epoch_to_dt( $in_transit->{real_arr_ts} ),
- arr_ds100 => $in_transit->{arr_ds100},
- arr_eva => $in_transit->{arr_eva},
- arr_name => $in_transit->{arr_name},
- arr_platform => $in_transit->{arr_platform},
- route_after => \@route_after,
- messages => $in_transit->{messages},
- extra_data => $in_transit->{data},
- comment => $in_transit->{user_data}{comment},
- };
-
- my @parsed_messages;
- for my $message ( @{ $ret->{messages} // [] } ) {
- my ( $ts, $msg ) = @{$message};
- push( @parsed_messages, [ epoch_to_dt($ts), $msg ] );
- }
- $ret->{messages} = [ reverse @parsed_messages ];
- @parsed_messages = ();
- for my $message ( @{ $ret->{extra_data}{qos_msg} // [] } ) {
- my ( $ts, $msg ) = @{$message};
- push( @parsed_messages, [ epoch_to_dt($ts), $msg ] );
- }
- $ret->{extra_data}{qos_msg} = [@parsed_messages];
-
- if ( $dep_info and $dep_info->{sched_arr} ) {
- $dep_info->{sched_arr}
- = epoch_to_dt( $dep_info->{sched_arr} );
- $dep_info->{rt_arr} = $dep_info->{sched_arr}->clone;
- if ( $dep_info->{adelay}
- and $dep_info->{adelay} =~ m{^\d+$} )
+ my $stop_after_dep
+ = scalar @{ $ret->{route_after} }
+ ? $ret->{route_after}[0][0]
+ : undef;
+ my $stop_before_dest;
+ for my $i ( 1 .. $#{ $ret->{route_after} } ) {
+ if ( $ret->{arr_name}
+ and $ret->{route_after}[$i][0] eq $ret->{arr_name} )
{
- $dep_info->{rt_arr}
- ->add( minutes => $dep_info->{adelay} );
+ $stop_before_dest = $ret->{route_after}[ $i - 1 ][0];
+ last;
}
- $dep_info->{rt_arr_countdown} = $ret->{boarding_countdown}
- = $dep_info->{rt_arr}->epoch - $epoch;
}
- for my $station (@route_after) {
- if ( @{$station} > 1 ) {
-
- # Note: $station->[1]{sched_arr} may already have been
- # converted to a DateTime object in $station->[1] is
- # $dep_info. This can happen when a station is present
- # several times in a train's route, e.g. for Frankfurt
- # Flughafen in some nightly connections.
- my $times = $station->[1];
- if ( $times->{sched_arr}
- and ref( $times->{sched_arr} ) ne 'DateTime' )
- {
- $times->{sched_arr}
- = epoch_to_dt( $times->{sched_arr} );
- $times->{rt_arr} = $times->{sched_arr}->clone;
- if ( $times->{adelay}
- and $times->{adelay} =~ m{^\d+$} )
- {
- $times->{rt_arr}
- ->add( minutes => $times->{adelay} );
- }
- $times->{rt_arr_countdown}
- = $times->{rt_arr}->epoch - $epoch;
- }
- if ( $times->{sched_dep}
- and ref( $times->{sched_dep} ) ne 'DateTime' )
- {
- $times->{sched_dep}
- = epoch_to_dt( $times->{sched_dep} );
- $times->{rt_dep} = $times->{sched_dep}->clone;
- if ( $times->{ddelay}
- and $times->{ddelay} =~ m{^\d+$} )
- {
- $times->{rt_dep}
- ->add( minutes => $times->{ddelay} );
- }
- $times->{rt_dep_countdown}
- = $times->{rt_dep}->epoch - $epoch;
- }
- }
+ my ($dep_platform_number)
+ = ( ( $ret->{dep_platform} // 0 ) =~ m{(\d+)} );
+ if ( $dep_platform_number
+ and
+ exists $ret->{data}{stationinfo_dep}{$dep_platform_number} )
+ {
+ $ret->{dep_direction} = $self->stationinfo_to_direction(
+ $ret->{data}{stationinfo_dep}{$dep_platform_number},
+ $ret->{data}{wagonorder_dep},
+ undef, $stop_after_dep
+ );
}
- $ret->{departure_countdown}
- = $ret->{real_departure}->epoch - $now->epoch;
+ my ($arr_platform_number)
+ = ( ( $ret->{arr_platform} // 0 ) =~ m{(\d+)} );
+ if ( $arr_platform_number
+ and
+ exists $ret->{data}{stationinfo_arr}{$arr_platform_number} )
+ {
+ $ret->{arr_direction} = $self->stationinfo_to_direction(
+ $ret->{data}{stationinfo_arr}{$arr_platform_number},
+ $ret->{data}{wagonorder_arr},
+ $stop_before_dest,
+ undef
+ );
+ }
if ( $ret->{departure_countdown} > 0
and $in_transit->{data}{wagonorder_dep} )
@@ -2915,7 +1673,6 @@ sub startup {
from_json => $in_transit->{data}{wagonorder_dep} );
};
if ( $wr
- and $wr->sections
and $wr->wagons
and defined $wr->direction )
{
@@ -2923,97 +1680,57 @@ sub startup {
}
}
- if ( $in_transit->{real_arr_ts} ) {
- $ret->{arrival_countdown}
- = $ret->{real_arrival}->epoch - $now->epoch;
- $ret->{journey_duration}
- = $ret->{real_arrival}->epoch
- - $ret->{real_departure}->epoch;
- $ret->{journey_completion}
- = $ret->{journey_duration}
- ? 1
- - ( $ret->{arrival_countdown} / $ret->{journey_duration} )
- : 1;
- if ( $ret->{journey_completion} > 1 ) {
- $ret->{journey_completion} = 1;
- }
- elsif ( $ret->{journey_completion} < 0 ) {
- $ret->{journey_completion} = 0;
- }
-
- my ($dep_platform_number)
- = ( ( $ret->{dep_platform} // 0 ) =~ m{(\d+)} );
- if ( $dep_platform_number
- and exists $in_transit->{data}{stationinfo_dep}
- {$dep_platform_number} )
- {
- $ret->{dep_direction}
- = $self->stationinfo_to_direction(
- $in_transit->{data}{stationinfo_dep}
- {$dep_platform_number},
- $in_transit->{data}{wagonorder_dep},
- undef,
- $stop_after_dep
- );
- }
-
- my ($arr_platform_number)
- = ( ( $ret->{arr_platform} // 0 ) =~ m{(\d+)} );
- if ( $arr_platform_number
- and exists $in_transit->{data}{stationinfo_arr}
- {$arr_platform_number} )
- {
- $ret->{arr_direction}
- = $self->stationinfo_to_direction(
- $in_transit->{data}{stationinfo_arr}
- {$arr_platform_number},
- $in_transit->{data}{wagonorder_arr},
- $stop_before_dest,
- undef
- );
- }
-
- }
- else {
- $ret->{arrival_countdown} = undef;
- $ret->{journey_duration} = undef;
- $ret->{journey_completion} = undef;
- }
-
return $ret;
}
- my $latest = $db->select(
- 'journeys_str',
- '*',
+ my ( $latest, $latest_cancellation ) = $self->journeys->get_latest(
+ uid => $uid,
+ db => $db,
+ );
+
+ if ( $latest_cancellation and $latest_cancellation->{cancelled} ) {
+ if (
+ my $station = $self->stations->get_by_eva(
+ $latest_cancellation->{dep_eva}
+ )
+ )
{
- user_id => $uid,
- cancelled => 0
- },
+ $latest_cancellation->{dep_ds100} = $station->{ds100};
+ $latest_cancellation->{dep_name} = $station->{name};
+ }
+ if (
+ my $station = $self->stations->get_by_eva(
+ $latest_cancellation->{arr_eva}
+ )
+ )
{
- order_by => { -desc => 'journey_id' },
- limit => 1
+ $latest_cancellation->{arr_ds100} = $station->{ds100};
+ $latest_cancellation->{arr_name} = $station->{name};
}
- )->expand->hash;
+ }
+ else {
+ $latest_cancellation = undef;
+ }
if ($latest) {
my $ts = $latest->{checkout_ts};
my $action_time = epoch_to_dt($ts);
if ( my $station
- = $self->app->station_by_eva->{ $latest->{dep_eva} } )
+ = $self->stations->get_by_eva( $latest->{dep_eva} ) )
{
- $latest->{dep_ds100} = $station->[0];
- $latest->{dep_name} = $station->[1];
+ $latest->{dep_ds100} = $station->{ds100};
+ $latest->{dep_name} = $station->{name};
}
if ( my $station
- = $self->app->station_by_eva->{ $latest->{arr_eva} } )
+ = $self->stations->get_by_eva( $latest->{arr_eva} ) )
{
- $latest->{arr_ds100} = $station->[0];
- $latest->{arr_name} = $station->[1];
+ $latest->{arr_ds100} = $station->{ds100};
+ $latest->{arr_name} = $station->{name};
}
return {
checked_in => 0,
cancelled => 0,
+ cancellation => $latest_cancellation,
journey_id => $latest->{journey_id},
timestamp => $action_time,
timestamp_delta => $now->epoch - $action_time->epoch,
@@ -3026,20 +1743,30 @@ sub startup {
dep_ds100 => $latest->{dep_ds100},
dep_eva => $latest->{dep_eva},
dep_name => $latest->{dep_name},
+ dep_lat => $latest->{dep_lat},
+ dep_lon => $latest->{dep_lon},
dep_platform => $latest->{dep_platform},
sched_arrival => epoch_to_dt( $latest->{sched_arr_ts} ),
real_arrival => epoch_to_dt( $latest->{real_arr_ts} ),
arr_ds100 => $latest->{arr_ds100},
arr_eva => $latest->{arr_eva},
arr_name => $latest->{arr_name},
+ arr_lat => $latest->{arr_lat},
+ arr_lon => $latest->{arr_lon},
arr_platform => $latest->{arr_platform},
comment => $latest->{user_data}{comment},
+ visibility => $latest->{visibility},
+ visibility_str => $latest->{visibility_str},
+ effective_visibility => $latest->{effective_visibility},
+ effective_visibility_str =>
+ $latest->{effective_visibility_str},
};
}
return {
checked_in => 0,
cancelled => 0,
+ cancellation => $latest_cancellation,
no_journeys_yet => 1,
timestamp => epoch_to_dt(0),
timestamp_delta => $now->epoch,
@@ -3049,10 +1776,11 @@ sub startup {
$self->helper(
'get_user_status_json_v1' => sub {
- my ( $self, $uid ) = @_;
- my $status = $self->get_user_status($uid);
-
- # TODO simplify lon/lat (can be returned from get_user_status)
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $privacy = $opt{privacy}
+ // $self->users->get_privacy_by( uid => $uid );
+ my $status = $opt{status} // $self->get_user_status($uid);
my $ret = {
deprecated => \0,
@@ -3060,57 +1788,82 @@ sub startup {
$status->{checked_in}
or $status->{cancelled}
) ? \1 : \0,
+ comment => $status->{comment},
fromStation => {
ds100 => $status->{dep_ds100},
name => $status->{dep_name},
uic => $status->{dep_eva},
- longitude => undef,
- latitude => undef,
- scheduledTime => $status->{sched_departure}->epoch || undef,
- realTime => $status->{real_departure}->epoch || undef,
+ longitude => $status->{dep_lon},
+ latitude => $status->{dep_lat},
+ scheduledTime => $status->{sched_departure}
+ ? $status->{sched_departure}->epoch
+ : undef,
+ realTime => $status->{real_departure}
+ ? $status->{real_departure}->epoch
+ : undef,
},
toStation => {
ds100 => $status->{arr_ds100},
name => $status->{arr_name},
uic => $status->{arr_eva},
- longitude => undef,
- latitude => undef,
- scheduledTime => $status->{sched_arrival}->epoch || undef,
- realTime => $status->{real_arrival}->epoch || undef,
+ longitude => $status->{arr_lon},
+ latitude => $status->{arr_lat},
+ scheduledTime => $status->{sched_arrival}
+ ? $status->{sched_arrival}->epoch
+ : undef,
+ realTime => $status->{real_arrival}
+ ? $status->{real_arrival}->epoch
+ : undef,
},
train => {
- type => $status->{train_type},
- line => $status->{train_line},
- no => $status->{train_no},
- id => $status->{train_id},
+ type => $status->{train_type},
+ line => $status->{train_line},
+ no => $status->{train_no},
+ id => $status->{train_id},
+ hafasId => $status->{extra_data}{trip_id},
},
- actionTime => $status->{timestamp}->epoch,
+ intermediateStops => [],
+ visibility => {
+ level => $status->{effective_visibility},
+ desc => $status->{effective_visibility_str},
+ }
};
- if ( $status->{dep_eva} ) {
- my @station_descriptions
- = Travel::Status::DE::IRIS::Stations::get_station(
- $status->{dep_eva} );
- if ( @station_descriptions == 1 ) {
- (
- undef, undef, undef,
- $ret->{fromStation}{longitude},
- $ret->{fromStation}{latitude}
- ) = @{ $station_descriptions[0] };
+ if ( $opt{public} ) {
+ if ( not $privacy->{comments_visible} ) {
+ delete $ret->{comment};
}
}
+ else {
+ $ret->{actionTime}
+ = $status->{timestamp}
+ ? $status->{timestamp}->epoch
+ : undef;
+ }
- if ( $status->{arr_ds100} ) {
- my @station_descriptions
- = Travel::Status::DE::IRIS::Stations::get_station(
- $status->{arr_ds100} );
- if ( @station_descriptions == 1 ) {
- (
- undef, undef, undef,
- $ret->{toStation}{longitude},
- $ret->{toStation}{latitude}
- ) = @{ $station_descriptions[0] };
+ for my $stop ( @{ $status->{route_after} // [] } ) {
+ if ( $status->{arr_name} and $stop->[0] eq $status->{arr_name} )
+ {
+ last;
}
+ push(
+ @{ $ret->{intermediateStops} },
+ {
+ name => $stop->[0],
+ scheduledArrival => $stop->[2]{sched_arr}
+ ? $stop->[2]{sched_arr}->epoch
+ : undef,
+ realArrival => $stop->[2]{rt_arr}
+ ? $stop->[2]{rt_arr}->epoch
+ : undef,
+ scheduledDeparture => $stop->[2]{sched_dep}
+ ? $stop->[2]{sched_dep}->epoch
+ : undef,
+ realDeparture => $stop->[2]{rt_dep}
+ ? $stop->[2]{rt_dep}->epoch
+ : undef,
+ }
+ );
}
return $ret;
@@ -3118,140 +1871,464 @@ sub startup {
);
$self->helper(
- 'get_travel_distance' => sub {
- my ( $self, $from, $to, $route_ref ) = @_;
-
- my $distance_intermediate = 0;
- my $distance_beeline = 0;
- my $skipped = 0;
- my $geo = Geo::Distance->new();
- my @stations = map { $_->[0] } @{$route_ref};
- my @route = after_incl { $_ eq $from } @stations;
- @route = before_incl { $_ eq $to } @route;
+ 'traewelling_to_travelynx_p' => sub {
+ my ( $self, %opt ) = @_;
+ my $traewelling = $opt{traewelling};
+ my $user_data = $opt{user_data};
+ my $uid = $user_data->{user_id};
- if ( @route < 2 ) {
+ my $promise = Mojo::Promise->new;
- # I AM ERROR
- return ( 0, 0 );
+ if ( not $traewelling->{checkin}
+ or $self->now->epoch - $traewelling->{checkin}->epoch > 900 )
+ {
+ $self->log->debug("... not checked in");
+ return $promise->resolve;
}
-
- my $prev_station = get_station( shift @route );
- if ( not $prev_station ) {
- return ( 0, 0 );
+ if ( $traewelling->{status_id}
+ and $user_data->{data}{latest_pull_status_id}
+ and $traewelling->{status_id}
+ == $user_data->{data}{latest_pull_status_id} )
+ {
+ $self->log->debug("... already handled");
+ return $promise->resolve;
+ }
+ $self->log->debug(
+"... checked in : $traewelling->{dep_name} $traewelling->{dep_eva} -> $traewelling->{arr_name} $traewelling->{arr_eva}"
+ );
+ my $user_status = $self->get_user_status($uid);
+ if ( $user_status->{checked_in} ) {
+ $self->log->debug(
+ "... also checked in via travelynx. aborting.");
+ return $promise->resolve;
}
- # Geo-coordinates for stations outside Germany are not available
- # at the moment. When calculating distance with intermediate stops,
- # these are simply left out (as if they were not part of the route).
- # For beeline distance calculation, we use the route's first and last
- # station with known geo-coordinates.
- my $from_station_beeline;
- my $to_station_beeline;
-
- for my $station_name (@route) {
- if ( my $station = get_station($station_name) ) {
- if ( not $from_station_beeline and $#{$prev_station} >= 4 )
- {
- $from_station_beeline = $prev_station;
- }
- if ( $#{$station} >= 4 ) {
- $to_station_beeline = $station;
+ if ( $traewelling->{category}
+ !~ m{^ (?: national .* | regional .* | suburban ) $ }x )
+ {
+
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+
+ $self->checkin_p(
+ station => $traewelling->{dep_eva},
+ train_id => $traewelling->{trip_id},
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
+ )->then(
+ sub {
+ $self->log->debug("... handled origin");
+ return $self->checkout_p(
+ station => $traewelling->{arr_eva},
+ train_id => $traewelling->{trip_id},
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
+ );
}
- if ( $#{$prev_station} >= 4 and $#{$station} >= 4 ) {
- $distance_intermediate
- += $geo->distance( 'kilometer', $prev_station->[3],
- $prev_station->[4], $station->[3], $station->[4] );
+ )->then(
+ sub {
+ my ( undef, $err ) = @_;
+ if ($err) {
+ $self->log->debug("... error: $err");
+ return Mojo::Promise->reject($err);
+ }
+ $self->log->debug("... handled destination");
+ if ( $traewelling->{message} ) {
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ db => $db,
+ user_data =>
+ { comment => $traewelling->{message} }
+ );
+ }
+ $self->traewelling->log(
+ uid => $uid,
+ db => $db,
+ message =>
+"Eingecheckt in $traewelling->{line} nach $traewelling->{arr_name}",
+ status_id => $traewelling->{status_id},
+ );
+ $self->traewelling->set_latest_pull_status_id(
+ uid => $uid,
+ status_id => $traewelling->{status_id},
+ db => $db
+ );
+
+ $tx->commit;
+ $promise->resolve;
+ return;
}
- else {
- $skipped++;
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->log->debug("... error: $err");
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $err",
+ status_id => $traewelling->{status_id},
+ is_error => 1
+ );
+ $promise->resolve;
+ return;
}
- $prev_station = $station;
- }
+ )->wait;
+ return $promise;
}
- if ( $from_station_beeline and $to_station_beeline ) {
- $distance_beeline = $geo->distance(
- 'kilometer', $from_station_beeline->[3],
- $from_station_beeline->[4], $to_station_beeline->[3],
- $to_station_beeline->[4]
- );
- }
+ $self->iris->get_departures_p(
+ station => $traewelling->{dep_eva},
+ lookbehind => 60,
+ lookahead => 40
+ )->then(
+ sub {
+ my ($dep) = @_;
+ my ( $train_ref, $train_id );
+
+ if ( $dep->{errstr} ) {
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $dep->{errstr}",
+ status_id => $traewelling->{status_id},
+ is_error => 1,
+ );
+ $promise->resolve;
+ return;
+ }
+
+ for my $train ( @{ $dep->{results} } ) {
+ if ( $train->line ne $traewelling->{line} ) {
+ next;
+ }
+ if ( not $train->sched_departure
+ or $train->sched_departure->epoch
+ != $traewelling->{dep_dt}->epoch )
+ {
+ next;
+ }
+ if (
+ not
+ List::Util::first { $_ eq $traewelling->{arr_name} }
+ $train->route_post
+ )
+ {
+ next;
+ }
+ $train_id = $train->train_id;
+ $train_ref = $train;
+ last;
+ }
+
+ if ( not $train_id ) {
+ $self->log->debug(
+ "... train $traewelling->{line} not found");
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: Zug nicht gefunden",
+ status_id => $traewelling->{status_id},
+ is_error => 1
+ );
+ return $promise->resolve;
+ }
- return ( $distance_intermediate, $distance_beeline, $skipped );
+ $self->log->debug("... found train: $train_id");
+
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+
+ $self->checkin_p(
+ station => $traewelling->{dep_eva},
+ train_id => $train_id,
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
+ )->then(
+ sub {
+ $self->log->debug("... handled origin");
+ return $self->checkout_p(
+ station => $traewelling->{arr_eva},
+ train_id => 0,
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
+ );
+ }
+ )->then(
+ sub {
+ my ( undef, $err ) = @_;
+ if ($err) {
+ $self->log->debug("... error: $err");
+ return Mojo::Promise->reject($err);
+ }
+ $self->log->debug("... handled destination");
+ if ( $traewelling->{message} ) {
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ db => $db,
+ user_data =>
+ { comment => $traewelling->{message} }
+ );
+ }
+ $self->traewelling->log(
+ uid => $uid,
+ db => $db,
+ message =>
+"Eingecheckt in $traewelling->{line} nach $traewelling->{arr_name}",
+ status_id => $traewelling->{status_id},
+ );
+ $self->traewelling->set_latest_pull_status_id(
+ uid => $uid,
+ status_id => $traewelling->{status_id},
+ db => $db
+ );
+
+ $tx->commit;
+ $promise->resolve;
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->log->debug("... error: $err");
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $err",
+ status_id => $traewelling->{status_id},
+ is_error => 1
+ );
+ $promise->resolve;
+ return;
+ }
+ )->wait;
+ }
+ )->catch(
+ sub {
+ my ( $err, $dep ) = @_;
+ $self->traewelling->log(
+ uid => $uid,
+ message =>
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $dep->{errstr}",
+ status_id => $traewelling->{status_id},
+ is_error => 1,
+ );
+ $promise->resolve;
+ return;
+ }
+ )->wait;
+
+ return $promise;
}
);
$self->helper(
- 'compute_journey_stats' => sub {
- my ( $self, @journeys ) = @_;
- my $km_route = 0;
- my $km_beeline = 0;
- my $min_travel_sched = 0;
- my $min_travel_real = 0;
- my $delay_dep = 0;
- my $delay_arr = 0;
- my $interchange_real = 0;
- my $num_trains = 0;
- my $num_journeys = 0;
- my @inconsistencies;
-
- my $next_departure = 0;
-
- for my $journey (@journeys) {
- $num_trains++;
- $km_route += $journey->{km_route};
- $km_beeline += $journey->{km_beeline};
- if ( $journey->{sched_duration}
- and $journey->{sched_duration} > 0 )
+ 'journeys_to_map_data' => sub {
+ my ( $self, %opt ) = @_;
+
+ my @journeys = @{ $opt{journeys} // [] };
+ my $route_type = $opt{route_type} // 'polybee';
+ my $include_manual = $opt{include_manual} ? 1 : 0;
+
+ my $location = $self->app->coordinates_by_station;
+
+ my $with_polyline = $route_type eq 'beeline' ? 0 : 1;
+
+ if ( not @journeys ) {
+ return {
+ skipped_journeys => [],
+ station_coordinates => [],
+ polyline_groups => [],
+ };
+ }
+
+ my $json = JSON->new->utf8;
+
+ my $first_departure = $journeys[-1]->{rt_departure};
+ my $last_departure = $journeys[0]->{rt_departure};
+
+ my @stations = List::Util::uniq map { $_->{to_name} } @journeys;
+ push( @stations,
+ List::Util::uniq map { $_->{from_name} } @journeys );
+ @stations = List::Util::uniq @stations;
+ my @station_coordinates = map { [ $location->{$_}, $_ ] }
+ grep { exists $location->{$_} } @stations;
+
+ my @station_pairs;
+ my @polylines;
+ my %seen;
+
+ my @skipped_journeys;
+ my @polyline_journeys = grep { $_->{polyline} } @journeys;
+ my @beeline_journeys = grep { not $_->{polyline} } @journeys;
+
+ if ( $route_type eq 'polyline' ) {
+ @beeline_journeys = ();
+ }
+ elsif ( $route_type eq 'beeline' ) {
+ push( @beeline_journeys, @polyline_journeys );
+ @polyline_journeys = ();
+ }
+
+ for my $journey (@polyline_journeys) {
+ my @polyline = @{ $journey->{polyline} };
+ my $from_eva = $journey->{from_eva};
+ my $to_eva = $journey->{to_eva};
+
+ my $from_index
+ = first_index { $_->[2] and $_->[2] == $from_eva } @polyline;
+ my $to_index
+ = first_index { $_->[2] and $_->[2] == $to_eva } @polyline;
+
+ if ( $from_index == -1
+ or $to_index == -1 )
{
- $min_travel_sched += $journey->{sched_duration} / 60;
- }
- if ( $journey->{rt_duration} and $journey->{rt_duration} > 0 ) {
- $min_travel_real += $journey->{rt_duration} / 60;
+ # Fall back to route
+ delete $journey->{polyline};
+ next;
}
- if ( $journey->{sched_dep_ts} and $journey->{rt_dep_ts} ) {
- $delay_dep
- += ( $journey->{rt_dep_ts} - $journey->{sched_dep_ts} )
- / 60;
+
+ my $key
+ = $from_eva . '!'
+ . $to_eva . '!'
+ . ( $to_index - $from_index );
+
+ if ( $seen{$key} ) {
+ next;
}
- if ( $journey->{sched_arr_ts} and $journey->{rt_arr_ts} ) {
- $delay_arr
- += ( $journey->{rt_arr_ts} - $journey->{sched_arr_ts} )
- / 60;
+
+ $seen{$key} = 1;
+
+ # direction does not matter at the moment
+ $key
+ = $to_eva . '!'
+ . $from_eva . '!'
+ . ( $to_index - $from_index );
+ $seen{$key} = 1;
+
+ @polyline = @polyline[ $from_index .. $to_index ];
+ my @polyline_coords;
+ for my $coord (@polyline) {
+ push( @polyline_coords, [ $coord->[1], $coord->[0] ] );
}
+ push( @polylines, [@polyline_coords] );
+ }
- # Note that journeys are sorted from recent to older entries
- if ( $journey->{rt_arr_ts}
- and $next_departure
- and $next_departure - $journey->{rt_arr_ts} < ( 60 * 60 ) )
- {
- if ( $next_departure - $journey->{rt_arr_ts} < 0 ) {
- push( @inconsistencies,
- epoch_to_dt($next_departure)
- ->strftime('%d.%m.%Y %H:%M') );
+ for my $journey (@beeline_journeys) {
+
+ my @route = map { $_->[0] } @{ $journey->{route} };
+
+ my $from_index
+ = first_index { $_ eq $journey->{from_name} } @route;
+ my $to_index = first_index { $_ eq $journey->{to_name} } @route;
+
+ if ( $from_index == -1 ) {
+ my $rename = $self->app->renamed_station;
+ $from_index = first_index {
+ ( $rename->{$_} // $_ ) eq $journey->{from_name}
}
- else {
- $interchange_real
- += ( $next_departure - $journey->{rt_arr_ts} ) / 60;
+ @route;
+ }
+ if ( $to_index == -1 ) {
+ my $rename = $self->app->renamed_station;
+ $to_index = first_index {
+ ( $rename->{$_} // $_ ) eq $journey->{to_name}
}
+ @route;
+ }
+
+ if ( $from_index == -1
+ or $to_index == -1 )
+ {
+ push( @skipped_journeys,
+ [ $journey, 'Start/Ziel nicht in Route gefunden' ] );
+ next;
+ }
+
+ # Manual journey entries are only included if one of the following
+ # conditions is satisfied:
+ # * their route has more than two elements (-> probably more than just
+ # start and stop station), or
+ # * $include_manual is true (-> user wants to see incomplete routes)
+ # This avoids messing up the map in case an A -> B connection has been
+ # tracked both with a regular checkin (-> detailed route shown on map)
+ # and entered manually (-> beeline also shown on map, typically
+ # significantly differs from detailed route) -- unless the user
+ # sets include_manual, of course.
+ if ( $journey->{edited} & 0x0010
+ and @route <= 2
+ and not $include_manual )
+ {
+ push( @skipped_journeys,
+ [ $journey, 'Manueller Eintrag ohne Unterwegshalte' ] );
+ next;
+ }
+
+ @route = @route[ $from_index .. $to_index ];
+
+ my $key = join( '|', @route );
+
+ if ( $seen{$key} ) {
+ next;
}
- else {
- $num_journeys++;
+
+ $seen{$key} = 1;
+
+ # direction does not matter at the moment
+ $seen{ join( '|', reverse @route ) } = 1;
+
+ my $prev_station = shift @route;
+ for my $station (@route) {
+ push( @station_pairs, [ $prev_station, $station ] );
+ $prev_station = $station;
}
- $next_departure = $journey->{rt_dep_ts};
}
- return {
- km_route => $km_route,
- km_beeline => $km_beeline,
- num_trains => $num_trains,
- num_journeys => $num_journeys,
- min_travel_sched => $min_travel_sched,
- min_travel_real => $min_travel_real,
- min_interchange_real => $interchange_real,
- delay_dep => $delay_dep,
- delay_arr => $delay_arr,
- inconsistencies => \@inconsistencies,
+
+ @station_pairs = uniq_by { $_->[0] . '|' . $_->[1] } @station_pairs;
+ @station_pairs = grep {
+ exists $location->{ $_->[0] }
+ and exists $location->{ $_->[1] }
+ } @station_pairs;
+ @station_pairs
+ = map { [ $location->{ $_->[0] }, $location->{ $_->[1] } ] }
+ @station_pairs;
+
+ my $ret = {
+ skipped_journeys => \@skipped_journeys,
+ station_coordinates => \@station_coordinates,
+ polyline_groups => [
+ {
+ polylines => $json->encode( \@station_pairs ),
+ color => '#673ab7',
+ opacity => @polylines
+ ? $with_polyline
+ ? 0.4
+ : 0.6
+ : 0.8,
+ },
+ {
+ polylines => $json->encode( \@polylines ),
+ color => '#673ab7',
+ opacity => 0.8,
+ }
+ ],
};
+
+ if (@station_coordinates) {
+ my @lats = map { $_->[0][0] } @station_coordinates;
+ my @lons = map { $_->[0][1] } @station_coordinates;
+ my $min_lat = List::Util::min @lats;
+ my $max_lat = List::Util::max @lats;
+ my $min_lon = List::Util::min @lons;
+ my $max_lon = List::Util::max @lons;
+ $ret->{bounds}
+ = [ [ $min_lat, $min_lon ], [ $max_lat, $max_lon ] ];
+ }
+
+ return $ret;
}
);
@@ -3274,71 +2351,108 @@ sub startup {
$r->get('/changelog')->to('static#changelog');
$r->get('/impressum')->to('static#imprint');
$r->get('/imprint')->to('static#imprint');
- $r->get('/offline')->to('static#offline');
+ $r->get('/legend')->to('static#legend');
+ $r->get('/offline.html')->to('static#offline');
$r->get('/api/v1/:user_action/:token')->to('api#get_v1');
$r->get('/login')->to('account#login_form');
$r->get('/recover')->to('account#request_password_reset');
$r->get('/recover/:id/:token')->to('account#recover_password');
- $r->get('/register')->to('account#registration_form');
$r->get('/reg/:id/:token')->to('account#verify');
- $r->get('/status/:name')->to('traveling#user_status');
- $r->get('/status/:name/:ts')->to('traveling#user_status');
- $r->get('/ajax/status/:name')->to('traveling#public_status_card');
- $r->get('/ajax/status/:name/:ts')->to('traveling#public_status_card');
+ $r->get( '/status/:name' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'profile#user_status', format => undef );
+ $r->get( '/status/:name/:ts' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'profile#user_status', format => undef );
+ $r->get('/ajax/status/#name')->to('profile#status_card');
+ $r->get('/ajax/status/:name/:ts')->to('profile#status_card');
+ $r->get('/p/:name')->to('profile#profile');
+ $r->get( '/p/:name/j/:id' => 'public_journey' )
+ ->to('profile#journey_details');
+ $r->get('/.well-known/webfinger')->to('account#webfinger');
+ $r->get('/dyn/:av/autocomplete.js')->to('api#autocomplete');
$r->post('/api/v1/import')->to('api#import_v1');
$r->post('/api/v1/travel')->to('api#travel_v1');
- $r->post('/action')->to('traveling#log_action');
+ $r->post('/action')->to('traveling#travel_action');
$r->post('/geolocation')->to('traveling#geolocation');
$r->post('/list_departures')->to('traveling#redirect_to_station');
$r->post('/login')->to('account#do_login');
- $r->post('/register')->to('account#register');
$r->post('/recover')->to('account#request_password_reset');
+ if ( $self->config->{traewelling}{oauth} ) {
+ $r->get('/oauth/traewelling')->to('traewelling#oauth');
+ $r->post('/oauth/traewelling')->to('traewelling#oauth');
+ }
+
+ if ( not $self->config->{registration}{disabled} ) {
+ $r->get('/register')->to('account#registration_form');
+ $r->post('/register')->to('account#register');
+ }
+
my $authed_r = $r->under(
sub {
my ($self) = @_;
if ( $self->is_user_authenticated ) {
return 1;
}
- $self->render( 'login', redirect_to => $self->req->url );
+ $self->render(
+ 'login',
+ redirect_to => $self->req->url,
+ from => 'auth_required'
+ );
return undef;
}
);
$authed_r->get('/account')->to('account#account');
$authed_r->get('/account/privacy')->to('account#privacy');
+ $authed_r->get('/account/social')->to('account#social');
+ $authed_r->get('/account/social/:kind')->to('account#social_list');
+ $authed_r->get('/account/profile')->to('account#profile');
$authed_r->get('/account/hooks')->to('account#webhook');
+ $authed_r->get('/account/traewelling')->to('traewelling#settings');
$authed_r->get('/account/insight')->to('account#insight');
+ $authed_r->get('/account/services')->to('account#services');
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
$authed_r->get('/cancelled')->to('traveling#cancelled');
$authed_r->get('/fgr')->to('passengerrights#list_candidates');
$authed_r->get('/account/password')->to('account#password_form');
$authed_r->get('/account/mail')->to('account#change_mail');
+ $authed_r->get('/account/name')->to('account#change_name');
$authed_r->get('/export.json')->to('account#json_export');
$authed_r->get('/history.json')->to('traveling#json_history');
+ $authed_r->get('/history.csv')->to('traveling#csv_history');
$authed_r->get('/history')->to('traveling#history');
+ $authed_r->get('/history/commute')->to('traveling#commute');
$authed_r->get('/history/map')->to('traveling#map_history');
$authed_r->get('/history/:year')->to('traveling#yearly_history');
+ $authed_r->get('/history/:year/review')->to('traveling#year_in_review');
$authed_r->get('/history/:year/:month')->to('traveling#monthly_history');
$authed_r->get('/journey/add')->to('traveling#add_journey_form');
$authed_r->get('/journey/comment')->to('traveling#comment_form');
+ $authed_r->get('/journey/visibility')->to('traveling#visibility_form');
$authed_r->get('/journey/:id')->to('traveling#journey_details');
$authed_r->get('/s/*station')->to('traveling#station');
$authed_r->get('/confirm_mail/:token')->to('account#confirm_mail');
$authed_r->post('/account/privacy')->to('account#privacy');
+ $authed_r->post('/account/social')->to('account#social');
+ $authed_r->post('/account/profile')->to('account#profile');
$authed_r->post('/account/hooks')->to('account#webhook');
+ $authed_r->post('/account/traewelling')->to('traewelling#settings');
$authed_r->post('/account/insight')->to('account#insight');
- $authed_r->post('/history/map')->to('traveling#map_history');
+ $authed_r->post('/account/services')->to('account#services');
$authed_r->post('/journey/add')->to('traveling#add_journey_form');
$authed_r->post('/journey/comment')->to('traveling#comment_form');
+ $authed_r->post('/journey/visibility')->to('traveling#visibility_form');
$authed_r->post('/journey/edit')->to('traveling#edit_journey');
$authed_r->post('/journey/passenger_rights/*filename')
->to('passengerrights#generate');
$authed_r->post('/account/password')->to('account#change_password');
$authed_r->post('/account/mail')->to('account#change_mail');
+ $authed_r->post('/account/name')->to('account#change_name');
+ $authed_r->post('/social-action')->to('account#social_action');
$authed_r->post('/delete')->to('account#delete');
$authed_r->post('/logout')->to('account#do_logout');
$authed_r->post('/set_token')->to('api#set_token');
+ $authed_r->get('/timeline/in-transit')->to('profile#checked_in');
}