diff options
Diffstat (limited to 'lib/Travelynx')
28 files changed, 5594 insertions, 1026 deletions
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index d13b2a7..95d67f5 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -1,14 +1,19 @@ package Travelynx::Command::database; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Command'; use DateTime; use File::Slurp qw(read_file); +use List::Util qw(); use JSON; +use Travel::Status::DE::EFA; +use Travel::Status::DE::HAFAS; use Travel::Status::DE::IRIS::Stations; +use Travel::Status::MOTIS; has description => 'Initialize or upgrade database layout'; @@ -1918,7 +1923,7 @@ my @migrations = ( # v49 -> v50 # travelynx 2.0 introduced proper HAFAS support, so there is no need for - # the 'FYI, here is some hAFAS data' kludge anymore. + # the 'FYI, here is some HAFAS data' kludge anymore. sub { my ($db) = @_; $db->query( @@ -1946,6 +1951,1263 @@ my @migrations = ( } ); }, + + # v51 -> v52 + # Explicitly encode backend type; preparation for multiple HAFAS backends + sub { + my ($db) = @_; + $db->query( + qq{ + create table backends ( + id smallserial not null primary key, + iris bool not null, + hafas bool not null, + efa bool not null, + ris bool not null, + name varchar(32) not null, + unique (iris, hafas, efa, ris, name) + ); + insert into backends (id, iris, hafas, efa, ris, name) values (0, true, false, false, false, ''); + insert into backends (id, iris, hafas, efa, ris, name) values (1, false, true, false, false, 'DB'); + alter sequence backends_id_seq restart with 2; + alter table in_transit add column backend_id smallint references backends (id); + alter table journeys add column backend_id smallint references backends (id); + update in_transit set backend_id = 0 where train_id not like '%|%'; + update journeys set backend_id = 0 where train_id not like '%|%'; + update in_transit set backend_id = 1 where train_id like '%|%'; + update journeys set backend_id = 1 where train_id like '%|%'; + update journeys set backend_id = 1 where train_id = 'manual'; + alter table in_transit alter column backend_id set not null; + alter table journeys alter column backend_id set not null; + + drop view in_transit_str; + drop view journeys_str; + create view in_transit_str as select + user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.ris as is_ris, + backend.name as backend_name, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva + left join stations as arr_station on checkout_station_id = arr_station.eva + left join backends as backend on backend_id = backend.id + ; + create view journeys_str as select + journeys.id as journey_id, user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.ris as is_ris, + backend.name as backend_name, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, edited, route, messages, user_data, + dep_platform, arr_platform + from journeys + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva + left join stations as arr_station on checkout_station_id = arr_station.eva + left join backends as backend on backend_id = backend.id + ; + update schema_version set version = 52; + } + ); + }, + + # v52 -> v53 + # Extend train_id to be compatible with more recent HAFAS versions + sub { + my ($db) = @_; + $db->query( + qq{ + drop view in_transit_str; + drop view journeys_str; + drop view follows_in_transit; + alter table in_transit alter column train_id type varchar(384); + alter table journeys alter column train_id type varchar(384); + create view in_transit_str as select + user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.ris as is_ris, + backend.name as backend_name, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva + left join stations as arr_station on checkout_station_id = arr_station.eva + left join backends as backend on backend_id = backend.id + ; + create view journeys_str as select + journeys.id as journey_id, user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.ris as is_ris, + backend.name as backend_name, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, edited, route, messages, user_data, + dep_platform, arr_platform + from journeys + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva + left join stations as arr_station on checkout_station_id = arr_station.eva + left join backends as backend on backend_id = backend.id + ; + create view follows_in_transit as select + r1.subject_id as follower_id, user_id as followee_id, + users.name as followee_name, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva + left join stations as arr_station on checkout_station_id = arr_station.eva + order by checkin_time desc + ; + update schema_version set version = 53; + } + ); + }, + + # v53 -> v54 + # Retrofit lat/lon data onto routes logged before v2.7.8; ensure + # consistent name and eva entries as well. + sub { + my ($db) = @_; + + say +'Adding lat/lon to routes of journeys logged before v2.7.8 and improving consistency of name/eva data in very old route entries.'; + say 'This may take a while ...'; + + my %legacy_to_new; + if ( -r 'share/old_station_names.json' ) { + %legacy_to_new = %{ JSON->new->utf8->decode( + scalar read_file('share/old_station_names.json') + ) + }; + } + + my %latlon_by_eva; + my %latlon_by_name; + my $res = $db->select( 'stations', [ 'name', 'eva', 'lat', 'lon' ] ); + while ( my $row = $res->hash ) { + $latlon_by_eva{ $row->{eva} } = $row; + $latlon_by_name{ $row->{name} } = $row; + } + + my $total + = $db->select( 'journeys', 'count(*) as count' )->hash->{count}; + my $count = 0; + my $total_no_eva = 0; + my $total_no_latlon = 0; + + my $json = JSON->new; + + $res = $db->select( 'journeys_str', [ 'route', 'journey_id' ] ); + while ( my $row = $res->expand->hash ) { + my $no_eva = 0; + my $no_latlon = 0; + my $changed = 0; + my @route = @{ $row->{route} }; + for my $stop (@route) { + my $name = $stop->[0]; + my $eva = $stop->[1]; + + if ( not $eva and $stop->[2]{eva} ) { + $eva = $stop->[1] = 0 + $stop->[2]{eva}; + } + + if ( $stop->[2]{eva} and $eva and $eva == $stop->[2]{eva} ) { + delete $stop->[2]{eva}; + } + + if ( $stop->[2]{name} and $name eq $stop->[2]{name} ) { + delete $stop->[2]{name}; + } + + if ( not $eva ) { + if ( $latlon_by_name{$name} ) { + $eva = $stop->[1] = $latlon_by_name{$name}{eva}; + $changed = 1; + } + elsif ( $legacy_to_new{$name} + and $latlon_by_name{ $legacy_to_new{$name} } ) + { + $eva = $stop->[1] + = $latlon_by_name{ $legacy_to_new{$name} }{eva}; + $stop->[2]{lat} + = $latlon_by_name{ $legacy_to_new{$name} }{lat}; + $stop->[2]{lon} + = $latlon_by_name{ $legacy_to_new{$name} }{lon}; + $changed = 1; + } + else { + $no_eva = 1; + } + } + + if ( $stop->[2]{lat} and $stop->[2]{lon} ) { + next; + } + + if ( $eva and $latlon_by_eva{$eva} ) { + $stop->[2]{lat} = $latlon_by_eva{$eva}{lat}; + $stop->[2]{lon} = $latlon_by_eva{$eva}{lon}; + $changed = 1; + } + elsif ( $latlon_by_name{$name} ) { + $stop->[2]{lat} = $latlon_by_name{$name}{lat}; + $stop->[2]{lon} = $latlon_by_name{$name}{lon}; + $changed = 1; + } + elsif ( $legacy_to_new{$name} + and $latlon_by_name{ $legacy_to_new{$name} } ) + { + $stop->[2]{lat} + = $latlon_by_name{ $legacy_to_new{$name} }{lat}; + $stop->[2]{lon} + = $latlon_by_name{ $legacy_to_new{$name} }{lon}; + $changed = 1; + } + else { + $no_latlon = 1; + } + } + if ($no_eva) { + $total_no_eva += 1; + } + if ($no_latlon) { + $total_no_latlon += 1; + } + if ($changed) { + $db->update( + 'journeys', + { + route => $json->encode( \@route ), + }, + { id => $row->{journey_id} } + ); + } + if ( $count++ % 10000 == 0 ) { + printf( " %2.0f%% complete\n", $count * 100 / $total ); + } + } + say ' done'; + if ($total_no_eva) { + printf( " (%d of %d routes still lack some EVA IDs)\n", + $total_no_eva, $total ); + } + if ($total_no_latlon) { + printf( " (%d of %d routes still lack some lat/lon data)\n", + $total_no_latlon, $total ); + } + + $db->query( + qq{ + update schema_version set version = 54; + } + ); + }, + + # v54 -> v55 + # do not share stations between backends + sub { + my ($db) = @_; + $db->query( + qq{ + alter table schema_version add column hafas varchar(12); + alter table users drop column external_services; + alter table users add column backend_id smallint references backends (id) default 1; + alter table stations drop constraint stations_pkey; + alter table stations add unique (eva, source); + create index eva_by_source on stations (eva, source); + create index eva on stations (eva); + alter table related_stations drop constraint related_stations_eva_meta_key; + drop index rel_eva; + alter table related_stations add column backend_id smallint; + update related_stations set backend_id = 1; + alter table related_stations alter column backend_id set not null; + alter table related_stations add constraint backend_fk foreign key (backend_id) references backends (id); + alter table related_stations add unique (eva, meta, backend_id); + create index related_stations_eva_backend_key on related_stations (eva, backend_id); + } + ); + + # up until now, IRIS and DB HAFAS shared stations, with IRIS taking + # preference. As of v2.7, this is no longer the case. However, old DB + # HAFAS journeys may still reference IRIS-specific stations. So, we + # make all IRIS stations available as DB HAFAS stations as well. + my $total + = $db->select( 'stations', 'count(*) as count', { source => 0 } ) + ->hash->{count}; + my $count = 0; + + # Caveat: If this is a fresh installation, there are no IRIS stations + # in the database yet. So we have to populate it first. + if ( not $total ) { + say +'Preparing to untangle IRIS / HAFAS stations, this may take a while ...'; + $total = scalar Travel::Status::DE::IRIS::Stations::get_stations(); + for my $s ( Travel::Status::DE::IRIS::Stations::get_stations() ) { + my ( $ds100, $name, $eva, $lon, $lat ) = @{$s}; + if ( $ENV{__TRAVELYNX_TEST_MINI_IRIS} + and ( $eva < 8000000 or $eva > 8000100 ) ) + { + next; + } + $db->insert( + 'stations', + { + eva => $eva, + ds100 => $ds100, + name => $name, + lat => $lat, + lon => $lon, + source => 0, + archived => 0 + }, + ); + if ( $count++ % 1000 == 0 ) { + printf( " %2.0f%% complete\n", $count * 100 / $total ); + } + } + $count = 0; + } + + say 'Untangling IRIS / HAFAS stations, this may take a while ...'; + my $res = $db->query( + qq{ + select eva, ds100, name, lat, lon, archived + from stations + where source = 0; + } + ); + while ( my $row = $res->hash ) { + $db->insert( + 'stations', + { + eva => $row->{eva}, + ds100 => $row->{ds100}, + name => $row->{name}, + lat => $row->{lat}, + lon => $row->{lon}, + archived => $row->{archived}, + source => 1, + } + ); + if ( $count++ % 1000 == 0 ) { + printf( " %2.0f%% complete\n", $count * 100 / $total ); + } + } + + # Occasionally, IRIS checkins refer to stations that are not part of + # the Travel::Status::DE::IRIS database. Add those as HAFAS stops to + # satisfy the upcoming foreign key constraints. + + my %iris_has_eva; + $res = $db->query(qq{select eva from stations where source = 0;}); + while ( my $row = $res->hash ) { + $iris_has_eva{ $row->{eva} } = 1; + } + + my %hafas_by_eva; + $res = $db->query(qq{select * from stations where source = 1;}); + while ( my $row = $res->hash ) { + $hafas_by_eva{ $row->{eva} } = $row; + } + + my @iris_ref_stations; + $res + = $db->query( +qq{select distinct checkin_station_id from journeys where backend_id = 0;} + ); + while ( my $row = $res->hash ) { + push( @iris_ref_stations, $row->{checkin_station_id} ); + } + $res + = $db->query( +qq{select distinct checkout_station_id from journeys where backend_id = 0;} + ); + while ( my $row = $res->hash ) { + push( @iris_ref_stations, $row->{checkout_station_id} ); + } + $res + = $db->query( +qq{select distinct checkin_station_id from in_transit where backend_id = 0;} + ); + while ( my $row = $res->hash ) { + push( @iris_ref_stations, $row->{checkin_station_id} ); + } + $res + = $db->query( +qq{select distinct checkout_station_id from in_transit where backend_id = 0;} + ); + while ( my $row = $res->hash ) { + if ( $row->{checkout_station_id} ) { + push( @iris_ref_stations, $row->{checkout_station_id} ); + } + } + + @iris_ref_stations = List::Util::uniq @iris_ref_stations; + + for my $station (@iris_ref_stations) { + if ( not $iris_has_eva{$station} ) { + $hafas_by_eva{$station}{source} = 0; + $hafas_by_eva{$station}{archived} = 1; + $db->insert( 'stations', $hafas_by_eva{$station} ); + } + } + + $db->query( + qq{ + alter table in_transit add constraint in_transit_checkin_eva_fk + foreign key (checkin_station_id, backend_id) + references stations (eva, source); + alter table in_transit add constraint in_transit_checkout_eva_fk + foreign key (checkout_station_id, backend_id) + references stations (eva, source); + alter table journeys add constraint journeys_checkin_eva_fk + foreign key (checkin_station_id, backend_id) + references stations (eva, source); + alter table journeys add constraint journeys_checkout_eva_fk + foreign key (checkout_station_id, backend_id) + references stations (eva, source); + drop view in_transit_str; + drop view journeys_str; + drop view follows_in_transit; + create view in_transit_str as select + user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.ris as is_ris, + backend.name as backend_name, in_transit.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join backends as backend on in_transit.backend_id = backend.id + ; + create view journeys_str as select + journeys.id as journey_id, user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.ris as is_ris, + backend.name as backend_name, journeys.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, edited, route, messages, user_data, + dep_platform, arr_platform + from journeys + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source + left join backends as backend on journeys.backend_id = backend.id + ; + create view follows_in_transit as select + r1.subject_id as follower_id, user_id as followee_id, + users.name as followee_name, + train_type, train_line, train_no, train_id, + in_transit.backend_id as backend_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + order by checkin_time desc + ; + create view users_with_backend as select + users.id as id, users.name as name, status, public_level, + email, password, registered_at, last_seen, + deletion_requested, deletion_notified, use_history, + accept_follows, notifications, profile, backend_id, iris, + hafas, efa, ris, backend.name as backend_name + from users + left join backends as backend on users.backend_id = backend.id + ; + update schema_version set version = 55; + update schema_version set hafas = '0'; + } + ); + say + 'This travelynx instance now has support for non-DB HAFAS backends.'; + say +'If the migration fails due to a deadlock, re-run it after stopping all background workers'; + }, + + # v55 -> v56 + # include backend data in dumpstops command + sub { + my ($db) = @_; + $db->query( + qq{ + create view stations_str as + select stations.name as name, + eva, lat, lon, + backends.name as backend, + iris as is_iris, + hafas as is_hafas, + efa as is_efa, + ris as is_ris + from stations + left join backends + on source = backends.id; + update schema_version set version = 56; + } + ); + }, + + # v56 -> v57 + # Berlin Hbf used to be divided between "Berlin Hbf" (8011160) and "Berlin + # Hbf (tief)" (8098160). Since 2024, both are called "Berlin Hbf". + # As there are some places in the IRIS backend where station names are + # mapped to EVA IDs, this is not good. As of 2.8.21, travelynx deals with + # this IRIS edge case (and probably similar edge cases in Karlsruhe). + # Rebuild stats to ensure no bogus data is in there. + sub { + my ($db) = @_; + $db->query( + qq{ + truncate journey_stats; + update schema_version set version = 57; + } + ); + }, + + # v57 -> v58 + # Add backend data to follows_in_transit + sub { + my ($db) = @_; + $db->query( + qq{ + drop view follows_in_transit; + create view follows_in_transit as select + r1.subject_id as follower_id, user_id as followee_id, + users.name as followee_name, + train_type, train_line, train_no, train_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.ris as is_ris, + backend.name as backend_name, in_transit.backend_id as backend_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join backends as backend on in_transit.backend_id = backend.id + order by checkin_time desc + ; + update schema_version set version = 58; + } + ); + }, + + # v58 -> v59 + # DB HAFAS is dead. Default to DB IRIS for now. + sub { + my ($db) = @_; + $db->query( + qq{ + alter table users alter column backend_id set default 0; + update schema_version set version = 59; + } + ); + }, + + # v59 -> v60 + # Add bahn.de / DBRIS backend + sub { + my ($db) = @_; + $db->insert( + 'backends', + { + iris => 0, + hafas => 0, + efa => 0, + ris => 1, + name => 'bahn.de', + }, + ); + $db->query( + qq{ + update schema_version set version = 60; + } + ); + }, + + # v60 -> v61 + # Rename "ris" / "is_ris" to "dbris" / "is_dbris", as it is DB-specific + sub { + my ($db) = @_; + $db->query( + qq{ + drop view in_transit_str; + drop view journeys_str; + drop view users_with_backend; + drop view follows_in_transit; + alter table backends rename column ris to dbris; + create view in_transit_str as select + user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.name as backend_name, in_transit.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join backends as backend on in_transit.backend_id = backend.id + ; + create view journeys_str as select + journeys.id as journey_id, user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.name as backend_name, journeys.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, edited, route, messages, user_data, + dep_platform, arr_platform + from journeys + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source + left join backends as backend on journeys.backend_id = backend.id + ; + create view users_with_backend as select + users.id as id, users.name as name, status, public_level, + email, password, registered_at, last_seen, + deletion_requested, deletion_notified, use_history, + accept_follows, notifications, profile, backend_id, iris, + hafas, efa, dbris, backend.name as backend_name + from users + left join backends as backend on users.backend_id = backend.id + ; + create view follows_in_transit as select + r1.subject_id as follower_id, user_id as followee_id, + users.name as followee_name, + train_type, train_line, train_no, train_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.name as backend_name, in_transit.backend_id as backend_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join backends as backend on in_transit.backend_id = backend.id + order by checkin_time desc + ; + update schema_version set version = 61; + } + ); + }, + + # v61 -> v62 + # Add MOTIS backend type, add RNV and transitous MOTIS backends + sub { + my ($db) = @_; + $db->query( + qq{ + alter table backends add column motis bool default false; + alter table schema_version add column motis varchar(12); + + create table stations_external_ids ( + eva serial not null primary key, + backend_id smallint not null, + external_id text not null, + + unique (backend_id, external_id), + foreign key (eva, backend_id) references stations (eva, source) + ); + + create view stations_with_external_ids as select + stations.*, stations_external_ids.external_id + from stations + left join stations_external_ids on + stations.eva = stations_external_ids.eva and + stations.source = stations_external_ids.backend_id + ; + + drop view in_transit_str; + drop view journeys_str; + drop view users_with_backend; + drop view follows_in_transit; + + create view in_transit_str as select + user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.motis as is_motis, + backend.name as backend_name, in_transit.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + dep_station_external_id.external_id as dep_external_id, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + arr_station_external_id.external_id as arr_external_id, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and in_transit.backend_id = dep_station_external_id.backend_id + left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and in_transit.backend_id = arr_station_external_id.backend_id + left join backends as backend on in_transit.backend_id = backend.id + ; + create view journeys_str as select + journeys.id as journey_id, user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.motis as is_motis, + backend.name as backend_name, journeys.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + dep_station_external_id.external_id as dep_external_id, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + arr_station_external_id.external_id as arr_external_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, edited, route, messages, user_data, + dep_platform, arr_platform + from journeys + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source + left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and journeys.backend_id = dep_station_external_id.backend_id + left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and journeys.backend_id = arr_station_external_id.backend_id + left join backends as backend on journeys.backend_id = backend.id + ; + create view users_with_backend as select + users.id as id, users.name as name, status, public_level, + email, password, registered_at, last_seen, + deletion_requested, deletion_notified, use_history, + accept_follows, notifications, profile, backend_id, iris, + hafas, efa, dbris, motis, backend.name as backend_name + from users + left join backends as backend on users.backend_id = backend.id + ; + create view follows_in_transit as select + r1.subject_id as follower_id, user_id as followee_id, + users.name as followee_name, + train_type, train_line, train_no, train_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.motis as is_motis, + backend.name as backend_name, in_transit.backend_id as backend_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join backends as backend on in_transit.backend_id = backend.id + order by checkin_time desc + ; + } + ); + $db->query( + qq{ + update schema_version set version = 62; + } + ); + }, + + # v62 -> v63 + # Add EFA backend support + sub { + my ($db) = @_; + $db->query( + qq{ + alter table schema_version add column efa varchar(12); + update schema_version set version = 63; + update schema_version set efa = '0'; + } + ); + }, + + # v63 -> v64 + # Relax train_type length constraints for EFA and MOTIS checkins + sub { + my ($db) = @_; + $db->query( + qq{ + drop view in_transit_str; + drop view journeys_str; + drop view users_with_backend; + drop view follows_in_transit; + + alter table in_transit alter column train_type type varchar(32); + alter table journeys alter column train_type type varchar(32); + + create view in_transit_str as select + user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.motis as is_motis, + backend.name as backend_name, in_transit.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + dep_station_external_id.external_id as dep_external_id, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + arr_station_external_id.external_id as arr_external_id, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and in_transit.backend_id = dep_station_external_id.backend_id + left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and in_transit.backend_id = arr_station_external_id.backend_id + left join backends as backend on in_transit.backend_id = backend.id + ; + create view journeys_str as select + journeys.id as journey_id, user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.motis as is_motis, + backend.name as backend_name, journeys.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + dep_station_external_id.external_id as dep_external_id, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + arr_station_external_id.external_id as arr_external_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, edited, route, messages, user_data, + dep_platform, arr_platform + from journeys + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source + left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and journeys.backend_id = dep_station_external_id.backend_id + left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and journeys.backend_id = arr_station_external_id.backend_id + left join backends as backend on journeys.backend_id = backend.id + ; + create view users_with_backend as select + users.id as id, users.name as name, status, public_level, + email, password, registered_at, last_seen, + deletion_requested, deletion_notified, use_history, + accept_follows, notifications, profile, backend_id, iris, + hafas, efa, dbris, motis, backend.name as backend_name + from users + left join backends as backend on users.backend_id = backend.id + ; + create view follows_in_transit as select + r1.subject_id as follower_id, user_id as followee_id, + users.name as followee_name, + train_type, train_line, train_no, train_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.motis as is_motis, + backend.name as backend_name, in_transit.backend_id as backend_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join backends as backend on in_transit.backend_id = backend.id + order by checkin_time desc + ; + + update schema_version set version = 64; + } + ); + }, + + # v64 -> v65 + # stations_str: add is_motis + sub { + my ($db) = @_; + $db->query( + qq{ + drop view stations_str; + create view stations_str as + select stations.name as name, + eva, lat, lon, + backends.name as backend, + dbris as is_dbris, + efa as is_efa, + iris as is_iris, + hafas as is_hafas, + motis as is_motis + from stations + left join backends + on source = backends.id; + update schema_version set version = 65; + } + ); + }, ); sub sync_stations { @@ -1977,7 +3239,7 @@ sub sync_stations { }, { on_conflict => \ -'(eva) do update set archived = false, source = 0, ds100 = EXCLUDED.ds100, name=EXCLUDED.name, lat=EXCLUDED.lat, lon=EXCLUDED.lon' +'(eva, source) do update set archived = false, source = 0, ds100 = EXCLUDED.ds100, name=EXCLUDED.name, lat=EXCLUDED.lat, lon=EXCLUDED.lon' } ); if ( $count++ % 1000 == 0 ) { @@ -2136,6 +3398,99 @@ sub sync_stations { } } +sub sync_backends_efa { + my ($db) = @_; + for my $service ( Travel::Status::DE::EFA::get_services() ) { + my $present = $db->select( + 'backends', + 'count(*) as count', + { + efa => 1, + name => $service->{shortname} + } + )->hash->{count}; + if ( not $present ) { + $db->insert( + 'backends', + { + dbris => 0, + efa => 1, + hafas => 0, + iris => 0, + motis => 0, + name => $service->{shortname}, + }, + { on_conflict => undef } + ); + } + } + + $db->update( 'schema_version', + { efa => $Travel::Status::DE::EFA::VERSION } ); +} + +sub sync_backends_hafas { + my ($db) = @_; + for my $service ( Travel::Status::DE::HAFAS::get_services() ) { + my $present = $db->select( + 'backends', + 'count(*) as count', + { + hafas => 1, + name => $service->{shortname} + } + )->hash->{count}; + if ( not $present ) { + $db->insert( + 'backends', + { + dbris => 0, + efa => 0, + hafas => 1, + iris => 0, + motis => 0, + name => $service->{shortname}, + }, + { on_conflict => undef } + ); + } + } + + $db->update( 'schema_version', + { hafas => $Travel::Status::DE::HAFAS::VERSION } ); +} + +sub sync_backends_motis { + my ($db) = @_; + for my $service ( Travel::Status::MOTIS::get_services() ) { + my $present = $db->select( + 'backends', + 'count(*) as count', + { + motis => 1, + name => $service->{shortname} + } + )->hash->{count}; + if ( not $present ) { + $db->insert( + 'backends', + { + dbris => 0, + efa => 0, + hafas => 0, + iris => 0, + motis => 1, + name => $service->{shortname}, + }, + { on_conflict => undef } + ); + } + } + + $db->update( 'schema_version', + { motis => $Travel::Status::MOTIS::VERSION } ); +} + sub setup_db { my ($db) = @_; my $tx = $db->begin; @@ -2202,9 +3557,9 @@ sub migrate_db { } my $iris_version = get_schema_version( $db, 'iris' ); - say "Found IRIS station database v${iris_version}"; + say "Found IRIS station table v${iris_version}"; if ( $iris_version eq $Travel::Status::DE::IRIS::Stations::VERSION ) { - say 'Station database is up-to-date'; + say 'Station table is up-to-date'; } else { eval { @@ -2223,6 +3578,39 @@ sub migrate_db { } } + my $efa_version = get_schema_version( $db, 'efa' ); + say "Found backend table for EFA v${efa_version}"; + if ( $efa_version eq $Travel::Status::DE::EFA::VERSION ) { + say 'Backend table is up-to-date'; + } + else { + say +"Synchronizing with Travel::Status::DE::EFA $Travel::Status::DE::EFA::VERSION"; + sync_backends_efa($db); + } + + my $hafas_version = get_schema_version( $db, 'hafas' ); + say "Found backend table for HAFAS v${hafas_version}"; + if ( $hafas_version eq $Travel::Status::DE::HAFAS::VERSION ) { + say 'Backend table is up-to-date'; + } + else { + say +"Synchronizing with Travel::Status::DE::HAFAS $Travel::Status::DE::HAFAS::VERSION"; + sync_backends_hafas($db); + } + + my $motis_version = get_schema_version( $db, 'motis' ) // '0'; + say "Found backend table for Motis v${motis_version}"; + if ( $motis_version eq $Travel::Status::MOTIS::VERSION ) { + say 'Backend table is up-to-date'; + } + else { + say +"Synchronizing with Travel::Status::MOTIS $Travel::Status::MOTIS::VERSION"; + sync_backends_motis($db); + } + $db->update( 'schema_version', { travelynx => $self->app->config->{version} } ); diff --git a/lib/Travelynx/Command/dumpconfig.pm b/lib/Travelynx/Command/dumpconfig.pm index 600ffb0..2c308c9 100644 --- a/lib/Travelynx/Command/dumpconfig.pm +++ b/lib/Travelynx/Command/dumpconfig.pm @@ -1,4 +1,5 @@ package Travelynx::Command::dumpconfig; + # Copyright (C) 2020-2023 Birte Kristina Friesel # # SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/Travelynx/Command/dumpstops.pm b/lib/Travelynx/Command/dumpstops.pm index e6740ec..15f5861 100644 --- a/lib/Travelynx/Command/dumpstops.pm +++ b/lib/Travelynx/Command/dumpstops.pm @@ -1,6 +1,6 @@ package Travelynx::Command::dumpstops; -# Copyright (C) 2024 Birte Kristina Friesel +# Copyright (C) 2024-2025 Birte Kristina Friesel # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -8,7 +8,7 @@ use Mojo::Base 'Mojolicious::Command'; use List::Util qw(); use Text::CSV; -has description => 'Export HAFAS/IRIS stops to CSV'; +has description => 'Export known stops to CSV'; has usage => sub { shift->extract_usage }; @@ -24,12 +24,13 @@ sub run { or die("open($filename): $!\n"); my $csv = Text::CSV->new( { eol => "\r\n" } ); - $csv->combine(qw(name eva lat lon source archived)); + $csv->combine(qw(name eva lat lon backend is_dbris is_efa is_iris is_hafas is_motis)); print $fh $csv->string; my $iter = $self->app->stations->get_db_iterator; while ( my $row = $iter->hash ) { - $csv->combine( @{$row}{qw{name eva lat lon source archived}} ); + $csv->combine( + @{$row}{qw{name eva lat lon backend is_dbris is_efa is_iris is_hafas is_motis}} ); print $fh $csv->string; } close($fh); diff --git a/lib/Travelynx/Command/influxdb.pm b/lib/Travelynx/Command/influxdb.pm index f3fc3de..4b779a2 100644 --- a/lib/Travelynx/Command/influxdb.pm +++ b/lib/Travelynx/Command/influxdb.pm @@ -29,7 +29,7 @@ sub run { my $active = $now->clone->subtract( months => 1 ); my @stats; - my @stations; + my @backend_stats; my @traewelling; push( @@ -85,50 +85,31 @@ sub run { ) ); - push( - @stations, - query_to_influx( - 'iris', - $db->select( - 'stations', - 'count(*) as count', - { - source => 0, - archived => 0 - } - )->hash->{count} - ) - ); - push( - @stations, - query_to_influx( - 'hafas', - $db->select( - 'stations', - 'count(*) as count', - { - source => 1, - archived => 0 - } - )->hash->{count} - ) - ); - push( - @stations, - query_to_influx( - 'archived', - $db->select( 'stations', 'count(*) as count', { archived => 1 } ) - ->hash->{count} - ) - ); - push( - @stations, - query_to_influx( - 'meta', - $db->select( 'related_stations', 'count(*) as count' ) - ->hash->{count} - ) - ); + my @backends = $self->app->stations->get_backends; + + for my $backend (@backends) { + push( + @backend_stats, + [ + $backend->{iris} ? 'IRIS' : $backend->{name}, + $db->select( + 'stations', + 'count(*) as count', + { + source => $backend->{id}, + archived => 0 + } + )->hash->{count}, + $db->select( + 'related_stations', + 'count(*) as count', + { + backend_id => $backend->{id}, + } + )->hash->{count} + ] + ); + } push( @traewelling, @@ -167,10 +148,18 @@ sub run { . $self->app->config->{influxdb}->{url} . ' stats ' . join( ',', @stats ) ); - $self->app->log->debug( 'POST ' - . $self->app->config->{influxdb}->{url} - . ' stations ' - . join( ',', @stations ) ); + for my $backend_entry (@backend_stats) { + $self->app->log->debug( + 'POST ' + . $self->app->config->{influxdb}->{url} + . ' stations,backend=' + . $backend_entry->[0] + . sprintf( + ' count=%d,meta=%d', + $backend_entry->[1], $backend_entry->[2] + ) + ); + } $self->app->log->debug( 'POST ' . $self->app->config->{influxdb}->{url} . ' traewelling ' @@ -181,10 +170,16 @@ sub run { $self->app->config->{influxdb}->{url}, 'stats ' . join( ',', @stats ) )->wait; - $self->app->ua->post_p( - $self->app->config->{influxdb}->{url}, - 'stations ' . join( ',', @stations ) - )->wait; + my $buf = q{}; + for my $backend_entry (@backend_stats) { + $buf + .= "\nstations,backend=" + . $backend_entry->[0] + . sprintf( ' count=%d,meta=%d', + $backend_entry->[1], $backend_entry->[2] ); + } + $self->app->ua->post_p( $self->app->config->{influxdb}->{url}, $buf ) + ->wait; $self->app->ua->post_p( $self->app->config->{influxdb}->{url}, 'traewelling ' . join( ',', @traewelling ) diff --git a/lib/Travelynx/Command/integritycheck.pm b/lib/Travelynx/Command/integritycheck.pm index 4894c3d..be5fe71 100644 --- a/lib/Travelynx/Command/integritycheck.pm +++ b/lib/Travelynx/Command/integritycheck.pm @@ -9,58 +9,60 @@ use List::Util qw(); use Travel::Status::DE::IRIS::Stations; sub run { - my ($self) = @_; - my $found = 0; - my $db = $self->app->pg->db; - - my $res1 = $db->query( - qq{ - select checkin_station_id - from journeys - left join stations on journeys.checkin_station_id = stations.eva - where stations.eva is null; - } - ); - - my $res2 = $db->query( - qq{ - select checkout_station_id - from journeys - left join stations on journeys.checkout_station_id = stations.eva - where stations.eva is null; - } - ); - - my %notified; - while ( my $row = $res1->hash ) { - my $eva = $row->{checkin_station_id}; - if ( not $found ) { - $found = 1; - say + my ( $self, $mode ) = @_; + my $found = 0; + my $db = $self->app->pg->db; + + if ( $mode eq 'all' or $mode eq 'unknown-evas' ) { + + my %notified; + my $res1 = $db->query( + qq{ + select checkin_station_id + from journeys + left join stations on journeys.checkin_station_id = stations.eva + where stations.eva is null; + } + ); + my $res2 = $db->query( + qq{ + select checkout_station_id + from journeys + left join stations on journeys.checkout_station_id = stations.eva + where stations.eva is null; + } + ); + + while ( my $row = $res1->hash ) { + my $eva = $row->{checkin_station_id}; + if ( not $found ) { + $found = 1; + say 'Journeys in the travelynx database contain the following unknown EVA IDs.'; - say '------------8<----------'; - say 'Travel::Status::DE::IRIS v' - . $Travel::Status::DE::IRIS::Stations::VERSION; - } - if ( not $notified{$eva} ) { - say $eva; - $notified{$eva} = 1; + say '------------8<----------'; + say 'Travel::Status::DE::IRIS v' + . $Travel::Status::DE::IRIS::Stations::VERSION; + } + if ( not $notified{$eva} ) { + say $eva; + $notified{$eva} = 1; + } } - } - while ( my $row = $res2->hash ) { - my $eva = $row->{checkout_station_id}; - if ( not $found ) { - $found = 1; - say + while ( my $row = $res2->hash ) { + my $eva = $row->{checkout_station_id}; + if ( not $found ) { + $found = 1; + say 'Journeys in the travelynx database contain the following unknown EVA IDs.'; - say '------------8<----------'; - say 'Travel::Status::DE::IRIS v' - . $Travel::Status::DE::IRIS::Stations::VERSION; - } - if ( not $notified{$eva} ) { - say $eva; - $notified{$eva} = 1; + say '------------8<----------'; + say 'Travel::Status::DE::IRIS v' + . $Travel::Status::DE::IRIS::Stations::VERSION; + } + if ( not $notified{$eva} ) { + say $eva; + $notified{$eva} = 1; + } } } @@ -70,42 +72,101 @@ sub run { $found = 0; } - my $rename = $self->app->renamed_station; + if ( $mode eq 'all' or $mode eq 'unknown-route-entries' ) { - my $res = $db->select( 'journeys', [ 'route', 'edited' ] )->expand; - while ( my $j = $res->hash ) { - if ( $j->{edited} & 0x0010 ) { - next; - } - my @stops = @{ $j->{route} // [] }; - for my $stop (@stops) { - my $stop_name = $stop->[0]; - if ( $rename->{ $stop->[0] } ) { - $stop->[0] = $rename->{ $stop->[0] }; + my %notified; + my $rename = $self->app->renamed_station; + my $res = $db->select( 'journeys', [ 'route', 'edited' ] )->expand; + + while ( my $j = $res->hash ) { + if ( $j->{edited} & 0x0010 ) { + next; + } + my @stops = @{ $j->{route} // [] }; + for my $stop (@stops) { + my $stop_name = $stop->[0]; + if ( $rename->{ $stop->[0] } ) { + $stop->[0] = $rename->{ $stop->[0] }; + } + } + my @unknown + = $self->app->stations->grep_unknown( map { $_->[0] } @stops ); + for my $stop_name (@unknown) { + if ( not $notified{$stop_name} ) { + if ( not $found ) { + say +'Journeys in the travelynx database contain the following unknown route entries.'; + say + 'Note that this check ignores manual route entries.'; + say +'All reports refer to routes obtained via HAFAS/IRIS.'; + say '------------8<----------'; + say 'Travel::Status::DE::IRIS v' + . $Travel::Status::DE::IRIS::Stations::VERSION; + $found = 1; + } + say $stop_name; + $notified{$stop_name} = 1; + } } } - my @unknown - = $self->app->stations->grep_unknown( map { $_->[0] } @stops ); - for my $stop_name (@unknown) { - if ( not $notified{$stop_name} ) { + } + + if ($found) { + say '------------8<----------'; + say ''; + $found = 0; + } + + if ( $mode eq 'all' or $mode eq 'checkout-eva-vs-route-eva' ) { + + my $res = $db->select( + 'journeys_str', + [ 'journey_id', 'sched_arr_ts', 'route', 'arr_name', 'arr_eva' ], + { backend_id => 0 } + )->expand; + + journey: while ( my $j = $res->hash ) { + my $found_in_route; + my $found_arr; + for my $stop ( @{ $j->{route} // [] } ) { + if ( not $stop->[1] ) { + next journey; + } + if ( $stop->[1] == $j->{arr_eva} ) { + $found_in_route = 1; + last; + } + if ( $stop->[2]{sched_arr} + and $j->{sched_arr_ts} + and $stop->[2]{sched_arr} == int( $j->{sched_arr_ts} ) ) + { + $found_arr = $stop; + } + } + if ( $found_arr and not $found_in_route ) { if ( not $found ) { + say q{}; say -'Journeys in the travelynx database contain the following unknown route entries.'; - say 'Note that this check ignores manual route entries.'; - say 'All reports refer to routes obtained via HAFAS/IRIS.'; +'The following journeys have route entries which do not agree with checkout EVA ID.'; + say +'checkout station ID (left) vs route entry with matching checkout time (right)'; say '------------8<----------'; - say 'Travel::Status::DE::IRIS v' - . $Travel::Status::DE::IRIS::Stations::VERSION; $found = 1; } - say $stop_name; - $notified{$stop_name} = 1; + printf( + "%7d %d (%s) vs %d (%s)\n", + $j->{journey_id}, $j->{arr_eva}, $j->{arr_name}, + $found_arr->[1], $found_arr->[0] + ); } } } + if ($found) { say '------------8<----------'; say ''; + $found = 0; } } diff --git a/lib/Travelynx/Command/maintenance.pm b/lib/Travelynx/Command/maintenance.pm index c9c7ed6..7baf762 100644 --- a/lib/Travelynx/Command/maintenance.pm +++ b/lib/Travelynx/Command/maintenance.pm @@ -153,22 +153,6 @@ sub run { } $tx->commit; - - # Computing stats may take a while, but we've got all time in the - # world here. This means users won't have to wait when loading their - # own journey log. - say 'Generating missing stats ...'; - for - my $user ( $db->select( 'users', ['id'], { status => 1 } )->hashes->each ) - { - $tx = $db->begin; - $self->app->journeys->generate_missing_stats( uid => $user->{id} ); - $self->app->journeys->get_stats( - uid => $user->{id}, - year => $now->year - ); - $tx->commit; - } } 1; diff --git a/lib/Travelynx/Command/traewelling.pm b/lib/Travelynx/Command/traewelling.pm index 4c47e84..e4e0134 100644 --- a/lib/Travelynx/Command/traewelling.pm +++ b/lib/Travelynx/Command/traewelling.pm @@ -20,6 +20,12 @@ sub pull_sync { my $request_count = 0; for my $account_data ( $self->app->traewelling->get_pull_accounts ) { + if ( -e 'maintenance' ) { + $self->app->log->debug( + 'treawelling: "maintenance" file found, aborting'); + return; + } + my $in_transit = $self->app->in_transit->get( uid => $account_data->{user_id}, ); @@ -30,6 +36,13 @@ sub pull_sync { next; } + if ( not defined $account_data->{data}{user_name} ) { + $self->app->log->debug( +"travelynx user $account_data->{user_id} has a Traewellig connection, but no username" + ); + next; + } + # $account_data->{user_id} is the travelynx uid # $account_data->{user_name} is the Träwelling username $request_count += 1; @@ -39,7 +52,7 @@ sub pull_sync { # In 'work', the event loop is not running, # so there's no need to multiply by $request_count at the moment - Mojo::Promise->timer(1)->then( + Mojo::Promise->timer(1.5)->then( sub { return $self->app->traewelling_api->get_status_p( username => $account_data->{data}{user_name}, @@ -77,6 +90,13 @@ sub push_sync { my %push_result; for my $candidate ( $self->app->traewelling->get_pushable_accounts ) { + + if ( -e 'maintenance' ) { + $self->app->log->debug( + 'treawelling: "maintenance" file found, aborting'); + return; + } + $self->app->log->debug( "Pushing to Traewelling for UID $candidate->{uid}"); my $trip_id = $candidate->{journey_data}{trip_id}; @@ -102,12 +122,12 @@ sub push_sync { my ($status) = @_; $push_result{ $status->{http} } += 1; } - )->catch( + )->catch( sub { my ($status) = @_; $push_result{ $status->{http} // 0 } += 1; } - )->wait; + )->wait; } return \%push_result; @@ -121,6 +141,12 @@ sub run { my $push_result; my $pull_result; + if ( -e 'maintenance' ) { + $self->app->log->debug( + 'treawelling: "maintenance" file found, aborting'); + return; + } + if ( not $direction or $direction eq 'push' ) { $push_result = $self->push_sync; } @@ -133,6 +159,12 @@ sub run { my $trwl_pull_finished_at = DateTime->now( time_zone => 'Europe/Berlin' ); + if ( -e 'maintenance' ) { + $self->app->log->debug( + 'treawelling: "maintenance" file found, aborting'); + return; + } + my $trwl_push_duration = $trwl_push_finished_at->epoch - $started_at->epoch; my $trwl_pull_duration = $trwl_pull_finished_at->epoch - $trwl_push_finished_at->epoch; diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm index 10b1b69..071befa 100644 --- a/lib/Travelynx/Command/work.pm +++ b/lib/Travelynx/Command/work.pm @@ -1,11 +1,14 @@ package Travelynx::Command::work; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Command'; use Mojo::Promise; +use utf8; + use DateTime; use JSON; use List::Util; @@ -15,12 +18,17 @@ has description => 'Update real-time data of active journeys'; has usage => sub { shift->extract_usage }; sub run { - my ($self) = @_; + my ( $self, $backend ) = @_; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); my $checkin_deadline = $now->clone->subtract( hours => 48 ); my $json = JSON->new; + if ( -e 'maintenance' ) { + $self->app->log->debug('work: "maintenance" file found, aborting'); + return; + } + my $num_incomplete = $self->app->in_transit->delete_incomplete_checkins( earlier_than => $checkin_deadline ); @@ -28,245 +36,700 @@ sub run { $self->app->log->debug("Removed ${num_incomplete} incomplete checkins"); } - my $errors = 0; + my $errors = 0; + my $backend_issues = 0; + my $rate_limit_counts = 0; + my $dbris_rate_limited = 0; for my $entry ( $self->app->in_transit->get_all_active ) { + if ( -e 'maintenance' ) { + $self->app->log->debug('work: "maintenance" file found, aborting'); + return; + } + my $uid = $entry->{user_id}; my $dep = $entry->{dep_eva}; my $arr = $entry->{arr_eva}; my $train_id = $entry->{train_id}; - if ( $train_id =~ m{[|]} ) { + if ( $train_id eq 'manual' + and ( not $backend or $backend eq 'manual' ) ) + { + if ( $arr + and $entry->{real_arr_ts} + and $now->epoch - $entry->{real_arr_ts} > 600 ) + { + $self->app->checkout_p( + station => $arr, + force => 2, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; + } + } + + elsif ( $entry->{is_dbris} and ( not $backend or $backend eq 'dbris' ) ) + { - $self->app->hafas->get_journey_p( trip_id => $train_id )->then( - sub { - my ($journey) = @_; + eval { - my $found_dep; - my $found_arr; - for my $stop ( $journey->route ) { - if ( $stop->loc->eva == $dep ) { - $found_dep = $stop; + Mojo::Promise->timer( + $dbris_rate_limited ? 4.5 : ( $backend ? 1.2 : 1.0 ) ) + ->then( + sub { + return $self->app->dbris->get_journey_p( + trip_id => $train_id ); + } + )->then( + sub { + my ($journey) = @_; + + $dbris_rate_limited = 0; + + my $found_dep; + my $found_arr; + for my $stop ( $journey->route ) { + if ( $stop->eva == $dep ) { + $found_dep = $stop; + } + if ( $arr and $stop->eva == $arr ) { + $found_arr = $stop; + last; + } + } + if ( not $found_dep ) { + $self->app->log->debug( + "Did not find $dep within journey $train_id"); + return; } - if ( $arr and $stop->loc->eva == $arr ) { - $found_arr = $stop; - last; + + if ( $found_dep->rt_dep ) { + $self->app->in_transit->update_departure_dbris( + uid => $uid, + journey => $journey, + stop => $found_dep, + dep_eva => $dep, + arr_eva => $arr, + train_id => $train_id, + ); + } + if ( $found_dep->sched_dep + and $found_dep->dep->epoch > $now->epoch ) + { + $self->app->add_wagonorder( + uid => $uid, + train_id => $train_id, + is_departure => 1, + eva => $dep, + datetime => $found_dep->sched_dep, + train_type => $journey->type, + train_no => $journey->number, + ); + $self->app->add_stationinfo( $uid, 1, + $train_id, $found_dep->eva ); + } + + if ( + $found_arr + and + ( $found_arr->rt_arr or $found_arr->is_cancelled ) + ) + { + $self->app->in_transit->update_arrival_dbris( + uid => $uid, + journey => $journey, + train_id => $train_id, + stop => $found_arr, + dep_eva => $dep, + arr_eva => $arr + ); + } + if ( $found_arr and $found_arr->rt_arr ) { + if ( $found_arr->arr->epoch - $now->epoch < 600 ) { + $self->app->add_wagonorder( + uid => $uid, + train_id => $train_id, + is_arrival => 1, + eva => $arr, + datetime => $found_arr->sched_dep, + train_type => $journey->type, + train_no => $journey->number, + ); + $self->app->add_stationinfo( $uid, 0, + $train_id, $found_dep->eva, + $found_arr->eva ); + } + } + if ( $found_arr and $found_arr->is_cancelled ) { + + # check out (adds a cancelled journey and resets journey state + # to destination selection) + $self->app->checkout_p( + station => $arr, + force => 0, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; } } - if ( not $found_dep ) { + )->catch( + sub { + my ($err) = @_; $self->app->log->debug( - "Did not find $dep within journey $train_id"); - return; - } - - if ( $found_dep->{rt_dep} ) { - $self->app->in_transit->update_departure_hafas( - uid => $uid, - journey => $journey, - stop => $found_dep, - dep_eva => $dep, - arr_eva => $arr +"work($uid) @ DBRIS $entry->{backend_name}: journey: $err" ); + if ( $err =~ m{HTTP 429} ) { + $dbris_rate_limited = 1; + $rate_limit_counts += 1; + } + else { + $backend_issues += 1; + } } + )->wait; - if ( $found_arr and $found_arr->{rt_arr} ) { - $self->app->in_transit->update_arrival_hafas( - uid => $uid, - journey => $journey, - stop => $found_arr, - dep_eva => $dep, - arr_eva => $arr + if ( $arr + and $entry->{real_arr_ts} + and $now->epoch - $entry->{real_arr_ts} > 600 ) + { + $self->app->checkout_p( + station => $arr, + force => 2, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; + } + }; + if ($@) { + $errors += 1; + $self->app->log->error( + "work($uid) @ DBRIS $entry->{backend_name}: $@"); + } + } + + elsif ( $entry->{is_efa} and ( not $backend or $backend eq 'efa' ) ) { + eval { + $self->app->efa->get_journey_p( + trip_id => $train_id, + service => $entry->{backend_name} + )->then( + sub { + my ($journey) = @_; + + my $found_dep; + my $found_arr; + for my $stop ( $journey->route ) { + if ( $stop->id_num == $dep ) { + $found_dep = $stop; + } + if ( $arr and $stop->id_num == $arr ) { + $found_arr = $stop; + last; + } + } + if ( not $found_dep ) { + $self->app->log->debug( + "Did not find $dep within journey $train_id"); + return; + } + + if ( $found_dep->rt_dep ) { + $self->app->in_transit->update_departure_efa( + uid => $uid, + journey => $journey, + stop => $found_dep, + dep_eva => $dep, + arr_eva => $arr, + trip_id => $train_id, + ); + } + + if ( + $found_arr + and + ( $found_arr->rt_arr or $found_arr->is_cancelled ) + ) + { + $self->app->in_transit->update_arrival_efa( + uid => $uid, + journey => $journey, + stop => $found_arr, + dep_eva => $dep, + arr_eva => $arr, + trip_id => $train_id, + ); + } + if ( $found_arr and $found_arr->is_cancelled ) { + + # check out (adds a cancelled journey and resets journey state + # to destination selection) + $self->app->checkout_p( + station => $arr, + force => 0, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; + } + } + )->catch( + sub { + my ($err) = @_; + $backend_issues += 1; + $self->app->log->error( +"work($uid) @ EFA $entry->{backend_name}: journey: $err" ); } + )->wait; + + if ( $arr + and $entry->{real_arr_ts} + and $now->epoch - $entry->{real_arr_ts} > 600 ) + { + $self->app->checkout_p( + station => $arr, + force => 2, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; } - )->catch( - sub { - my ($err) = @_; - if ( $err =~ m{svcResL\[0\][.]err is (?:FAIL|PARAMETER)$} ) - { - # HAFAS do be weird. These are not actionable. - $self->app->log->debug("work($uid)/journey: $err"); + }; + if ($@) { + $errors += 1; + $self->app->log->error( + "work($uid) @ EFA $entry->{backend_name}: $@"); + } + } + + elsif ( $entry->{is_motis} and ( not $backend or $backend eq 'motis' ) ) + { + + eval { + $self->app->motis->get_trip_p( + service => $entry->{backend_name}, + trip_id => $train_id, + )->then( + sub { + my ($journey) = @_; + + for my $stopover ( $journey->stopovers ) { + if ( not defined $stopover->stop->{eva} ) { + + # Looks like MOTIS / transitous station IDs can change after the fact. + # So let's be safe rather than sorry, even if this causes way too many calls to the slow path + # (Stations::get_by_external_id uses string lookups). + # This function call implicitly sets $stopover->stop->{eva} for MOTIS backends. + $self->app->stations->add_or_update( + stop => $stopover->stop, + motis => $entry->{backend_name}, + ); + + $self->app->log->debug( "mapped " + . $stopover->stop->id . " to " + . $stopover->stop->{eva} ); + } + } + + my $found_departure; + my $found_arrival; + for my $stopover ( $journey->stopovers ) { + if ( $stopover->stop->{eva} == $dep ) { + $found_departure = $stopover; + } + + if ( $arr and $stopover->stop->{eva} == $arr ) { + $found_arrival = $stopover; + last; + } + } + + if ( not $found_departure ) { + $self->app->log->debug( + "Did not find $dep within trip $train_id"); + return; + } + + if ( $found_departure->realtime_departure ) { + $self->app->in_transit->update_departure_motis( + uid => $uid, + journey => $journey, + stopover => $found_departure, + dep_eva => $dep, + arr_eva => $arr, + train_id => $train_id, + ); + } + + if ( $found_arrival + and $found_arrival->realtime_arrival ) + { + $self->app->in_transit->update_arrival_motis( + uid => $uid, + journey => $journey, + train_id => $train_id, + stopover => $found_arrival, + dep_eva => $dep, + arr_eva => $arr + ); + } } - else { - $self->app->log->error("work($uid)/journey: $err"); + )->catch( + sub { + my ($err) = @_; + $self->app->log->error( +"work($uid) @ MOTIS $entry->{backend_name}: journey: $err" + ); } + )->wait; + + if ( $arr + and $entry->{real_arr_ts} + and $now->epoch - $entry->{real_arr_ts} > 600 ) + { + $self->app->checkout_p( + station => $arr, + force => 2, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; } - )->wait; + }; + if ($@) { + $errors += 1; + $self->app->log->error( + "work($uid) @ MOTIS $entry->{backend_name}: $@"); + } + } - if ( $arr - and $entry->{real_arr_ts} - and $now->epoch - $entry->{real_arr_ts} > 600 ) - { - $self->app->checkout_p( - station => $arr, - force => 2, - dep_eva => $dep, - arr_eva => $arr, - uid => $uid + elsif ( $entry->{is_hafas} and ( not $backend or $backend eq 'hafas' ) ) + { + + eval { + + $self->app->hafas->get_journey_p( + trip_id => $train_id, + service => $entry->{backend_name} + )->then( + sub { + my ($journey) = @_; + + my $found_dep; + my $found_arr; + for my $stop ( $journey->route ) { + if ( $stop->loc->eva == $dep ) { + $found_dep = $stop; + } + if ( $arr and $stop->loc->eva == $arr ) { + $found_arr = $stop; + last; + } + } + if ( not $found_dep ) { + $self->app->log->debug( + "Did not find $dep within journey $train_id"); + return; + } + + if ( $found_dep->rt_dep ) { + $self->app->in_transit->update_departure_hafas( + uid => $uid, + journey => $journey, + stop => $found_dep, + dep_eva => $dep, + arr_eva => $arr + ); + } + if ( + $found_dep->sched_dep + and ( $entry->{backend_id} <= 1 + or $entry->{backend_name} eq 'VRN' + or $entry->{backend_name} eq 'ÖBB' ) + and $journey->class <= 16 + and $found_dep->dep->epoch > $now->epoch + ) + { + $self->app->add_wagonorder( + uid => $uid, + train_id => $journey->id, + is_departure => 1, + eva => $dep, + datetime => $found_dep->sched_dep, + train_type => $journey->type =~ s{ +$}{}r, + train_no => $journey->number, + ); + $self->app->add_stationinfo( $uid, 1, + $journey->id, $found_dep->loc->eva ); + } + + if ( $found_arr and $found_arr->rt_arr ) { + $self->app->in_transit->update_arrival_hafas( + uid => $uid, + journey => $journey, + stop => $found_arr, + dep_eva => $dep, + arr_eva => $arr + ); + if ( + ( + $entry->{backend_id} <= 1 + or $entry->{backend_name} eq 'VRN' + or $entry->{backend_name} eq 'ÖBB' + ) + and $journey->class <= 16 + and $found_arr->arr->epoch - $now->epoch < 600 + ) + { + $self->app->add_wagonorder( + uid => $uid, + train_id => $journey->id, + is_arrival => 1, + eva => $arr, + datetime => $found_arr->sched_dep, + train_type => $journey->type, + train_no => $journey->number, + ); + $self->app->add_stationinfo( $uid, 0, + $journey->id, $found_dep->loc->eva, + $found_arr->loc->eva ); + } + } + } + )->catch( + sub { + my ($err) = @_; + $backend_issues += 1; + if ( $err + =~ m{svcResL\[0\][.]err is (?:FAIL|PARAMETER)$} + or $err =~ m{timeout} ) + { + # These are not actionable. + $self->app->log->debug( +"work($uid) @ HAFAS $entry->{backend_name}: journey: $err" + ); + } + else { + $self->app->log->error( +"work($uid) @ HAFAS $entry->{backend_name}: journey: $err" + ); + } + } )->wait; + + if ( $arr + and $entry->{real_arr_ts} + and $now->epoch - $entry->{real_arr_ts} > 600 ) + { + $self->app->checkout_p( + station => $arr, + force => 2, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; + } + }; + if ($@) { + $errors += 1; + $self->app->log->error( + "work($uid) @ HAFAS $entry->{backend_name}: $@"); } - next; } + # TODO irgendwo ist hier ne race condition wo ein neuer checkin (in HAFAS) mit IRIS-Daten überschrieben wird. + # Die ganzen updates brauchen wirklich mal sanity checks mit train id ... + # Note: IRIS data is not always updated in real-time. Both departure and # arrival delays may take several minutes to appear, especially in case # of large-scale disturbances. We work around this by continuing to # update departure data for up to 15 minutes after departure and # delaying automatic checkout by at least 10 minutes. - eval { - if ( $now->epoch - $entry->{real_dep_ts} < 900 ) { - my $status = $self->app->iris->get_departures( - station => $dep, - lookbehind => 30, - lookahead => 30 - ); - if ( $status->{errstr} ) { - die("get_departures($dep): $status->{errstr}\n"); - } - - my ($train) = List::Util::first { $_->train_id eq $train_id } - @{ $status->{results} }; + elsif ( $entry->{is_iris} and ( not $backend or $backend eq 'iris' ) ) { + eval { + if ( $now->epoch - $entry->{real_dep_ts} < 900 ) { + my $status = $self->app->iris->get_departures( + station => $dep, + lookbehind => 30, + lookahead => 30 + ); + if ( $status->{errstr} ) { + die("get_departures($dep): $status->{errstr}\n"); + } - if ( not $train ) { - $self->app->log->debug( - "could not find train $train_id at $dep\n"); - return; - } + my ($train) + = List::Util::first { $_->train_id eq $train_id } + @{ $status->{results} }; - $self->app->in_transit->update_departure( - uid => $uid, - train => $train, - dep_eva => $dep, - arr_eva => $arr, - route => [ $self->app->iris->route_diff($train) ] - ); + if ( not $train ) { + $self->app->log->debug( + "could not find train $train_id at $dep\n"); + return; + } - if ( $train->departure_is_cancelled and $arr ) { - my $checked_in - = $self->app->in_transit->update_departure_cancelled( + $self->app->in_transit->update_departure( uid => $uid, train => $train, dep_eva => $dep, arr_eva => $arr, - ); - - # depending on the amount of users in transit, some time may - # have passed between fetching $entry from the database and - # now. Only check out if the user is still checked into this - # train. - if ($checked_in) { + route => [ $self->app->iris->route_diff($train) ] + ); - # check out (adds a cancelled journey and resets journey state - # to checkin - $self->app->checkout_p( - station => $arr, - force => 2, + if ( $train->departure_is_cancelled and $arr ) { + my $checked_in + = $self->app->in_transit->update_departure_cancelled( + uid => $uid, + train => $train, dep_eva => $dep, arr_eva => $arr, - uid => $uid - )->wait; + ); + + # depending on the amount of users in transit, some time may + # have passed between fetching $entry from the database and + # now. Only check out if the user is still checked into this + # train. + if ($checked_in) { + + # check out (adds a cancelled journey and resets journey state + # to checkin + $self->app->checkout_p( + station => $arr, + force => 2, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; + } + } + else { + $self->app->add_route_timestamps( $uid, $train, 1 ); + $self->app->add_wagonorder( + uid => $uid, + train_id => $train->train_id, + is_departure => 1, + eva => $dep, + datetime => $train->sched_departure, + train_type => $train->type, + train_no => $train->train_no + ); + $self->app->add_stationinfo( $uid, 1, $train->train_id, + $dep, $arr ); } } - else { - $self->app->add_route_timestamps( $uid, $train, 1 ); - } + }; + if ($@) { + $errors += 1; + $self->app->log->error("work($uid) @ IRIS: departure: $@"); } - }; - if ($@) { - $errors += 1; - $self->app->log->error("work($uid)/departure: $@"); - } - eval { - if ( - $arr - and ( not $entry->{real_arr_ts} - or $now->epoch - $entry->{real_arr_ts} < 600 ) - ) - { - my $status = $self->app->iris->get_departures( - station => $arr, - lookbehind => 20, - lookahead => 220 - ); - if ( $status->{errstr} ) { - die("get_departures($arr): $status->{errstr}\n"); - } + eval { + if ( + $arr + and ( not $entry->{real_arr_ts} + or $now->epoch - $entry->{real_arr_ts} < 600 ) + ) + { + my $status = $self->app->iris->get_departures( + station => $arr, + lookbehind => 20, + lookahead => 220 + ); + if ( $status->{errstr} ) { + die("get_departures($arr): $status->{errstr}\n"); + } - # 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 > $entry->{sched_dep_ts} - } - @{ $status->{results} }; + # 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 > $entry->{sched_dep_ts} + } + @{ $status->{results} }; - $train //= List::Util::first { $_->train_id eq $train_id } - @{ $status->{results} }; + $train //= List::Util::first { $_->train_id eq $train_id } + @{ $status->{results} }; - if ( not $train ) { + if ( not $train ) { - # If we haven't seen the train yet, its arrival is probably - # too far in the future. This is not critical. - return; - } + # If we haven't seen the train yet, its arrival is probably + # too far in the future. This is not critical. + return; + } - my $checked_in = $self->app->in_transit->update_arrival( - uid => $uid, - train => $train, - route => [ $self->app->iris->route_diff($train) ], - dep_eva => $dep, - arr_eva => $arr, - ); + my $checked_in = $self->app->in_transit->update_arrival( + uid => $uid, + train => $train, + route => [ $self->app->iris->route_diff($train) ], + dep_eva => $dep, + arr_eva => $arr, + ); - if ( $checked_in and $train->arrival_is_cancelled ) { + if ( $checked_in and $train->arrival_is_cancelled ) { - # check out (adds a cancelled journey and resets journey state - # to destination selection) - $self->app->checkout_p( + # check out (adds a cancelled journey and resets journey state + # to destination selection) + $self->app->checkout_p( + station => $arr, + force => 0, + dep_eva => $dep, + arr_eva => $arr, + uid => $uid + )->wait; + } + else { + $self->app->add_route_timestamps( + $uid, $train, 0, + ( + defined $entry->{real_arr_ts} + and $now->epoch > $entry->{real_arr_ts} + ) ? 1 : 0 + ); + $self->app->add_wagonorder( + uid => $uid, + train_id => $train->train_id, + is_arrival => 1, + eva => $arr, + datetime => $train->sched_departure, + train_type => $train->type, + train_no => $train->train_no + ); + $self->app->add_stationinfo( $uid, 0, $train->train_id, + $dep, $arr ); + } + } + elsif ( $entry->{real_arr_ts} ) { + my ( undef, $error ) = $self->app->checkout_p( station => $arr, - force => 0, + force => 2, dep_eva => $dep, arr_eva => $arr, uid => $uid + )->catch( + sub { + my ($error) = @_; + $backend_issues += 1; + $self->app->log->error( + "work($uid) @ IRIS: arrival: $error"); + $errors += 1; + } )->wait; } - else { - $self->app->add_route_timestamps( - $uid, $train, 0, - ( - defined $entry->{real_arr_ts} - and $now->epoch > $entry->{real_arr_ts} - ) ? 1 : 0 - ); - } + }; + if ($@) { + $self->app->log->error("work($uid) @ IRIS: arrival: $@"); + $errors += 1; } - elsif ( $entry->{real_arr_ts} ) { - my ( undef, $error ) = $self->app->checkout_p( - station => $arr, - force => 2, - dep_eva => $dep, - arr_eva => $arr, - uid => $uid - )->catch( - sub { - my ($error) = @_; - $self->app->log->error("work($uid)/arrival: $error"); - $errors += 1; - } - )->wait; - } - }; - if ($@) { - $self->app->log->error("work($uid)/arrival: $@"); - $errors += 1; + + eval { }; } - eval { }; } my $started_at = $now; @@ -274,22 +737,35 @@ sub run { my $worker_duration = $main_finished_at->epoch - $started_at->epoch; if ( $self->app->config->{influxdb}->{url} ) { + my $tags = q{}; + if ($backend) { + $tags .= ",backend=${backend}"; + } if ( $self->app->mode eq 'development' ) { $self->app->log->debug( 'POST ' . $self->app->config->{influxdb}->{url} - . " worker runtime_seconds=${worker_duration},errors=${errors}" + . " worker${tags} runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}" ); } else { $self->app->ua->post_p( $self->app->config->{influxdb}->{url}, - "worker runtime_seconds=${worker_duration},errors=${errors}" ) - ->wait; +"worker${tags} runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}" + )->wait; } } if ( not $self->app->config->{traewelling}->{separate_worker} ) { $self->app->start('traewelling'); } + + # add_wagonorder and add_stationinfo assume a permanently running IOLoop + # and do not allow Mojolicious commands to wait until they have completed. + # Hence, some add_wagonorder and add_stationinfo calls made here may not + # complete before the work command exits, and thus have no effect. + # + # This is not ideal and will need fixing at some point. Until then, here + # is the pragmatic solution for 99% of the associated issues. + Mojo::Promise->timer(5)->wait; } 1; diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index f1dc43e..bf1eac2 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1,11 +1,13 @@ package Travelynx::Controller::Account; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Controller'; use JSON; +use Math::Polygon; use Mojo::Util qw(xml_escape); use Text::Markdown; use UUID::Tiny qw(:std); @@ -45,6 +47,7 @@ sub send_registration_mail { my $ua = $self->req->headers->user_agent; my $reg_url = $self->url_for('reg')->to_abs->scheme('https'); + my $tos_url = $self->url_for('tos')->to_abs->scheme('https'); my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https'); my $body = "Hallo, ${user}!\n\n"; @@ -53,7 +56,8 @@ sub send_registration_mail { $body .= "Falls die Registrierung von dir ausging, kannst du den Account unter\n"; $body .= "${reg_url}/${user_id}/${token}\n"; - $body .= "freischalten.\n\n"; + $body .= "freischalten.\n"; + $body .= "Beachte dabei die Nutzungsbedingungen: ${tos_url}\n\n"; $body .= "Falls nicht, ignoriere diese Mail bitte. Nach etwa 48 Stunden wird deine\n"; $body @@ -831,29 +835,6 @@ sub insight { } -sub services { - my ($self) = @_; - my $user = $self->current_user; - - if ( $self->param('action') and $self->param('action') eq 'save' ) { - my $sb = $self->param('stationboard'); - my $value = 0; - if ( $sb =~ m{ ^ \d+ $ }x and $sb >= 0 and $sb <= 4 ) { - $value = int($sb); - } - $self->users->use_external_services( - uid => $user->{id}, - set => $value - ); - $self->flash( success => 'external' ); - $self->redirect_to('account'); - } - - $self->param( stationboard => - $self->users->use_external_services( uid => $user->{id} ) ); - $self->render('use_external_links'); -} - sub webhook { my ($self) = @_; @@ -1022,6 +1003,273 @@ sub password_form { $self->render('change_password'); } +sub lonlat_in_polygon { + my ( $self, $polygon, $lonlat ) = @_; + + my $circle = shift( @{$polygon} ); + my @holes = @{$polygon}; + + my $circle_poly = Math::Polygon->new( @{$circle} ); + if ( $circle_poly->contains($lonlat) ) { + for my $hole (@holes) { + my $hole_poly = Math::Polygon->new( @{$hole} ); + if ( $hole_poly->contains($lonlat) ) { + return; + } + } + return 1; + } + return; +} + +sub backend_form { + my ($self) = @_; + my $user = $self->current_user; + + my @backends = $self->stations->get_backends; + my @suggested_backends; + + my %place_map = ( + AT => 'Österreich', + CH => 'Schweiz', + 'CH-BE' => 'Kanton Bern', + 'CH-GE' => 'Kanton Genf', + 'CH-LU' => 'Kanton Luzern', + 'CH-ZH' => 'Kanton Zürich', + DE => 'Deutschland', + 'DE-BB' => 'Brandenburg', + 'DE-BW' => 'Baden-Württemberg', + 'DE-BE' => 'Berlin', + 'DE-BY' => 'Bayern', + 'DE-HB' => 'Bremen', + 'DE-HE' => 'Hessen', + 'DE-MV' => 'Mecklenburg-Vorpommern', + 'DE-NI' => 'Niedersachsen', + 'DE-NW' => 'Nordrhein-Westfalen', + 'DE-RP' => 'Rheinland-Pfalz', + 'DE-SH' => 'Schleswig-Holstein', + 'DE-ST' => 'Sachsen-Anhalt', + 'DE-TH' => 'Thüringen', + DK => 'Dänemark', + 'GB-NIR' => 'Nordirland', + LI => 'Liechtenstein', + LU => 'Luxembourg', + IE => 'Irland', + 'US-CA' => 'California', + 'US-TX' => 'Texas', + ); + + my ( $user_lat, $user_lon ) + = $self->journeys->get_latest_checkout_latlon( uid => $user->{id} ); + + for my $backend (@backends) { + my $type = 'UNKNOWN'; + if ( $backend->{iris} ) { + $type = 'IRIS-TTS'; + $backend->{name} = 'IRIS'; + $backend->{longname} = 'Deutsche Bahn: IRIS-TTS'; + $backend->{homepage} = 'https://www.bahn.de'; + $backend->{legacy} = 1; + } + elsif ( $backend->{dbris} ) { + $type = 'DBRIS'; + $backend->{longname} = 'Deutsche Bahn: bahn.de'; + $backend->{homepage} = 'https://www.bahn.de'; + $backend->{recommended} = 1; + } + elsif ( $backend->{efa} ) { + if ( my $s = $self->efa->get_service( $backend->{name} ) ) { + $type = 'EFA'; + $backend->{longname} = $s->{name}; + $backend->{homepage} = $s->{homepage}; + $backend->{regions} = [ map { $place_map{$_} // $_ } + @{ $s->{coverage}{regions} // [] } ]; + $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + $backend->{association} = 1; + + if ( + $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'Polygon' + and $self->lonlat_in_polygon( + $s->{coverage}{area}{coordinates}, + [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + } + elsif ( $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'MultiPolygon' ) + { + for my $s_poly ( + @{ $s->{coverage}{area}{coordinates} // [] } ) + { + if ( + $self->lonlat_in_polygon( + $s_poly, [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + last; + } + } + } + } + else { + $type = undef; + } + } + elsif ( $backend->{hafas} ) { + + # These backends lack a journey endpoint or are no longer + # operational and are thus useless for travelynx + if ( $backend->{name} eq 'Resrobot' + or $backend->{name} eq 'TPG' + or $backend->{name} eq 'VRN' + or $backend->{name} eq 'DB' ) + { + $type = undef; + } + + # PKP is behind a GeoIP filter. Only list it if travelynx.conf + # indicates that our IP is allowed or provides a proxy. + elsif ( + $backend->{name} eq 'PKP' + and not( $self->app->config->{hafas}{PKP}{geoip_ok} + or $self->app->config->{hafas}{PKP}{proxy} ) + ) + { + $type = undef; + } + elsif ( my $s = $self->hafas->get_service( $backend->{name} ) ) { + $type = 'HAFAS'; + $backend->{longname} = $s->{name}; + $backend->{homepage} = $s->{homepage}; + $backend->{regions} = [ map { $place_map{$_} // $_ } + @{ $s->{coverage}{regions} // [] } ]; + $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + + if ( $backend->{name} eq 'ÖBB' ) { + $backend->{recommended} = 1; + } + else { + $backend->{association} = 1; + } + + if ( + $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'Polygon' + and $self->lonlat_in_polygon( + $s->{coverage}{area}{coordinates}, + [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + } + elsif ( $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'MultiPolygon' ) + { + for my $s_poly ( + @{ $s->{coverage}{area}{coordinates} // [] } ) + { + if ( + $self->lonlat_in_polygon( + $s_poly, [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + last; + } + } + } + } + else { + $type = undef; + } + } + elsif ( $backend->{motis} ) { + my $s = $self->motis->get_service( $backend->{name} ); + + $type = 'MOTIS'; + $backend->{longname} = $s->{name}; + $backend->{homepage} = $s->{homepage}; + $backend->{regions} = [ map { $place_map{$_} // $_ } + @{ $s->{coverage}{regions} // [] } ]; + $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + $backend->{experimental} = 1; + + if ( $backend->{name} eq 'transitous' ) { + $backend->{regions} = ['Weltweit']; + } + if ( $backend->{name} eq 'RNV' ) { + $backend->{homepage} = 'https://rnv-online.de/'; + } + + if ( + $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'Polygon' + and $self->lonlat_in_polygon( + $s->{coverage}{area}{coordinates}, + [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + } + elsif ( $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'MultiPolygon' ) + { + for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } ) + { + if ( + $self->lonlat_in_polygon( + $s_poly, [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + last; + } + } + } + } + $backend->{type} = $type; + } + + @backends = map { $_->[1] } + sort { $a->[0] cmp $b->[0] } + map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends; + + $self->render( + 'select_backend', + suggestions => \@suggested_backends, + backends => \@backends, + user => $user, + redirect_to => $self->req->param('redirect_to') // '/', + ); +} + +sub change_backend { + my ($self) = @_; + + my $backend_id = $self->req->param('backend'); + my $redir = $self->req->param('redirect_to') // '/'; + + if ( $backend_id !~ m{ ^ \d+ $ }x ) { + $self->redirect_to($redir); + } + + $self->users->set_backend( + uid => $self->current_user->{id}, + backend_id => $backend_id, + ); + + $self->redirect_to($redir); +} + sub change_password { my ($self) = @_; my $old_password = $self->req->param('oldpw'); diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm index 687243d..572d3fa 100755 --- a/lib/Travelynx/Controller/Api.pm +++ b/lib/Travelynx/Controller/Api.pm @@ -21,6 +21,9 @@ sub sanitize { if ( not defined $value ) { return undef; } + if ( not defined $type ) { + return $value ? ( '' . $value ) : undef; + } if ( $type eq '' ) { return '' . $value; } @@ -51,6 +54,8 @@ sub documentation { sub get_v1 { my ($self) = @_; + $self->res->headers->access_control_allow_origin(q{*}); + my $api_action = $self->stash('user_action'); my $api_token = $self->stash('token'); if ( $api_action !~ qr{ ^ (?: status | history | action ) $ }x ) { @@ -117,6 +122,7 @@ sub travel_v1 { deprecated => \0, error => 'Malformed JSON', }, + status => 400, ); return; } @@ -130,6 +136,7 @@ sub travel_v1 { deprecated => \0, error => 'Malformed token', }, + status => 400, ); return; } @@ -143,6 +150,7 @@ sub travel_v1 { deprecated => \0, error => 'Malformed token', }, + status => 400, ); return; } @@ -155,6 +163,7 @@ sub travel_v1 { deprecated => \0, error => 'Invalid token', }, + status => 400, ); return; } @@ -169,6 +178,7 @@ sub travel_v1 { error => 'Missing or invalid action', status => $self->get_user_status_json_v1( uid => $uid ) }, + status => 400, ); return; } @@ -177,7 +187,13 @@ sub travel_v1 { my $from_station = sanitize( q{}, $payload->{fromStation} ); my $to_station = sanitize( q{}, $payload->{toStation} ); my $train_id; - my $hafas = exists $payload->{train}{journeyID} ? 1 : 0; + my $dbris = sanitize( undef, $payload->{dbris} ); + my $hafas = sanitize( undef, $payload->{hafas} ); + my $motis = sanitize( undef, $payload->{motis} ); + + if ( not $hafas and exists $payload->{train}{journeyID} ) { + $dbris //= 'bahn.de'; + } if ( not( @@ -195,11 +211,15 @@ sub travel_v1 { error => 'Missing fromStation or train data', status => $self->get_user_status_json_v1( uid => $uid ) }, + status => 400, ); return; } - if ( not $hafas and not $self->stations->search($from_station) ) { + if ( not $hafas + and not $dbris + and not $self->stations->search( $from_station, backend_id => 1 ) ) + { $self->render( json => { success => \0, @@ -207,13 +227,15 @@ sub travel_v1 { error => 'Unknown fromStation', status => $self->get_user_status_json_v1( uid => $uid ) }, + status => 400, ); return; } if ( $to_station and not $hafas - and not $self->stations->search($to_station) ) + and not $dbris + and not $self->stations->search( $to_station, backend_id => 1 ) ) { $self->render( json => { @@ -222,6 +244,7 @@ sub travel_v1 { error => 'Unknown toStation', status => $self->get_user_status_json_v1( uid => $uid ) }, + status => 400, ); return; } @@ -273,7 +296,10 @@ sub travel_v1 { return $self->checkin_p( station => $from_station, train_id => $train_id, - uid => $uid + uid => $uid, + hafas => $hafas, + dbris => $dbris, + motis => $motis, ); } )->then( @@ -518,8 +544,9 @@ sub import_v1 { $payload->{toStation}{realTime} // $payload->{toStation}{scheduledTime} ), - comment => sanitize( q{}, $payload->{comment} ), - lax => $payload->{lax} ? 1 : 0, + comment => sanitize( q{}, $payload->{comment} ), + lax => $payload->{lax} ? 1 : 0, + backend_id => 1, ); if ( $payload->{intermediateStops} @@ -558,14 +585,20 @@ sub import_v1 { my $journey; if ( not $error ) { - $journey = $self->journeys->get_single( - uid => $uid, - db => $db, - journey_id => $journey_id, - verbose => 1 - ); - $error - = $self->journeys->sanity_check( $journey, $payload->{lax} ? 1 : 0 ); + eval { + $journey = $self->journeys->get_single( + uid => $uid, + db => $db, + journey_id => $journey_id, + verbose => 1 + ); + $error + = $self->journeys->sanity_check( $journey, + $payload->{lax} ? 1 : 0 ); + }; + if ($@) { + $error = $@; + } } if ($error) { @@ -654,10 +687,14 @@ sub autocomplete { $self->res->headers->cache_control('max-age=86400, immutable'); + my $backend_id = $self->param('backend_id') // 1; + my $output = "document.addEventListener('DOMContentLoaded',function(){M.Autocomplete.init(document.querySelectorAll('.autocomplete'),{\n"; $output .= 'minLength:3,limit:50,data:'; - $output .= encode_json( $self->stations->get_for_autocomplete ); + $output + .= encode_json( + $self->stations->get_for_autocomplete( backend_id => $backend_id ) ); $output .= "\n});});\n"; $self->render( diff --git a/lib/Travelynx/Controller/Passengerrights.pm b/lib/Travelynx/Controller/Passengerrights.pm index d80f1ae..5759d2e 100644 --- a/lib/Travelynx/Controller/Passengerrights.pm +++ b/lib/Travelynx/Controller/Passengerrights.pm @@ -121,6 +121,8 @@ sub list_candidates { } } + my @abo_journeys + = grep { $_->{delay} >= 20 and $_->{delay} < 60 } @journeys; @journeys = grep { $_->{delay} >= 60 or $_->{connection_missed} } @journeys; my @cancelled = $self->journeys->get( @@ -154,8 +156,9 @@ sub list_candidates { $self->respond_to( json => { json => [@journeys] }, any => { - template => 'passengerrights', - journeys => [@journeys] + template => 'passengerrights', + journeys => [@journeys], + abo_journeys => [@abo_journeys] } ); } diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm index fc2d38c..db30d36 100755 --- a/lib/Travelynx/Controller/Profile.pm +++ b/lib/Travelynx/Controller/Profile.pm @@ -111,6 +111,13 @@ sub profile { $status->{arr_name} = undef; } + my $map_data = {}; + if ( $status->{checked_in} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } + my @journeys; if ( @@ -152,34 +159,47 @@ sub profile { @journeys = $self->journeys->get(%opt); } - $self->render( - 'profile', - title => "travelynx: $name", - name => $name, - uid => $user->{id}, - privacy => $user, - bio => $profile->{bio}{html}, - metadata => $profile->{metadata}, - is_self => $is_self, - following => ( $relation and $relation eq 'follows' ) ? 1 : 0, - follow_requested => ( $relation and $relation eq 'requests_follow' ) - ? 1 - : 0, - can_follow => ( $my_user and $user->{accept_follows} and not $relation ) - ? 1 - : 0, - can_request_follow => - ( $my_user and $user->{accept_follow_requests} and not $relation ) - ? 1 - : 0, - follows_me => ( $inverse_relation and $inverse_relation eq 'follows' ) - ? 1 - : 0, - follow_reqs_me => - ( $inverse_relation and $inverse_relation eq 'requests_follow' ) ? 1 - : 0, - journey => $status, - journeys => [@journeys], + $self->respond_to( + json => { + json => { + name => $name, + uid => $user->{id}, + bio => $profile->{bio}{html}, + metadata => $profile->{metadata}, + } + }, + any => { + template => 'profile', + title => "travelynx: $name", + name => $name, + uid => $user->{id}, + privacy => $user, + bio => $profile->{bio}{html}, + metadata => $profile->{metadata}, + is_self => $is_self, + following => ( $relation and $relation eq 'follows' ) ? 1 : 0, + follow_requested => ( $relation and $relation eq 'requests_follow' ) + ? 1 + : 0, + can_follow => + ( $my_user and $user->{accept_follows} and not $relation ) ? 1 + : 0, + can_request_follow => ( + $my_user and $user->{accept_follow_requests} and not $relation + ) ? 1 + : 0, + follows_me => + ( $inverse_relation and $inverse_relation eq 'follows' ) ? 1 + : 0, + follow_reqs_me => ( + $inverse_relation and $inverse_relation eq 'requests_follow' + ) ? 1 + : 0, + journey => $status, + journeys => [@journeys], + with_map => 1, + %{$map_data}, + } ); } @@ -220,12 +240,13 @@ sub journey_details { } my $journey = $self->journeys->get_single( - uid => $user->{id}, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, - with_polyline => 1, - with_visibility => 1, + uid => $user->{id}, + journey_id => $journey_id, + verbose => 1, + with_datetime => 1, + with_route_datetime => 1, + with_polyline => 1, + with_visibility => 1, ); if ( not $journey ) { @@ -482,6 +503,13 @@ sub user_status { $og_data{description} = $tw_data{description} = q{}; } + my $map_data = {}; + if ( $status->{checked_in} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } + $self->respond_to( json => { json => { @@ -504,7 +532,9 @@ sub user_status { journey => $status, twitter => \%tw_data, opengraph => \%og_data, - version => $self->app->config->{version} // 'UNKNOWN', + with_map => 1, + %{$map_data}, + version => $self->app->config->{version} // 'UNKNOWN', }, ); } @@ -543,6 +573,7 @@ sub status_card { my $status = $self->get_user_status( $user->{id} ); my $visibility; + my $map_data = {}; if ( $status->{checked_in} or $status->{arr_name} ) { my $visibility = $status->{effective_visibility}; if ( @@ -567,12 +598,19 @@ sub status_card { $status->{arr_name} = undef; } + if ( $status->{checked_in} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } + $self->render( '_public_status_card', name => $name, privacy => $user, journey => $status, from_profile => $self->param('profile') ? 1 : 0, + %{$map_data}, ); } diff --git a/lib/Travelynx/Controller/Static.pm b/lib/Travelynx/Controller/Static.pm index 04c2d0f..bcd6fda 100644 --- a/lib/Travelynx/Controller/Static.pm +++ b/lib/Travelynx/Controller/Static.pm @@ -35,4 +35,10 @@ sub offline { $self->render('offline'); } +sub tos { + my ($self) = @_; + + $self->render('terms-of-service'); +} + 1; diff --git a/lib/Travelynx/Controller/Traewelling.pm b/lib/Travelynx/Controller/Traewelling.pm index 3cdeff8..6aa789c 100644 --- a/lib/Travelynx/Controller/Traewelling.pm +++ b/lib/Travelynx/Controller/Traewelling.pm @@ -29,7 +29,7 @@ sub oauth { redirect_uri => $self->base_url_for('/oauth/traewelling')->to_abs->scheme( $self->app->mode eq 'development' ? 'http' : 'https' - )->to_string, + )->to_string, scope => 'read-statuses write-statuses' } )->then( diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 89385e1..fd2abb1 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -1,6 +1,7 @@ package Travelynx::Controller::Traveling; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Controller'; @@ -10,6 +11,7 @@ use DateTime::Format::Strptime; use List::Util qw(uniq min max); use List::UtilsBy qw(max_by uniq_by); use List::MoreUtils qw(first_index); +use Mojo::UserAgent; use Mojo::Promise; use Text::CSV; use Travel::Status::DE::IRIS::Stations; @@ -24,10 +26,15 @@ sub has_str_in_list { return; } +# when called with "eva" provided: look up connections from eva, either +# for provided backend_id / hafas or (if not provided) for user backend id. +# When calld without "eva": look up connections from current/latest arrival +# eva, using the checkin's backend id. sub get_connecting_trains_p { my ( $self, %opt ) = @_; - my $uid = $opt{uid} //= $self->current_user->{id}; + my $user = $self->current_user; + my $uid = $opt{uid} //= $user->{id}; my $use_history = $self->users->use_history( uid => $uid ); my ( $eva, $exclude_via, $exclude_train_id, $exclude_before ); @@ -36,6 +43,23 @@ sub get_connecting_trains_p { my $promise = Mojo::Promise->new; + if ( $user->{backend_dbris} ) { + + # We do get a little bit of via information, so this might work in some + # cases. But not reliably. Probably best to leave it out entirely then. + return $promise->reject; + } + if ( $user->{backend_efa} ) { + + # TODO + return $promise->reject; + } + if ( $user->{backend_motis} ) { + + # FIXME: The following code can't handle external_ids currently + return $promise->reject; + } + if ( $opt{eva} ) { if ( $use_history & 0x01 ) { $eva = $opt{eva}; @@ -43,10 +67,20 @@ sub get_connecting_trains_p { elsif ( $opt{destination_name} ) { $eva = $opt{eva}; } + if ( not defined $opt{backend_id} ) { + if ( $opt{hafas} ) { + $opt{backend_id} + = $self->stations->get_backend_id( hafas => $opt{hafas} ); + } + else { + $opt{backend_id} = $user->{backend_id}; + } + } } else { if ( $use_history & 0x02 ) { my $status = $self->get_user_status; + $opt{backend_id} = $status->{backend_id}; $eva = $status->{arr_eva}; $exclude_via = $status->{dep_name}; $exclude_train_id = $status->{train_id}; @@ -65,10 +99,12 @@ sub get_connecting_trains_p { return $promise->reject; } - my ( $dest_ids, $destinations ) - = $self->journeys->get_connection_targets(%opt); + $self->log->debug( + "get_connecting_trains_p(backend_id => $opt{backend_id}, eva => $eva)"); - my @destinations = uniq_by { $_->{name} } @{$destinations}; + my @destinations = $self->journeys->get_connection_targets(%opt); + + @destinations = uniq_by { $_->{name} } @destinations; if ($exclude_via) { @destinations = grep { $_->{name} ne $exclude_via } @destinations; @@ -78,11 +114,8 @@ sub get_connecting_trains_p { return $promise->reject; } - my $iris_eva = $eva; - if ( $eva < 8000000 ) { - $iris_eva = ( List::Util::first { $_ >= 8000000 } @{$dest_ids} ) - // $eva; - } + $self->log->debug( 'get_connection_targets returned ' + . join( q{, }, map { $_->{name} } @destinations ) ); my $can_check_in = not $arr_epoch || ( $arr_countdown // 1 ) < 0; my $lookahead @@ -91,11 +124,11 @@ sub get_connecting_trains_p { my $iris_promise = Mojo::Promise->new; my %via_count = map { $_->{name} => 0 } @destinations; - if ( $iris_eva >= 8000000 - and List::Util::any { $_->{eva} >= 8000000 } @destinations ) - { + my $backend + = $self->stations->get_backend( backend_id => $opt{backend_id} ); + if ( $opt{backend_id} == 0 ) { $self->iris->get_departures_p( - station => $iris_eva, + station => $eva, lookbehind => 10, lookahead => $lookahead, with_related => 1 @@ -103,7 +136,7 @@ sub get_connecting_trains_p { sub { my ($stationboard) = @_; if ( $stationboard->{errstr} ) { - $iris_promise->resolve( [] ); + $promise->resolve( [], [] ); return; } @@ -237,105 +270,35 @@ sub get_connecting_trains_p { } } - $iris_promise->resolve( [ @results, @cancellations ] ); + $promise->resolve( [ @results, @cancellations ], [] ); return; } )->catch( sub { - $iris_promise->resolve( [] ); + $promise->resolve( [], [] ); return; } )->wait; } - else { - $iris_promise->resolve( [] ); + elsif ( $backend->{dbris} ) { + return $promise->reject; } - - my $hafas_promise = Mojo::Promise->new; - $self->hafas->get_departures_p( - eva => $eva, - lookbehind => 10, - lookahead => $lookahead - )->then( - sub { - my ($status) = @_; - $hafas_promise->resolve( [ $status->results ] ); - return; - } - )->catch( - sub { - # HAFAS data is optional. - # Errors are logged by get_json_p and can be silently ignored here. - $hafas_promise->resolve( [] ); - return; - } - )->wait; - - Mojo::Promise->all( $iris_promise, $hafas_promise )->then( - sub { - my ( $iris, $hafas ) = @_; - my @iris_trains = @{ $iris->[0] }; - my @all_hafas_trains = @{ $hafas->[0] }; - my @hafas_trains; - - # We've already got a list of connecting trains; this function - # only adds further information to them. We ignore errors, as - # partial data is better than no data. - eval { - for my $iris_train (@iris_trains) { - if ( $iris_train->[0]->departure_is_cancelled ) { - for my $hafas_train (@all_hafas_trains) { - if ( $hafas_train->number - and $hafas_train->number - == $iris_train->[0]->train_no ) - { - $hafas_train->{iris_seen} = 1; - next; - } - } - next; - } - for my $hafas_train (@all_hafas_trains) { - if ( $hafas_train->number - and $hafas_train->number - == $iris_train->[0]->train_no ) - { - $hafas_train->{iris_seen} = 1; - if ( $hafas_train->load - and $hafas_train->load->{SECOND} ) - { - $iris_train->[3] = $hafas_train->load; - } - for my $stop ( $hafas_train->route ) { - if ( $stop->loc->name - and $stop->loc->name eq - $iris_train->[1]->{name} - and $stop->arr ) - { - $iris_train->[2] = $stop->arr; - if ( $iris_train->[0]->departure_delay - and not $stop->arr_delay ) - { - $iris_train->[2] - ->add( minutes => $iris_train->[0] - ->departure_delay ); - } - last; - } - } - last; - } - } - } + elsif ( $backend->{efa} ) { + return $promise->reject; + } + elsif ( $backend->{hafas} ) { + my $hafas_service = $backend->{name}; + $self->hafas->get_departures_p( + service => $hafas_service, + eva => $eva, + lookbehind => 10, + lookahead => $lookahead + )->then( + sub { + my ($status) = @_; + my @hafas_trains; + my @all_hafas_trains = $status->results; for my $hafas_train (@all_hafas_trains) { - if ( $hafas_train->{iris_seen} ) { - next; - } - if ( $hafas_train->station_eva >= 8000000 ) { - - # better safe than sorry, for now - next; - } for my $stop ( $hafas_train->route ) { for my $dest (@destinations) { if ( $stop->loc->name @@ -353,30 +316,30 @@ sub get_connecting_trains_p { } if ( $departure->epoch >= $exclude_before ) { $via_count{ $dest->{name} }++; - push( @hafas_trains, - [ $hafas_train, $dest, $arrival ] ); + push( + @hafas_trains, + [ + $hafas_train, $dest, + $arrival, $hafas_service + ] + ); } } } } } - }; - if ($@) { - $self->app->log->error( - "get_connecting_trains_p($uid): IRIS/HAFAS merge failed: $@" - ); + $promise->resolve( [], \@hafas_trains ); + return; } - - $promise->resolve( \@iris_trains, \@hafas_trains ); - return; - } - )->catch( - sub { - my ($err) = @_; - $promise->reject($err); - return; - } - )->wait; + )->catch( + sub { + my ($err) = @_; + $self->log->debug("get_connection_trains: hafas: $err"); + $promise->resolve( [], [] ); + return; + } + )->wait; + } return $promise; } @@ -394,7 +357,8 @@ sub compute_effective_visibility { sub homepage { my ($self) = @_; if ( $self->is_user_authenticated ) { - my $uid = $self->current_user->{id}; + my $user = $self->current_user; + my $uid = $user->{id}; my $status = $self->get_user_status; my @timeline = $self->in_transit->get_timeline( uid => $uid, @@ -403,9 +367,15 @@ sub homepage { $self->stash( timeline => [@timeline] ); my @recent_targets; if ( $status->{checked_in} ) { + my $map_data = {}; + if ( $status->{arr_name} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } my $journey_visibility = $self->compute_effective_visibility( - $self->current_user->{default_visibility_str}, + $user->{default_visibility_str}, $status->{visibility_str} ); if ( defined $status->{arrival_countdown} and $status->{arrival_countdown} < ( 40 * 60 ) ) @@ -416,10 +386,13 @@ sub homepage { my ( $connections_iris, $connections_hafas ) = @_; $self->render( 'landingpage', + user => $user, user_status => $status, journey_visibility => $journey_visibility, connections_iris => $connections_iris, connections_hafas => $connections_hafas, + with_map => 1, + %{$map_data}, ); $self->users->mark_seen( uid => $uid ); } @@ -427,8 +400,11 @@ sub homepage { sub { $self->render( 'landingpage', + user => $user, user_status => $status, journey_visibility => $journey_visibility, + with_map => 1, + %{$map_data}, ); $self->users->mark_seen( uid => $uid ); } @@ -438,23 +414,28 @@ sub homepage { else { $self->render( 'landingpage', + user => $user, user_status => $status, journey_visibility => $journey_visibility, + with_map => 1, + %{$map_data}, ); $self->users->mark_seen( uid => $uid ); return; } } else { - @recent_targets = uniq_by { $_->{eva} } + @recent_targets = uniq_by { $_->{external_id_or_eva} } $self->journeys->get_latest_checkout_stations( uid => $uid ); } $self->render( 'landingpage', + user => $user, user_status => $status, recent_targets => \@recent_targets, with_autocomplete => 1, - with_geolocation => 1 + with_geolocation => 1, + backend_id => $user->{backend_id}, ); $self->users->mark_seen( uid => $uid ); } @@ -476,6 +457,12 @@ sub status_card { $self->stash( timeline => [@timeline] ); if ( $status->{checked_in} ) { + my $map_data = {}; + if ( $status->{arr_name} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } my $journey_visibility = $self->compute_effective_visibility( $self->current_user->{default_visibility_str}, @@ -493,6 +480,7 @@ sub status_card { journey_visibility => $journey_visibility, connections_iris => $connections_iris, connections_hafas => $connections_hafas, + %{$map_data}, ); } )->catch( @@ -501,6 +489,7 @@ sub status_card { '_checked_in', journey => $status, journey_visibility => $journey_visibility, + %{$map_data}, ); } )->wait; @@ -510,11 +499,13 @@ sub status_card { '_checked_in', journey => $status, journey_visibility => $journey_visibility, + %{$map_data}, ); } elsif ( $status->{cancellation} ) { $self->render_later; $self->get_connecting_trains_p( + backend_id => $status->{backend_id}, eva => $status->{cancellation}{dep_eva}, destination_name => $status->{cancellation}{arr_name} )->then( @@ -563,14 +554,225 @@ sub status_card { sub geolocation { my ($self) = @_; - my $lon = $self->param('lon'); - my $lat = $self->param('lat'); + my $lon = $self->param('lon'); + my $lat = $self->param('lat'); + my $backend_id = $self->param('backend') // 0; if ( not $lon or not $lat ) { - $self->render( json => { error => 'Invalid lon/lat received' } ); + $self->render( + json => { error => "Invalid lon/lat (${lon}/${lat}) received" } ); + return; + } + + if ( $backend_id !~ m{ ^ \d+ $ }x ) { + $self->render( + json => { error => "Invalid backend (${backend_id}) received" } ); + return; + } + + my ( $dbris_service, $efa_service, $hafas_service, $motis_service ); + my $backend = $self->stations->get_backend( backend_id => $backend_id ); + if ( $backend->{dbris} ) { + $dbris_service = $backend->{name}; + } + if ( $backend->{efa} ) { + $efa_service = $backend->{name}; + } + elsif ( $backend->{hafas} ) { + $hafas_service = $backend->{name}; + } + elsif ( $backend->{motis} ) { + $motis_service = $backend->{name}; + } + + if ($dbris_service) { + $self->render_later; + + Travel::Status::DE::DBRIS->new_p( + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + geoSearch => { + latitude => $lat, + longitude => $lon + } + )->then( + sub { + my ($dbris) = @_; + my @results = map { + { + name => $_->name, + eva => $_->eva, + distance => 0, + dbris => $dbris_service, + } + } $dbris->results; + if ( @results > 10 ) { + @results = @results[ 0 .. 9 ]; + } + $self->render( + json => { + candidates => [@results], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + return; + } + elsif ($efa_service) { + $self->render_later; + + Travel::Status::DE::EFA->new_p( + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + service => $efa_service, + coord => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($efa) = @_; + my @results = map { + { + name => $_->full_name, + eva => $_->id_code, + distance => 0, + efa => $efa_service, + } + } $efa->results; + if ( @results > 10 ) { + @results = @results[ 0 .. 9 ]; + } + $self->render( + json => { + candidates => [@results], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + return; + } + elsif ($hafas_service) { + $self->render_later; + + my $agent = $self->ua; + if ( my $proxy = $self->app->config->{hafas}{$hafas_service}{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + + Travel::Status::DE::HAFAS->new_p( + promise => 'Mojo::Promise', + user_agent => $agent, + service => $hafas_service, + geoSearch => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($hafas) = @_; + my @hafas = map { + { + name => $_->name, + eva => $_->eva, + distance => $_->distance_m / 1000, + hafas => $hafas_service + } + } $hafas->results; + if ( @hafas > 10 ) { + @hafas = @hafas[ 0 .. 9 ]; + } + $self->render( + json => { + candidates => [@hafas], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + + return; + } + elsif ($motis_service) { + $self->render_later; + + Travel::Status::MOTIS->new_p( + promise => 'Mojo::Promise', + user_agent => $self->ua, + time_zone => 'Europe/Berlin', + + service => $motis_service, + stops_by_coordinate => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($motis) = @_; + my @motis = map { + { + id => $_->id, + name => $_->name, + distance => 0, + motis => $motis_service, + } + } $motis->results; + + if ( @motis > 10 ) { + @motis = @motis[ 0 .. 9 ]; + } + + $self->render( + json => { + candidates => [@motis], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + return; } - $self->render_later; my @iris = map { { @@ -588,48 +790,12 @@ sub geolocation { if ( @iris > 5 ) { @iris = @iris[ 0 .. 4 ]; } - - Travel::Status::DE::HAFAS->new_p( - promise => 'Mojo::Promise', - user_agent => $self->ua, - geoSearch => { - lat => $lat, - lon => $lon - } - )->then( - sub { - my ($hafas) = @_; - my @hafas = map { - { - name => $_->name, - eva => $_->eva, - distance => $_->distance_m / 1000, - hafas => 1 - } - } $hafas->results; - if ( @hafas > 10 ) { - @hafas = @hafas[ 0 .. 9 ]; - } - my @results = map { $_->[0] } - sort { $a->[1] <=> $b->[1] } - map { [ $_, $_->{distance} ] } ( @iris, @hafas ); - $self->render( - json => { - candidates => [@results], - } - ); - } - )->catch( - sub { - my ($err) = @_; - $self->render( - json => { - candidates => [@iris], - warning => $err, - } - ); + $self->render( + json => { + candidates => [@iris], } - )->wait; + ); + } sub travel_action { @@ -684,8 +850,14 @@ sub travel_action { $promise->then( sub { return $self->checkin_p( - station => $params->{station}, - train_id => $params->{train} + dbris => $params->{dbris}, + efa => $params->{efa}, + hafas => $params->{hafas}, + motis => $params->{motis}, + station => $params->{station}, + train_id => $params->{train}, + train_suffix => $params->{suffix}, + ts => $params->{ts}, ); } )->then( @@ -713,8 +885,14 @@ sub travel_action { my ( $still_checked_in, undef ) = @_; if ( my $destination = $params->{dest} ) { my $station_link = '/s/' . $destination; - if ( $status->{train_id} =~ m{[|]} ) { - $station_link .= '?hafas=1'; + if ( $status->{is_dbris} ) { + $station_link .= '?dbris=' . $status->{backend_name}; + } + elsif ( $status->{is_efa} ) { + $station_link .= '?efa=' . $status->{backend_name}; + } + elsif ( $status->{is_hafas} ) { + $station_link .= '?hafas=' . $status->{backend_name}; } $self->render( json => { @@ -749,8 +927,14 @@ sub travel_action { sub { my ( $still_checked_in, $error ) = @_; my $station_link = '/s/' . $params->{station}; - if ( $status->{train_id} =~ m{[|]} ) { - $station_link .= '?hafas=1'; + if ( $status->{is_dbris} ) { + $station_link .= '?dbris=' . $status->{backend_name}; + } + elsif ( $status->{is_efa} ) { + $station_link .= '?efa=' . $status->{backend_name}; + } + elsif ( $status->{is_hafas} ) { + $station_link .= '?hafas=' . $status->{backend_name}; } if ($error) { @@ -800,8 +984,32 @@ sub travel_action { else { my $redir = '/'; if ( $status->{checked_in} or $status->{cancelled} ) { - if ( $status->{train_id} =~ m{[|]} ) { - $redir = '/s/' . $status->{dep_eva} . '?hafas=1'; + if ( $status->{is_dbris} ) { + $redir + = '/s/' + . $status->{dep_eva} + . '?dbris=' + . $status->{backend_name}; + } + elsif ( $status->{is_efa} ) { + $redir + = '/s/' + . $status->{dep_eva} . '?efa=' + . $status->{backend_name}; + } + elsif ( $status->{is_hafas} ) { + $redir + = '/s/' + . $status->{dep_eva} + . '?hafas=' + . $status->{backend_name}; + } + elsif ( $status->{is_motis} ) { + $redir + = '/s/' + . $status->{dep_external_id} + . '?motis=' + . $status->{backend_name}; } else { $redir = '/s/' . $status->{dep_ds100}; @@ -818,8 +1026,13 @@ sub travel_action { elsif ( $params->{action} eq 'cancelled_from' ) { $self->render_later; $self->checkin_p( + dbris => $params->{dbris}, + efa => $params->{efa}, + hafas => $params->{hafas}, + motis => $params->{motis}, station => $params->{station}, - train_id => $params->{train} + train_id => $params->{train}, + ts => $params->{ts}, )->then( sub { $self->render( @@ -920,7 +1133,8 @@ sub station { my $train = $self->param('train'); my $trip_id = $self->param('trip_id'); my $timestamp = $self->param('timestamp'); - my $uid = $self->current_user->{id}; + my $user = $self->current_user; + my $uid = $user->{id}; my @timeline = $self->in_transit->get_timeline( uid => $uid, @@ -928,7 +1142,6 @@ sub station { ); my %checkin_by_train; for my $checkin (@timeline) { - say $checkin->{train_id}; push( @{ $checkin_by_train{ $checkin->{train_id} } }, $checkin ); } $self->stash( checkin_by_train => \%checkin_by_train ); @@ -945,16 +1158,105 @@ sub station { $timestamp = DateTime->now( time_zone => 'Europe/Berlin' ); } - my $use_hafas = $self->param('hafas'); + my ( $dbris_service, $efa_service, $hafas_service, $motis_service ); + + if ( $self->param('dbris') ) { + $dbris_service = $self->param('dbris'); + } + elsif ( $self->param('efa') ) { + $efa_service = $self->param('efa'); + } + elsif ( $self->param('hafas') ) { + $hafas_service = $self->param('hafas'); + } + elsif ( $self->param('motis') ) { + $motis_service = $self->param('motis'); + } + else { + if ( $user->{backend_dbris} ) { + $dbris_service = $user->{backend_name}; + } + elsif ( $user->{backend_efa} ) { + $efa_service = $user->{backend_name}; + } + elsif ( $user->{backend_hafas} ) { + $hafas_service = $user->{backend_name}; + } + elsif ( $user->{backend_motis} ) { + $motis_service = $user->{backend_name}; + } + } + my $promise; - if ($use_hafas) { + if ($dbris_service) { + if ( $station !~ m{ [@] L = \d+ }x ) { + $self->render_later; + $self->dbris->get_station_id_p($station)->then( + sub { + my ($dbris_station) = @_; + $self->redirect_to( '/s/' . $dbris_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + $self->redirect_to('/'); + } + )->wait; + return; + } + $promise = $self->dbris->get_departures_p( + station => $station, + timestamp => $timestamp, + lookbehind => 30, + ); + } + elsif ($efa_service) { + $promise = $self->efa->get_departures_p( + service => $efa_service, + name => $station, + timestamp => $timestamp, + lookbehind => 10, + lookahead => 50, + ); + } + elsif ($hafas_service) { $promise = $self->hafas->get_departures_p( + service => $hafas_service, eva => $station, timestamp => $timestamp, lookbehind => 30, lookahead => 30, ); } + elsif ($motis_service) { + if ( $station !~ m/.*_.*/ ) { + $self->render_later; + $self->motis->get_station_by_query_p( + service => $motis_service, + query => $station, + )->then( + sub { + my ($motis_station) = @_; + $self->redirect_to( '/s/' . $motis_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + say "$err"; + + $self->redirect_to('/'); + } + )->wait; + return; + } + $promise = $self->motis->get_departures_p( + service => $motis_service, + station_id => $station, + timestamp => $timestamp, + lookbehind => 30, + lookahead => 30, + ); + } else { $promise = $self->iris->get_departures_p( station => $station, @@ -966,28 +1268,39 @@ sub station { $promise->then( sub { my ($status) = @_; - my $api_link; my @results; my $now = $self->now->epoch; my $now_within_range = abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0; - if ($use_hafas) { + if ($dbris_service) { + + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->dep->epoch ] } $status->results; + + $status = { + station_eva => $station, + related_stations => [], + }; - my $iris_eva = List::Util::min grep { $_ >= 1000000 } - @{ $status->station->{evas} // [] }; - if ($iris_eva) { - $api_link = '/s/' . $iris_eva; + if ( $station =~ m{ [@] O = (?<name> [^@]+ ) [@] }x ) { + $status->{station_name} = $+{name}; } + } + elsif ($hafas_service) { @results = map { $_->[0] } sort { $b->[1] <=> $a->[1] } map { [ $_, $_->datetime->epoch ] } $status->results; - $self->stations->add_meta( - eva => $status->station->{eva}, - meta => $status->station->{evas} // [] - ); + if ( $status->station->{eva} ) { + $self->stations->add_meta( + eva => $status->station->{eva}, + meta => $status->station->{evas} // [], + hafas => $hafas_service, + ); + } $status = { station_eva => $status->station->{eva}, station_name => ( @@ -997,9 +1310,30 @@ sub station { related_stations => [], }; } - else { + elsif ($efa_service) { + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->datetime->epoch ] } $status->results; + $status = { + station_eva => $status->stop->id_num, + station_name => $status->stop->full_name, + related_stations => [], + }; + } + elsif ($motis_service) { + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->stopover->departure->epoch ] } + $status->results; - $api_link = '/s/' . $status->{station_eva} . '?hafas=1'; + $status = { + station_eva => $station, + station_name => + $status->{results}->[0]->stopover->stop->name, + related_stations => [], + }; + } + else { # You can't check into a train which terminates here @results = grep { $_->departure } @{ $status->{results} }; @@ -1029,10 +1363,10 @@ sub station { } my $connections_p; - if ( $trip_id and $use_hafas ) { + if ( $trip_id and ( $dbris_service or $hafas_service ) ) { @results = grep { $_->id eq $trip_id } @results; } - elsif ( $train and not $use_hafas ) { + elsif ( $train and not $hafas_service ) { @results = grep { $_->type . ' ' . $_->train_no eq $train } @results; } @@ -1044,12 +1378,17 @@ sub station { $connections_p = $self->get_connecting_trains_p( eva => $user_status->{cancellation}{dep_eva}, destination_name => - $user_status->{cancellation}{arr_name} + $user_status->{cancellation}{arr_name}, + efa => $efa_service, + hafas => $hafas_service, ); } else { $connections_p = $self->get_connecting_trains_p( - eva => $status->{station_eva} ); + eva => $status->{station_eva}, + efa => $efa_service, + hafas => $hafas_service + ); } } @@ -1059,18 +1398,21 @@ sub station { my ( $connections_iris, $connections_hafas ) = @_; $self->render( 'departures', + user => $user, + dbris => $dbris_service, + efa => $efa_service, + hafas => $hafas_service, + motis => $motis_service, eva => $status->{station_eva}, datetime => $timestamp, now_in_range => $now_within_range, results => \@results, - hafas => $use_hafas, station => $status->{station_name}, related_stations => $status->{related_stations}, user_status => $user_status, can_check_out => $can_check_out, connections_iris => $connections_iris, connections_hafas => $connections_hafas, - api_link => $api_link, title => "travelynx: $status->{station_name}", ); } @@ -1078,16 +1420,19 @@ sub station { sub { $self->render( 'departures', + user => $user, + dbris => $dbris_service, + efa => $efa_service, + hafas => $hafas_service, + motis => $motis_service, eva => $status->{station_eva}, datetime => $timestamp, now_in_range => $now_within_range, results => \@results, - hafas => $use_hafas, station => $status->{station_name}, related_stations => $status->{related_stations}, user_status => $user_status, can_check_out => $can_check_out, - api_link => $api_link, title => "travelynx: $status->{station_name}", ); } @@ -1096,16 +1441,19 @@ sub station { else { $self->render( 'departures', + user => $user, + dbris => $dbris_service, + efa => $efa_service, + hafas => $hafas_service, + motis => $motis_service, eva => $status->{station_eva}, datetime => $timestamp, now_in_range => $now_within_range, results => \@results, - hafas => $use_hafas, station => $status->{station_name}, related_stations => $status->{related_stations}, user_status => $user_status, can_check_out => $can_check_out, - api_link => $api_link, title => "travelynx: $status->{station_name}", ); } @@ -1120,15 +1468,35 @@ sub station { status => 300, ); } - elsif ( $use_hafas and $status and $status->errcode eq 'LOCATION' ) + elsif ( $efa_service + and $status + and scalar $status->name_candidates ) + { + $self->render( + 'disambiguation', + suggestions => [ + map { { name => $_->name, eva => $_->id_num } } + $status->name_candidates + ], + status => 300, + ); + } + elsif ( $hafas_service + and $status + and $status->errcode eq 'LOCATION' ) { - $self->hafas->search_location_p( query => $station )->then( + $self->hafas->search_location_p( + service => $hafas_service, + query => $station + )->then( sub { my ($hafas2) = @_; my @suggestions = $hafas2->results; if ( @suggestions == 1 ) { - $self->redirect_to( - '/s/' . $suggestions[0]->eva . '?hafas=1' ); + $self->redirect_to( '/s/' + . $suggestions[0]->eva + . '?hafas=' + . $hafas_service ); } else { $self->render( @@ -1153,11 +1521,30 @@ sub station { } )->wait; } + elsif ( $err + =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden|HTTP 500 Internal Server Error} + ) + { + $self->render( + 'bad_gateway', + message => $err, + status => 502, + select_new_backend => 1, + ); + } + elsif ( $err =~ m{timeout}i ) { + $self->render( + 'gateway_timeout', + message => $err, + status => 504, + select_new_backend => 1, + ); + } else { $self->render( 'exception', exception => $err, - status => 502 + status => 500 ); } } @@ -1169,25 +1556,49 @@ sub redirect_to_station { my ($self) = @_; my $station = $self->param('station'); - if ( my $s = $self->app->stations->search($station) ) { - if ( $s->{source} == 1 ) { - $self->redirect_to("/s/${station}?hafas=1"); - } - else { - $self->redirect_to("/s/${station}"); - } + if ( $self->param('backend_dbris') ) { + $self->render_later; + $self->dbris->get_station_id_p($station)->then( + sub { + my ($dbris_station) = @_; + $self->redirect_to( '/s/' . $dbris_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + $self->redirect_to('/'); + } + )->wait; + } + elsif ( $self->param('backend_motis') ) { + $self->render_later; + $self->motis->get_station_by_query( + service => $self->param('backend_motis'), + query => $station, + )->then( + sub { + my ($motis_station) = @_; + $self->redirect_to( '/s/' . $motis_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + $self->redirect_to('/'); + } + )->wait; } else { - $self->redirect_to("/s/${station}?hafas=1"); + $self->redirect_to("/s/${station}"); } } sub cancelled { my ($self) = @_; my @journeys = $self->journeys->get( - uid => $self->current_user->{id}, - cancelled => 1, - with_datetime => 1 + uid => $self->current_user->{id}, + cancelled => 1, + with_datetime => 1, + with_route_datetime => 1 ); $self->respond_to( @@ -1323,8 +1734,6 @@ sub commute { sub map_history { my ($self) = @_; - my $location = $self->app->coordinates_by_station; - if ( not $self->param('route_type') ) { $self->param( route_type => 'polybee' ); } @@ -1430,15 +1839,19 @@ sub csv_history { my $buf = q{}; $csv->combine( - qw(Zugtyp Linie Nummer Start Ziel), - 'Start (DS100)', - 'Ziel (DS100)', - 'Abfahrt (soll)', - 'Abfahrt (ist)', - 'Ankunft (soll)', - 'Ankunft (ist)', - 'Kommentar', - 'ID' + qw(type line number), + 'departure stop name', + 'departure stop id', + 'arrival stop name', + 'arrival stop id', + 'scheduled departure', + 'real-time departure', + 'scheduled arrival', + 'real-time arrival', + 'operator', + 'carriage type', + 'comment', + 'id' ); $buf .= $csv->string; @@ -1455,13 +1868,17 @@ sub csv_history { $journey->{line}, $journey->{no}, $journey->{from_name}, + $journey->{from_eva}, $journey->{to_name}, - $journey->{from_ds100}, - $journey->{to_ds100}, - $journey->{sched_departure}->strftime('%Y-%m-%d %H:%M'), - $journey->{rt_departure}->strftime('%Y-%m-%d %H:%M'), - $journey->{sched_arrival}->strftime('%Y-%m-%d %H:%M'), - $journey->{rt_arrival}->strftime('%Y-%m-%d %H:%M'), + $journey->{to_eva}, + $journey->{sched_departure}->strftime('%Y-%m-%d %H:%M:%S'), + $journey->{rt_departure}->strftime('%Y-%m-%d %H:%M:%S'), + $journey->{sched_arrival}->strftime('%Y-%m-%d %H:%M:%S'), + $journey->{rt_arrival}->strftime('%Y-%m-%d %H:%M:%S'), + $journey->{user_data}{operator} // q{}, + join( q{ + }, + map { $_->{desc} // $_->{name} } + @{ $journey->{user_data}{wagongroups} // [] } ), $journey->{user_data}{comment} // q{}, $journey->{id} ) @@ -1510,7 +1927,7 @@ sub year_in_review { if ( not @journeys ) { $self->render( 'not_found', - message => 'Keine Zugfahrten im angefragten Jahr gefunden.', + message => 'Keine Fahrten im angefragten Jahr gefunden.', status => 404 ); return; @@ -1583,7 +2000,7 @@ sub yearly_history { $self->render( 'not_found', status => 404, - message => 'Keine Zugfahrten im angefragten Jahr gefunden.' + message => 'Keine Fahrten im angefragten Jahr gefunden.' ); return; } @@ -1660,7 +2077,7 @@ sub monthly_history { if ( not @journeys ) { $self->render( 'not_found', - message => 'Keine Zugfahrten im angefragten Monat gefunden.', + message => 'Keine Fahrten im angefragten Monat gefunden.', status => 404 ); return; @@ -1682,13 +2099,15 @@ sub monthly_history { } }, any => { - template => 'history_by_month', - title => "travelynx: $month_name $year", - journeys => [@journeys], - year => $year, - month => $month, - month_name => $month_name, - statistics => $stats + template => 'history_by_month', + title => "travelynx: $month_name $year", + journeys => [@journeys], + year => $year, + month => $month, + month_name => $month_name, + filter_from => $interval_start, + filter_to => $interval_end->clone->subtract( days => 1 ), + statistics => $stats } ); @@ -1714,12 +2133,13 @@ sub journey_details { } my $journey = $self->journeys->get_single( - uid => $uid, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, - with_polyline => 1, - with_visibility => 1, + uid => $uid, + journey_id => $journey_id, + verbose => 1, + with_datetime => 1, + with_route_datetime => 1, + with_polyline => 1, + with_visibility => 1, ); if ($journey) { @@ -1942,10 +2362,11 @@ sub edit_journey { } my $journey = $self->journeys->get_single( - uid => $uid, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, + uid => $uid, + journey_id => $journey_id, + verbose => 1, + with_datetime => 1, + with_route_datetime => 1, ); if ( not $journey ) { @@ -2046,11 +2467,12 @@ sub edit_journey { if ( not $error ) { $journey = $self->journeys->get_single( - uid => $uid, - db => $db, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, + uid => $uid, + db => $db, + journey_id => $journey_id, + verbose => 1, + with_datetime => 1, + with_route_datetime => 1, ); $error = $self->journeys->sanity_check($journey); } @@ -2092,6 +2514,8 @@ sub edit_journey { sub add_journey_form { my ($self) = @_; + $self->stash( backend_id => $self->current_user->{backend_id} ); + if ( $self->param('action') and $self->param('action') eq 'save' ) { my $parser = DateTime::Format::Strptime->new( pattern => '%d.%m.%Y %H:%M', @@ -2112,8 +2536,9 @@ sub add_journey_form { $self->render( 'add_journey', with_autocomplete => 1, + status => 400, error => -'Zug muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.' +'Fahrt muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.' ); return; } @@ -2126,6 +2551,7 @@ sub add_journey_form { $self->render( 'add_journey', with_autocomplete => 1, + status => 400, error => "${key}: Ungültiges Datums-/Zeitformat" ); return; @@ -2148,8 +2574,9 @@ sub add_journey_form { my $db = $self->pg->db; my $tx = $db->begin; - $opt{db} = $db; - $opt{uid} = $self->current_user->{id}; + $opt{db} = $db; + $opt{uid} = $self->current_user->{id}; + $opt{backend_id} = $self->current_user->{backend_id}; my ( $journey_id, $error ) = $self->journeys->add(%opt); @@ -2167,6 +2594,7 @@ sub add_journey_form { $self->render( 'add_journey', with_autocomplete => 1, + status => 400, error => $error, ); } @@ -2184,4 +2612,241 @@ sub add_journey_form { } } +sub add_intransit_form { + my ($self) = @_; + + $self->stash( backend_id => $self->current_user->{backend_id} ); + + if ( $self->param('action') and $self->param('action') eq 'save' ) { + my $parser = DateTime::Format::Strptime->new( + pattern => '%d.%m.%Y %H:%M', + locale => 'de_DE', + time_zone => 'Europe/Berlin' + ); + my %opt; + my %trip; + + my @parts = split( qr{\s+}, $self->param('train') ); + + if ( @parts == 2 ) { + @trip{ 'train_type', 'train_no' } = @parts; + } + elsif ( @parts == 3 ) { + @trip{ 'train_type', 'train_line', 'train_no' } = @parts; + } + else { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => +'Fahrt muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.' + ); + return; + } + + for my $key (qw(sched_departure sched_arrival)) { + if ( $self->param($key) ) { + my $datetime = $parser->parse_datetime( $self->param($key) ); + if ( not $datetime ) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => "${key}: Ungültiges Datums-/Zeitformat" + ); + return; + } + $trip{$key} = $datetime; + } + } + + for my $key (qw(dep_station arr_station route comment)) { + $trip{$key} = $self->param($key); + } + + $opt{backend_id} = $self->current_user->{backend_id}; + + my $dep_stop = $self->stations->search( $trip{dep_station}, + backend_id => $opt{backend_id} ); + my $arr_stop = $self->stations->search( $trip{arr_station}, + backend_id => $opt{backend_id} ); + + if ( defined $trip{route} ) { + $trip{route} = [ split( qr{\r?\n\r?}, $trip{route} ) ]; + } + + my $route_has_start = 0; + my $route_has_stop = 0; + + for my $station ( @{ $trip{route} || [] } ) { + if ( $station eq $dep_stop->{name} + or $station eq $dep_stop->{eva} ) + { + $route_has_start = 1; + } + if ( $station eq $arr_stop->{name} + or $station eq $arr_stop->{eva} ) + { + $route_has_stop = 1; + } + } + + my @route; + + if ( not $route_has_start ) { + push( + @route, + [ + $dep_stop->{name}, + $dep_stop->{eva}, + { + lat => $dep_stop->{lat}, + lon => $dep_stop->{lon}, + } + ] + ); + } + + if ( $trip{route} ) { + my @unknown_stations; + my $prev_epoch; + for my $station ( @{ $trip{route} } ) { + my $ts; + my %station_data; + if ( $station + =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x + ) + { + $station = $+{stop}; + $ts = $parser->parse_datetime( $+{timestamp} ); + if ( $ts and $ts->epoch > $prev_epoch ) { + $station_data{sched_arr} = $ts->epoch; + $station_data{sched_dep} = $ts->epoch; + $prev_epoch = $ts->epoch; + } + else { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => "Ungültige Zeitangabe: $+{timestamp}" + ); + return; + } + } + my $station_info = $self->stations->search( $station, + backend_id => $opt{backend_id} ); + if ($station_info) { + $station_data{lat} = $station_info->{lat}; + $station_data{lon} = $station_info->{lon}; + push( + @route, + [ + $station_info->{name}, $station_info->{eva}, + \%station_data, + ] + ); + } + else { + push( @route, [ $station, undef, {} ] ); + push( @unknown_stations, $station ); + } + } + + if ( @unknown_stations == 1 ) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => "Unbekannter Unterwegshalt: $unknown_stations[0]" + ); + return; + } + elsif (@unknown_stations) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => 'Unbekannte Unterwegshalte: ' + . join( ', ', @unknown_stations ) + ); + return; + } + } + + if ( not $route_has_stop ) { + push( + @route, + [ + $arr_stop->{name}, + $arr_stop->{eva}, + { + lat => $arr_stop->{lat}, + lon => $arr_stop->{lon}, + } + ] + ); + } + + for my $station (@route) { + if ( $station->[0] eq $dep_stop->{name} + or $station->[1] eq $dep_stop->{eva} ) + { + $station->[2]{sched_dep} = $trip{sched_departure}->epoch; + } + if ( $station->[0] eq $arr_stop->{name} + or $station->[1] eq $arr_stop->{eva} ) + { + $station->[2]{sched_arr} = $trip{sched_arrival}->epoch; + } + } + + my $error; + my $db = $self->pg->db; + my $tx = $db->begin; + + $trip{dep_id} = $dep_stop->{eva}; + $trip{arr_id} = $arr_stop->{eva}; + $trip{route} = \@route; + + $opt{db} = $db; + $opt{manual} = \%trip; + $opt{uid} = $self->current_user->{id}; + + if ( not defined $trip{dep_id} ) { + $error = "Unknown departure stop '$trip{dep_station}'"; + } + elsif ( not defined $trip{arr_id} ) { + $error = "Unknown arrival stop '$trip{arr_station}'"; + } + elsif ( $trip{sched_arrival} <= $trip{sched_departure} ) { + $error = 'Ankunftszeit muss nach Abfahrtszeit liegen'; + } + else { + $error = $self->in_transit->add(%opt); + } + + if ($error) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => $error, + ); + } + else { + $tx->commit; + $self->redirect_to('/'); + } + } + else { + $self->render( + 'add_intransit', + with_autocomplete => 1, + error => undef + ); + } +} + 1; diff --git a/lib/Travelynx/Helper/DBDB.pm b/lib/Travelynx/Helper/DBDB.pm index b98a372..a310aa3 100644 --- a/lib/Travelynx/Helper/DBDB.pm +++ b/lib/Travelynx/Helper/DBDB.pm @@ -27,61 +27,102 @@ sub new { } sub has_wagonorder_p { - my ( $self, $ts, $train_no ) = @_; - my $api_ts = $ts->strftime('%Y%m%d%H%M'); - my $url - = "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}"; - my $cache = $self->{cache}; + my ( $self, %opt ) = @_; + + $opt{train_type} //= q{}; + my $datetime = $opt{datetime}->clone->set_time_zone('UTC'); + my %param = ( + administrationId => 80, + category => $opt{train_type}, + date => $datetime->strftime('%Y-%m-%d'), + evaNumber => $opt{eva}, + number => $opt{train_no}, + time => $datetime->rfc3339 =~ s{(?=Z)}{.000}r + ); + + my $url = sprintf( '%s?%s', +'https://www.bahn.de/web/api/reisebegleitung/wagenreihung/vehicle-sequence', + join( '&', map { $_ . '=' . $param{$_} } sort keys %param ) ); + my $promise = Mojo::Promise->new; + my $debug_prefix + = "has_wagonorder_p($opt{train_type} $opt{train_no} @ $opt{eva})"; - if ( my $content = $cache->get("HEAD $url") ) { + if ( my $content = $self->{main_cache}->get("HEAD $url") + // $self->{realtime_cache}->get("HEAD $url") ) + { if ( $content eq 'n' ) { + $self->{log}->debug("${debug_prefix}: n (cached)"); return $promise->reject; } else { + $self->{log}->debug("${debug_prefix}: ${content} (cached)"); return $promise->resolve($content); } } - $self->{user_agent}->request_timeout(5)->head_p( $url => $self->{header} ) + $self->{user_agent}->request_timeout(5) + ->get_p( $url => $self->{header} ) ->then( sub { my ($tx) = @_; if ( $tx->result->is_success ) { - $cache->set( "HEAD $url", 'a' ); + $self->{log}->debug("${debug_prefix}: a"); + $self->{main_cache}->set( "HEAD $url", 'a' ); + my $body = decode( 'utf-8', $tx->res->body ); + my $json = JSON->new->decode($body); + $self->{main_cache}->freeze( $url, $json ); $promise->resolve('a'); } else { - $cache->set( "HEAD $url", 'n' ); + my $code = $tx->res->code; + $self->{log}->debug("${debug_prefix}: n (HTTP $code)"); + $self->{realtime_cache}->set( "HEAD $url", 'n' ); $promise->reject; } return; } - )->catch( + )->catch( sub { - $cache->set( "HEAD $url", 'n' ); + my ($err) = @_; + $self->{log}->debug("${debug_prefix}: n ($err)"); + $self->{realtime_cache}->set( "HEAD $url", 'n' ); $promise->reject; return; } - )->wait; + )->wait; return $promise; } sub get_wagonorder_p { - my ( $self, $api, $ts, $train_no ) = @_; - my $api_ts = $ts->strftime('%Y%m%d%H%M'); - my $url - = "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}"; + my ( $self, %opt ) = @_; + + my $datetime = $opt{datetime}->clone->set_time_zone('UTC'); + my %param = ( + administrationId => 80, + category => $opt{train_type}, + date => $datetime->strftime('%Y-%m-%d'), + evaNumber => $opt{eva}, + number => $opt{train_no}, + time => $datetime->rfc3339 =~ s{(?=Z)}{.000}r + ); + + my $url = sprintf( '%s?%s', +'https://www.bahn.de/web/api/reisebegleitung/wagenreihung/vehicle-sequence', + join( '&', map { $_ . '=' . $param{$_} } sort keys %param ) ); + my $debug_prefix + = "get_wagonorder_p($opt{train_type} $opt{train_no} @ $opt{eva})"; - my $cache = $self->{cache}; my $promise = Mojo::Promise->new; - if ( my $content = $cache->thaw($url) ) { + if ( my $content = $self->{main_cache}->thaw($url) ) { + $self->{log}->debug("${debug_prefix}: (cached)"); $promise->resolve($content); return $promise; } - $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} ) + $self->{user_agent}->request_timeout(5) + ->get_p( $url => $self->{header} ) ->then( sub { my ($tx) = @_; @@ -89,22 +130,25 @@ sub get_wagonorder_p { if ( $tx->result->is_success ) { my $body = decode( 'utf-8', $tx->res->body ); my $json = JSON->new->decode($body); - $cache->freeze( $url, $json ); + $self->{log}->debug("${debug_prefix}: success"); + $self->{main_cache}->freeze( $url, $json ); $promise->resolve($json); } else { - my $code = $tx->code; + my $code = $tx->res->code; + $self->{log}->debug("${debug_prefix}: HTTP ${code}"); $promise->reject("HTTP ${code}"); } return; } - )->catch( + )->catch( sub { my ($err) = @_; + $self->{log}->debug("${debug_prefix}: error ${err}"); $promise->reject($err); return; } - )->wait; + )->wait; return $promise; } @@ -113,37 +157,44 @@ sub get_stationinfo_p { my $url = "https://lib.finalrewind.org/dbdb/s/${eva}.json"; - my $cache = $self->{cache}; + my $cache = $self->{main_cache}; my $promise = Mojo::Promise->new; if ( my $content = $cache->thaw($url) ) { + $self->{log}->debug("get_stationinfo_p(${eva}): (cached)"); return $promise->resolve($content); } - $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} ) + $self->{user_agent}->request_timeout(5) + ->get_p( $url => $self->{header} ) ->then( sub { my ($tx) = @_; if ( my $err = $tx->error ) { + $self->{log}->debug( +"get_stationinfo_p(${eva}): HTTP $err->{code} $err->{message}" + ); $cache->freeze( $url, {} ); $promise->reject("HTTP $err->{code} $err->{message}"); return; } my $json = $tx->result->json; + $self->{log}->debug("get_stationinfo_p(${eva}): success"); $cache->freeze( $url, $json ); $promise->resolve($json); return; } - )->catch( + )->catch( sub { my ($err) = @_; + $self->{log}->debug("get_stationinfo_p(${eva}): Error ${err}"); $cache->freeze( $url, {} ); $promise->reject($err); return; } - )->wait; + )->wait; return $promise; } diff --git a/lib/Travelynx/Helper/DBRIS.pm b/lib/Travelynx/Helper/DBRIS.pm new file mode 100644 index 0000000..1b7f099 --- /dev/null +++ b/lib/Travelynx/Helper/DBRIS.pm @@ -0,0 +1,146 @@ +package Travelynx::Helper::DBRIS; + +# Copyright (C) 2025 Birte Kristina Friesel +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +use strict; +use warnings; +use 5.020; +use utf8; + +use DateTime; +use Encode qw(decode); +use JSON; +use Mojo::Promise; +use Mojo::UserAgent; +use Travel::Status::DE::DBRIS; + +sub new { + my ( $class, %opt ) = @_; + + my $version = $opt{version}; + + $opt{header} + = { 'User-Agent' => +"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx" + }; + + return bless( \%opt, $class ); +} + +sub get_station_id_p { + my ( $self, $station_name ) = @_; + my $promise = Mojo::Promise->new; + Travel::Status::DE::DBRIS->new_p( + locationSearch => $station_name, + cache => $self->{cache}, + lwp_options => { + timeout => 10, + agent => $self->{header}{'User-Agent'}, + }, + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + )->then( + sub { + my ($dbris) = @_; + my $found; + for my $result ( $dbris->results ) { + if ( defined $result->eva ) { + $promise->resolve($result); + return; + } + } + $promise->reject("Unable to find station '$station_name'"); + return; + } + )->catch( + sub { + my ($err) = @_; + $promise->reject("'$err' while trying to look up '$station_name'"); + return; + } + )->wait; + return $promise; +} + +sub get_departures_p { + my ( $self, %opt ) = @_; + + my $agent = $self->{user_agent}; + + if ( $opt{station} =~ m{ [@] L = (?<eva> \d+ ) }x ) { + $opt{station} = { + eva => $+{eva}, + id => $opt{station}, + }; + } + + my $when = ( + $opt{timestamp} + ? $opt{timestamp}->clone + : DateTime->now( time_zone => 'Europe/Berlin' ) + )->subtract( minutes => $opt{lookbehind} ); + return Travel::Status::DE::DBRIS->new_p( + station => $opt{station}, + datetime => $when, + cache => $self->{cache}, + promise => 'Mojo::Promise', + user_agent => $agent->request_timeout(10), + ); +} + +sub get_journey_p { + my ( $self, %opt ) = @_; + + my $promise = Mojo::Promise->new; + + my $agent = $self->{user_agent}; + my $proxy; + if ( my @proxies = @{ $self->{service_config}{'bahn.de'}{proxies} // [] } ) + { + $proxy = $proxies[ int( rand( scalar @proxies ) ) ]; + } + elsif ( my $p = $self->{service_config}{'bahn.de'}{proxy} ) { + $proxy = $p; + } + + if ($proxy) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + + Travel::Status::DE::DBRIS->new_p( + journey => $opt{trip_id}, + with_polyline => $opt{with_polyline}, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => $agent->request_timeout(10), + )->then( + sub { + my ($dbris) = @_; + my $journey = $dbris->result; + + if ($journey) { + $self->{log}->debug("get_journey_p($opt{trip_id}): success"); + $promise->resolve($journey); + return; + } + $self->{log}->debug("get_journey_p($opt{trip_id}): no journey"); + $promise->reject('no journey'); + return; + } + )->catch( + sub { + my ($err) = @_; + $self->{log}->debug("get_journey_p($opt{trip_id}): error $err"); + $promise->reject($err); + return; + } + )->wait; + + return $promise; +} + +1; diff --git a/lib/Travelynx/Helper/EFA.pm b/lib/Travelynx/Helper/EFA.pm new file mode 100644 index 0000000..5cae51b --- /dev/null +++ b/lib/Travelynx/Helper/EFA.pm @@ -0,0 +1,105 @@ +package Travelynx::Helper::EFA; + +# Copyright (C) 2024 Birte Kristina Friesel +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +use strict; +use warnings; +use 5.020; + +use Travel::Status::DE::EFA; + +sub new { + my ( $class, %opt ) = @_; + + my $version = $opt{version}; + + $opt{header} + = { 'User-Agent' => +"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx" + }; + + return bless( \%opt, $class ); +} + +sub get_service { + my ( $self, $service ) = @_; + + return Travel::Status::DE::EFA::get_service($service); +} + +sub get_departures_p { + my ( $self, %opt ) = @_; + + my $when = ( + $opt{timestamp} + ? $opt{timestamp}->clone + : DateTime->now( time_zone => 'Europe/Berlin' ) + )->subtract( minutes => $opt{lookbehind} ); + return Travel::Status::DE::EFA->new_p( + service => $opt{service}, + name => $opt{name}, + datetime => $when, + full_routes => 1, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => $self->{user_agent}->request_timeout(5), + ); +} + +sub get_journey_p { + my ( $self, %opt ) = @_; + + my $promise = Mojo::Promise->new; + my $agent = $self->{user_agent}; + my $stopseq; + + if ( $opt{trip_id} + =~ m{ ^ ([^@]*) @ ([^@]*) [(] ([^T]*) T ([^)]*) [)] (.*) $ }x ) + { + $stopseq = { + stateless => $1, + stop_id => $2, + date => $3, + time => $4, + key => $5 + }; + } + else { + return $promise->reject("Invalid trip_id: $opt{trip_id}"); + } + + Travel::Status::DE::EFA->new_p( + service => $opt{service}, + stopseq => $stopseq, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => $agent->request_timeout(10), + )->then( + sub { + my ($efa) = @_; + my $journey = $efa->result; + + if ($journey) { + $self->{log}->debug("get_journey_p($opt{trip_id}): success"); + $promise->resolve($journey); + return; + } + $self->{log}->debug("get_journey_p($opt{trip_id}): no journey"); + $promise->reject('no journey'); + return; + } + )->catch( + sub { + my ($err) = @_; + $self->{log}->debug("get_journey_p($opt{trip_id}): error $err"); + $promise->reject($err); + return; + } + )->wait; + + return $promise; +} + +1; diff --git a/lib/Travelynx/Helper/HAFAS.pm b/lib/Travelynx/Helper/HAFAS.pm index 7671d78..c35dfdb 100644 --- a/lib/Travelynx/Helper/HAFAS.pm +++ b/lib/Travelynx/Helper/HAFAS.pm @@ -7,11 +7,13 @@ package Travelynx::Helper::HAFAS; use strict; use warnings; use 5.020; +use utf8; use DateTime; use Encode qw(decode); use JSON; use Mojo::Promise; +use Mojo::UserAgent; use Travel::Status::DE::HAFAS; sub _epoch { @@ -33,82 +35,73 @@ sub new { return bless( \%opt, $class ); } -sub get_json_p { - my ( $self, $url, %opt ) = @_; +sub class_to_product { + my ( $self, $hafas ) = @_; - my $cache = $self->{main_cache}; - my $promise = Mojo::Promise->new; - - if ( $opt{realtime} ) { - $cache = $self->{realtime_cache}; - } - $opt{encoding} //= 'ISO-8859-15'; + my $bits = $hafas->get_active_service->{productbits}; + my $ret; - if ( my $content = $cache->thaw($url) ) { - return $promise->resolve($content); + for my $i ( 0 .. $#{$bits} ) { + $ret->{ 2**$i } + = ref( $bits->[$i] ) eq 'ARRAY' ? $bits->[$i][0] : $bits->[$i]; } - $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} ) - ->then( - sub { - my ($tx) = @_; - - if ( my $err = $tx->error ) { - $promise->reject( -"hafas->get_json_p($url) returned HTTP $err->{code} $err->{message}" - ); - return; - } + return $ret; +} - my $body = decode( $opt{encoding}, $tx->res->body ); +sub get_service { + my ( $self, $service ) = @_; - $body =~ s{^TSLs[.]sls = }{}; - $body =~ s{;$}{}; - $body =~ s{(}{(}g; - $body =~ s{)}{)}g; - my $json = JSON->new->decode($body); - $cache->freeze( $url, $json ); - $promise->resolve($json); - return; - } - )->catch( - sub { - my ($err) = @_; - $self->{log}->info("hafas->get_json_p($url): $err"); - $promise->reject("hafas->get_json_p($url): $err"); - return; - } - )->wait; - return $promise; + return Travel::Status::DE::HAFAS::get_service($service); } sub get_departures_p { my ( $self, %opt ) = @_; + $opt{service} //= 'ÖBB'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + my $when = ( $opt{timestamp} ? $opt{timestamp}->clone : DateTime->now( time_zone => 'Europe/Berlin' ) )->subtract( minutes => $opt{lookbehind} ); return Travel::Status::DE::HAFAS->new_p( + service => $opt{service}, station => $opt{eva}, datetime => $when, lookahead => $opt{lookahead} + $opt{lookbehind}, results => 300, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(5), + user_agent => $agent->request_timeout(5), ); } sub search_location_p { my ( $self, %opt ) = @_; + $opt{service} //= 'ÖBB'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + return Travel::Status::DE::HAFAS->new_p( + service => $opt{service}, locationSearch => $opt{query}, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(5), + user_agent => $agent->request_timeout(5), ); } @@ -121,23 +114,36 @@ sub get_tripid_p { my $train_desc = $train->type . ' ' . $train->train_no; $train_desc =~ s{^- }{}; + $opt{service} //= 'ÖBB'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + Travel::Status::DE::HAFAS->new_p( + service => $opt{service}, journeyMatch => $train_desc, datetime => $train->start, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(10), + user_agent => $agent->request_timeout(10), )->then( sub { my ($hafas) = @_; my @results = $hafas->results; if ( not @results ) { + $self->{log}->debug("get_tripid_p($train_desc): no results"); $promise->reject( "journeyMatch($train_desc) returned no results"); return; } + $self->{log}->debug("get_tripid_p($train_desc): success"); + my $result = $results[0]; if ( @results > 1 ) { for my $journey (@results) { @@ -154,6 +160,7 @@ sub get_tripid_p { )->catch( sub { my ($err) = @_; + $self->{log}->debug("get_tripid_p($train_desc): error $err"); $promise->reject($err); return; } @@ -168,29 +175,42 @@ sub get_journey_p { my $promise = Mojo::Promise->new; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + $opt{service} //= 'ÖBB'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + Travel::Status::DE::HAFAS->new_p( + service => $opt{service}, journey => { id => $opt{trip_id}, }, with_polyline => $opt{with_polyline}, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(10), + user_agent => $agent->request_timeout(10), )->then( sub { my ($hafas) = @_; my $journey = $hafas->result; if ($journey) { + $self->{log}->debug("get_journey_p($opt{trip_id}): success"); $promise->resolve($journey); return; } + $self->{log}->debug("get_journey_p($opt{trip_id}): no journey"); $promise->reject('no journey'); return; } )->catch( sub { my ($err) = @_; + $self->{log}->debug("get_journey_p($opt{trip_id}): error $err"); $promise->reject($err); return; } @@ -199,13 +219,23 @@ sub get_journey_p { return $promise; } -sub get_route_timestamps_p { +sub get_route_p { my ( $self, %opt ) = @_; my $promise = Mojo::Promise->new; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + $opt{service} //= 'ÖBB'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + Travel::Status::DE::HAFAS->new_p( + service => $opt{service}, journey => { id => $opt{trip_id}, @@ -214,18 +244,17 @@ sub get_route_timestamps_p { with_polyline => $opt{with_polyline}, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(10), + user_agent => $agent->request_timeout(10), )->then( sub { my ($hafas) = @_; my $journey = $hafas->result; - my $ret = {}; + my $ret = []; my $polyline; my $station_is_past = 1; for my $stop ( $journey->route ) { - my $name = $stop->loc->name; - $ret->{$name} = $ret->{ $stop->loc->eva } = { + my $entry = { name => $stop->loc->name, eva => $stop->loc->eva, sched_arr => _epoch( $stop->sched_arr ), @@ -234,29 +263,32 @@ sub get_route_timestamps_p { rt_dep => _epoch( $stop->rt_dep ), arr_delay => $stop->arr_delay, dep_delay => $stop->dep_delay, - load => $stop->load + load => $stop->load, + lat => $stop->loc->lat, + lon => $stop->loc->lon, }; if ( $stop->tz_offset ) { - $ret->{$name}{tz_offset} = $stop->tz_offset; + $entry->{tz_offset} = $stop->tz_offset; } if ( ( $stop->arr_cancelled or not $stop->sched_arr ) and ( $stop->dep_cancelled or not $stop->sched_dep ) ) { - $ret->{$name}{isCancelled} = 1; + $entry->{isCancelled} = 1; } if ( $station_is_past - and not $ret->{$name}{isCancelled} + and not $entry->{isCancelled} and $now->epoch < ( - $ret->{$name}{rt_arr} // $ret->{$name}{rt_dep} - // $ret->{$name}{sched_arr} - // $ret->{$name}{sched_dep} // $now->epoch + $entry->{rt_arr} // $entry->{rt_dep} + // $entry->{sched_arr} // $entry->{sched_dep} + // $now->epoch ) ) { $station_is_past = 0; } - $ret->{$name}{isPast} = $station_is_past; + $entry->{isPast} = $station_is_past; + push( @{$ret}, $entry ); } if ( $journey->polyline ) { @@ -298,12 +330,14 @@ sub get_route_timestamps_p { } } + $self->{log}->debug("get_route_p($opt{trip_id}): success"); $promise->resolve( $ret, $journey, $polyline ); return; } )->catch( sub { my ($err) = @_; + $self->{log}->debug("get_route_p($opt{trip_id}): error $err"); $promise->reject($err); return; } diff --git a/lib/Travelynx/Helper/IRIS.pm b/lib/Travelynx/Helper/IRIS.pm index deed79a..34739eb 100644 --- a/lib/Travelynx/Helper/IRIS.pm +++ b/lib/Travelynx/Helper/IRIS.pm @@ -41,8 +41,12 @@ sub get_departures { my @station_matches = Travel::Status::DE::IRIS::Stations::get_station($station); + if ( $station =~ m{ ^ \d+ $ }x ) { + @station_matches = ( [ undef, undef, $station ] ); + } + if ( @station_matches == 1 ) { - $station = $station_matches[0][0]; + $station = $station_matches[0][2]; my $status = Travel::Status::DE::IRIS->new( station => $station, main_cache => $self->{main_cache}, @@ -108,8 +112,12 @@ sub get_departures_p { my @station_matches = Travel::Status::DE::IRIS::Stations::get_station($station); + if ( $station =~ m{ ^ \d+ $ }x ) { + @station_matches = ( [ undef, undef, $station ] ); + } + if ( @station_matches == 1 ) { - $station = $station_matches[0][0]; + $station = $station_matches[0][2]; my $promise = Mojo::Promise->new; Travel::Status::DE::IRIS->new_p( station => $station, diff --git a/lib/Travelynx/Helper/MOTIS.pm b/lib/Travelynx/Helper/MOTIS.pm new file mode 100644 index 0000000..df79385 --- /dev/null +++ b/lib/Travelynx/Helper/MOTIS.pm @@ -0,0 +1,161 @@ +package Travelynx::Helper::MOTIS; + +# Copyright (C) 2025 networkException <git@nwex.de> +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +use strict; +use warnings; +use 5.020; +use utf8; + +use DateTime; +use Encode qw(decode); +use JSON; +use Mojo::Promise; +use Mojo::UserAgent; + +use Travel::Status::MOTIS; + +sub _epoch { + my ($dt) = @_; + + return $dt ? $dt->epoch : 0; +} + +sub new { + my ( $class, %opt ) = @_; + + my $version = $opt{version}; + + $opt{header} + = { 'User-Agent' => +"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx" + }; + + return bless( \%opt, $class ); +} + +sub get_service { + my ( $self, $service ) = @_; + + return Travel::Status::MOTIS::get_service($service); +} + +sub get_station_by_query_p { + my ( $self, %opt ) = @_; + + $opt{service} //= 'transitous'; + + my $promise = Mojo::Promise->new; + + Travel::Status::MOTIS->new_p( + cache => $self->{cache}, + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + time_zone => 'Europe/Berlin', + lwp_options => { + timeout => 10, + agent => $self->{header}{'User-Agent'}, + }, + + service => $opt{service}, + stops_by_query => $opt{query}, + )->then( + sub { + my ($motis) = @_; + my $found; + + for my $result ( $motis->results ) { + if ( defined $result->id ) { + $promise->resolve($result); + return; + } + } + + $promise->reject("Unable to find station '$opt{query}'"); + return; + } + )->catch( + sub { + my ($err) = @_; + $promise->reject("'$err' while trying to look up '$opt{query}'"); + return; + } + )->wait; + + return $promise; +} + +sub get_departures_p { + my ( $self, %opt ) = @_; + + $opt{service} //= 'transitous'; + + my $timestamp = ( + $opt{timestamp} + ? $opt{timestamp}->clone + : DateTime->now + )->subtract( minutes => $opt{lookbehind} ); + + return Travel::Status::MOTIS->new_p( + cache => $self->{cache}, + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + time_zone => 'Europe/Berlin', + lwp_options => { + timeout => 10, + agent => $self->{header}{'User-Agent'}, + }, + + service => $opt{service}, + timestamp => $timestamp, + stop_id => $opt{station_id}, + results => 60, + ); +} + +sub get_trip_p { + my ( $self, %opt ) = @_; + + $opt{service} //= 'transitous'; + + my $promise = Mojo::Promise->new; + + Travel::Status::MOTIS->new_p( + with_polyline => $opt{with_polyline}, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + time_zone => 'Europe/Berlin', + + service => $opt{service}, + trip_id => $opt{trip_id}, + )->then( + sub { + my ($motis) = @_; + my $journey = $motis->result; + + if ($journey) { + $self->{log}->debug("get_trip_p($opt{trip_id}): success"); + $promise->resolve($journey); + return; + } + + $self->{log}->debug("get_trip_p($opt{trip_id}): no journey"); + $promise->reject('no journey'); + return; + } + )->catch( + sub { + my ($err) = @_; + $self->{log}->debug("get_trip_p($opt{trip_id}): error $err"); + $promise->reject($err); + return; + } + )->wait; + + return $promise; +} + +1; diff --git a/lib/Travelynx/Helper/Sendmail.pm b/lib/Travelynx/Helper/Sendmail.pm index baa1156..54829c8 100644 --- a/lib/Travelynx/Helper/Sendmail.pm +++ b/lib/Travelynx/Helper/Sendmail.pm @@ -9,7 +9,7 @@ use warnings; use 5.020; -use Encode qw(encode); +use Encode qw(encode); use Email::Sender::Simple qw(try_to_sendmail); use MIME::Entity; diff --git a/lib/Travelynx/Helper/Traewelling.pm b/lib/Travelynx/Helper/Traewelling.pm index d688004..66f2a29 100644 --- a/lib/Travelynx/Helper/Traewelling.pm +++ b/lib/Travelynx/Helper/Traewelling.pm @@ -78,7 +78,8 @@ sub get_status_p { $self->{user_agent}->request_timeout(20) ->get_p( "https://traewelling.de/api/v1/user/${username}/statuses?limit=1" => - $header )->then( + $header ) + ->then( sub { my ($tx) = @_; if ( my $err = $tx->error ) { @@ -116,6 +117,7 @@ sub get_status_p { my $category = $status->{train}{category}; my $linename = $status->{train}{lineName}; + my $train_no = $status->{train}{journeyNumber}; my $trip_id = $status->{train}{hafasId}; my ( $train_type, $train_line ) = split( qr{ }, $linename ); $promise->resolve( @@ -133,6 +135,7 @@ sub get_status_p { arr_ds100 => $arr_ds100, arr_name => $arr_name, trip_id => $trip_id, + train_no => $train_no, train_type => $train_type, line => $linename, line_no => $train_line, @@ -148,13 +151,13 @@ sub get_status_p { } } } - )->catch( + )->catch( sub { my ($err) = @_; $promise->reject( { text => "v1/${username}/statuses: $err" } ); return; } - )->wait; + )->wait; return $promise; } @@ -236,13 +239,13 @@ sub logout_p { return; } } - )->catch( + )->catch( sub { my ($err) = @_; $promise->reject("v1/auth/logout: $err"); return; } - )->wait; + )->wait; return $promise; } @@ -320,7 +323,8 @@ sub checkin_p { $self->{user_agent}->request_timeout(20) ->post_p( "https://traewelling.de/api/v1/trains/checkin" => $header => json => - $request )->then( + $request ) + ->then( sub { my ($tx) = @_; if ( my $err = $tx->error ) { @@ -366,7 +370,7 @@ sub checkin_p { # on the user status page return; } - )->catch( + )->catch( sub { my ($err) = @_; $self->{log}->debug("... $debug_prefix error: $err"); @@ -379,7 +383,7 @@ sub checkin_p { $promise->reject( { connection => $err } ); return; } - )->wait; + )->wait; return $promise; } diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm index 69026ac..11177dd 100644 --- a/lib/Travelynx/Model/InTransit.pm +++ b/lib/Travelynx/Model/InTransit.pm @@ -1,6 +1,7 @@ package Travelynx::Model::InTransit; -# Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2020-2025 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -12,11 +13,12 @@ use DateTime; use JSON; my %visibility_itoa = ( - 100 => 'public', - 80 => 'travelynx', - 60 => 'followers', - 30 => 'unlisted', - 10 => 'private', + 100 => 'public', + 80 => 'travelynx', + 60 => 'followers', + 30 => 'unlisted', + 10 => 'private', + default => 'default', ); my %visibility_atoi = ( @@ -30,7 +32,7 @@ my %visibility_atoi = ( sub _epoch { my ($dt) = @_; - return $dt ? $dt->epoch : 0; + return $dt ? $dt->epoch : undef; } sub epoch_to_dt { @@ -47,6 +49,16 @@ sub epoch_to_dt { ); } +sub epoch_or_dt_to_dt { + my ($input) = @_; + + if ( ref($input) eq 'DateTime' ) { + return $input; + } + + return epoch_to_dt($input); +} + sub new { my ( $class, %opt ) = @_; @@ -83,13 +95,20 @@ sub add { my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; + my $backend_id = $opt{backend_id}; my $train = $opt{train}; + my $train_suffix = $opt{train_suffix}; my $journey = $opt{journey}; my $stop = $opt{stop}; + my $stopover = $opt{stopover}; + my $manual = $opt{manual}; my $checkin_station_id = $opt{departure_eva}; my $route = $opt{route}; + my $data = $opt{data}; + my $persistent_data; my $json = JSON->new; + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); if ($train) { $db->insert( @@ -99,28 +118,86 @@ sub add { cancelled => $train->departure_is_cancelled ? 1 : 0, checkin_station_id => $checkin_station_id, - 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($route), - messages => $json->encode( + checkin_time => $now, + 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($route), + messages => $json->encode( [ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ] ), - data => JSON->new->encode( + data => $json->encode( { rt => $train->departure_has_realtime ? 1 - : 0 + : 0, + %{ $data // {} } } ), + backend_id => $backend_id, } ); } - elsif ( $journey and $stop ) { + elsif ( $journey + and $stop + and ref($journey) eq 'Travel::Status::DE::EFA::Trip' ) + { + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $j_stop->full_name, + $j_stop->id_num, + { + sched_arr => _epoch( $j_stop->sched_arr ), + sched_dep => _epoch( $j_stop->sched_dep ), + rt_arr => _epoch( $j_stop->rt_arr ), + rt_dep => _epoch( $j_stop->rt_dep ), + isCancelled => $j_stop->is_cancelled, + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + efa_load => $j_stop->occupancy, + lat => $j_stop->latlon->[0], + lon => $j_stop->latlon->[1], + } + ] + ); + } + $persistent_data->{operator} = $journey->operator; + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => $stop->is_cancelled ? 1 : 0, + checkin_station_id => $stop->id_num, + checkin_time => $now, + dep_platform => $stop->platform, + train_type => $journey->type // q{}, + train_line => $journey->line, + train_no => $journey->number // q{}, + train_id => $opt{trip_id}, + sched_departure => $stop->sched_dep, + real_departure => $stop->rt_dep // $stop->sched_dep, + route => $json->encode( \@route ), + data => $json->encode( + { + rt => $stop->rt_dep ? 1 : 0, + %{ $data // {} } + } + ), + user_data => $json->encode($persistent_data), + backend_id => $backend_id, + } + ); + } + elsif ( $journey + and $stop + and ref($journey) eq 'Travel::Status::DE::HAFAS::Journey' ) + { my @route; my $product = $journey->product_at( $stop->loc->eva ) // $journey->product; @@ -137,7 +214,9 @@ sub add { rt_dep => _epoch( $j_stop->rt_dep ), arr_delay => $j_stop->arr_delay, dep_delay => $j_stop->dep_delay, - load => $j_stop->load + load => $j_stop->load, + lat => $j_stop->loc->lat, + lon => $j_stop->loc->lon, } ] ); @@ -145,6 +224,9 @@ sub add { $route[-1][2]{tz_offset} = $j_stop->tz_offset; } } + if ( scalar $journey->operators ) { + $persistent_data->{operators} = [ $journey->operators ]; + } $db->insert( 'in_transit', { @@ -153,21 +235,207 @@ sub add { ? 1 : 0, checkin_station_id => $stop->loc->eva, + checkin_time => $now, + dep_platform => $stop->{platform}, + train_type => $product->type // q{}, + train_line => $product->line_no, + train_no => $product->number // q{}, + train_id => $journey->id, + sched_departure => $stop->{sched_dep}, + real_departure => $stop->{rt_dep} // $stop->{sched_dep}, + route => $json->encode( \@route ), + data => $json->encode( + { + rt => $stop->{rt_dep} ? 1 : 0, + %{ $data // {} } + } + ), + user_data => $json->encode($persistent_data), + backend_id => $backend_id, + } + ); + } + elsif ( $journey + and $stop + and ref($journey) eq 'Travel::Status::DE::DBRIS::Journey' ) + { + my $number = $journey->train_no // $journey->number // $train_suffix; + + my $line; + if ( defined $journey->line_no and $journey->line_no ne $number ) { + $line = $journey->line_no; + } + elsif ( defined $train_suffix and $train_suffix ne $number ) { + $line = $train_suffix; + } + + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $j_stop->name, + $j_stop->eva, + { + sched_arr => _epoch( $j_stop->sched_arr ), + sched_dep => _epoch( $j_stop->sched_dep ), + rt_arr => _epoch( $j_stop->rt_arr ), + rt_dep => _epoch( $j_stop->rt_dep ), + isCancelled => $j_stop->is_cancelled, + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + load => { + FIRST => $j_stop->occupancy_first, + SECOND => $j_stop->occupancy_second + }, + lat => $j_stop->lat, + lon => $j_stop->lon, + } + ] + ); + } + my @messages; + for my $msg ( $journey->messages ) { + if ( not $msg->{ueberschrift} ) { + push( + @{ $data->{him_msg} }, + { + header => q{}, + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + push( + @{ $persistent_data->{him_msg} }, + { + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + } + } + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => $stop->is_cancelled + ? 1 + : 0, + checkin_station_id => $stop->eva, + checkin_time => $now, + dep_platform => $stop->platform, + train_type => $journey->type // q{}, + train_line => $line, + train_no => $number, + train_id => $data->{trip_id}, + sched_departure => $stop->sched_dep, + real_departure => $stop->rt_dep // $stop->sched_dep, + route => $json->encode( \@route ), + data => $json->encode( + { + rt => $stop->{rt_dep} ? 1 : 0, + %{ $data // {} } + } + ), + user_data => $json->encode($persistent_data), + backend_id => $backend_id, + } + ); + } + elsif ( $journey + and $stopover + and ref($journey) eq 'Travel::Status::MOTIS::Trip' ) + { + my @route; + for my $journey_stopover ( $journey->stopovers ) { + push( + @route, + [ + $journey_stopover->stop->name, + $journey_stopover->stop->{eva} + // die('eva not set for stopover'), + { + sched_arr => + _epoch( $journey_stopover->scheduled_arrival ), + sched_dep => + _epoch( $journey_stopover->scheduled_departure ), + rt_arr => _epoch( $journey_stopover->realtime_arrival ), + rt_dep => + _epoch( $journey_stopover->realtime_departure ), + arr_delay => $journey_stopover->arrival_delay, + dep_delay => $journey_stopover->departure_delay, + lat => $journey_stopover->stop->lat, + lon => $journey_stopover->stop->lon, + } + ] + ); + } + + $persistent_data->{operator} = $journey->agency; + + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => $stopover->{is_cancelled} + ? 1 + : 0, + checkin_station_id => $stopover->stop->{eva}, checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ), - dep_platform => $stop->{platform}, - train_type => $product->type // q{}, - train_line => $product->line_no, - train_no => $product->number // q{}, + dep_platform => $stopover->track, + train_type => $journey->mode, + train_no => q{}, train_id => $journey->id, - sched_departure => $stop->{sched_dep}, - real_departure => $stop->{rt_dep} // $stop->{sched_dep}, + train_line => $journey->route_name, + sched_departure => $stopover->scheduled_departure, + real_departure => $stopover->departure, route => $json->encode( \@route ), - data => JSON->new->encode( { rt => $stop->{rt_dep} ? 1 : 0 } ), + data => $json->encode( + { + rt => $stopover->{is_realtime} ? 1 : 0, + %{ $data // {} } + } + ), + user_data => $json->encode($persistent_data), + backend_id => $backend_id, + } + ); + } + elsif ($manual) { + if ( $manual->{comment} ) { + $persistent_data->{comment} = $manual->{comment}; + } + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => 0, + checkin_station_id => $manual->{dep_id}, + checkout_station_id => $manual->{arr_id}, + checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ), + train_type => $manual->{train_type}, + train_no => $manual->{train_no} || q{}, + train_id => 'manual', + train_line => $manual->{train_line} || undef, + sched_departure => $manual->{sched_departure}, + real_departure => $manual->{sched_departure}, + sched_arrival => $manual->{sched_arrival}, + real_arrival => $manual->{sched_arrival}, + route => $json->encode( $manual->{route} // [] ), + data => $json->encode( + { + manual => \1, + %{ $data // {} } + } + ), + user_data => $json->encode($persistent_data), + backend_id => $backend_id, } ); + return; } else { - die('neither train nor journey specified'); + die('invalid arguments / argument types passed to InTransit->add'); } } @@ -211,8 +479,15 @@ sub postprocess { if ($is_after) { push( @route_after, $station ); } - if ( $ret->{dep_name} - and $station->[0] eq $ret->{dep_name} ) + + # Note that the departure stop may be present more than once in @route, + # e.g. when traveling along ring lines such as S41 / S42 in Berlin. + if ( + $ret->{dep_name} + and $station->[0] eq $ret->{dep_name} + and not($station->[2]{sched_dep} + and $station->[2]{sched_dep} < $ret->{sched_dep_ts} ) + ) { $is_after = 1; if ( @{$station} > 1 and not $dep_info ) { @@ -235,13 +510,17 @@ sub postprocess { $ret->{route_after} = \@route_after; $ret->{extra_data} = $ret->{data}; $ret->{comment} = $ret->{user_data}{comment}; + $ret->{wagongroups} = $ret->{user_data}{wagongroups}; + + $ret->{platform_type} = 'Gleis'; + if ( $ret->{train_type} and $ret->{train_type} =~ m{ ast | bus | ruf }ix ) { + $ret->{platform_type} = 'Steig'; + } $ret->{visibility_str} - = $ret->{visibility} - ? $visibility_itoa{ $ret->{visibility} } - : 'default'; + = $visibility_itoa{ $ret->{visibility} // 'default' }; $ret->{effective_visibility_str} - = $visibility_itoa{ $ret->{effective_visibility} }; + = $visibility_itoa{ $ret->{effective_visibility} // 'default' }; my @parsed_messages; for my $message ( @{ $ret->{messages} // [] } ) { @@ -265,7 +544,7 @@ sub postprocess { = $dep_info->{rt_arr}->epoch - $epoch; } - for my $station (@route_after) { + for my $station (@route) { if ( @{$station} > 1 ) { # Note: $station->[2]{sched_arr} may already have been @@ -273,31 +552,25 @@ sub postprocess { # station is present several times in a train's route, e.g. # for Frankfurt Flughafen in some nightly connections. my $times = $station->[2] // {}; - if ( $times->{sched_arr} - and ref( $times->{sched_arr} ) ne 'DateTime' ) - { - $times->{sched_arr} - = epoch_to_dt( $times->{sched_arr} ); - if ( $times->{rt_arr} ) { - $times->{rt_arr} - = epoch_to_dt( $times->{rt_arr} ); - $times->{arr_delay} - = $times->{rt_arr}->epoch - $times->{sched_arr}->epoch; + for my $key (qw(sched_arr rt_arr sched_dep rt_dep)) { + if ( $times->{$key} ) { + $times->{$key} + = epoch_or_dt_to_dt( $times->{$key} ); } + } + if ( $times->{sched_arr} and $times->{rt_arr} ) { + $times->{arr_delay} + = $times->{rt_arr}->epoch - $times->{sched_arr}->epoch; + } + if ( $times->{sched_arr} or $times->{rt_arr} ) { $times->{arr} = $times->{rt_arr} || $times->{sched_arr}; $times->{arr_countdown} = $times->{arr}->epoch - $epoch; } - if ( $times->{sched_dep} - and ref( $times->{sched_dep} ) ne 'DateTime' ) - { - $times->{sched_dep} - = epoch_to_dt( $times->{sched_dep} ); - if ( $times->{rt_dep} ) { - $times->{rt_dep} - = epoch_to_dt( $times->{rt_dep} ); - $times->{dep_delay} - = $times->{rt_dep}->epoch - $times->{sched_dep}->epoch; - } + if ( $times->{sched_dep} and $times->{rt_dep} ) { + $times->{dep_delay} + = $times->{rt_dep}->epoch - $times->{sched_dep}->epoch; + } + if ( $times->{sched_dep} or $times->{rt_dep} ) { $times->{dep} = $times->{rt_dep} || $times->{sched_dep}; $times->{dep_countdown} = $times->{dep}->epoch - $epoch; } @@ -339,7 +612,7 @@ sub get { my $table = 'in_transit'; - if ( $opt{with_timestamps} ) { + if ( $opt{with_timestamps} or $opt{with_polyline} ) { $table = 'in_transit_str'; } @@ -353,13 +626,16 @@ sub get { $ret = $res->hash; } + if ( $opt{with_polyline} and $ret ) { + $ret->{dep_latlon} = [ $ret->{dep_lat}, $ret->{dep_lon} ]; + $ret->{arr_latlon} = [ $ret->{arr_lat}, $ret->{arr_lon} ]; + } + if ( $opt{with_visibility} and $ret ) { $ret->{visibility_str} - = $ret->{visibility} - ? $visibility_itoa{ $ret->{visibility} } - : 'default'; + = $visibility_itoa{ $ret->{visibility} // 'default' }; $ret->{effective_visibility_str} - = $visibility_itoa{ $ret->{effective_visibility} }; + = $visibility_itoa{ $ret->{effective_visibility} // 'default' }; } if ( $opt{postprocess} and $ret ) { @@ -408,17 +684,20 @@ sub get_all_active { ->hashes->each; } -sub get_checkout_station_id { +sub get_checkout_ids { my ( $self, %opt ) = @_; my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; - my $status = $db->select( 'in_transit', ['checkout_station_id'], - { user_id => $uid } )->hash; + my $status = $db->select( + 'in_transit', + [ 'checkout_station_id', 'backend_id' ], + { user_id => $uid } + )->hash; if ($status) { - return $status->{checkout_station_id}; + return $status->{checkout_station_id}, $status->{backend_id}; } return; } @@ -457,13 +736,6 @@ sub set_arrival { my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; my $train = $opt{train}; - my $route = $opt{route}; - - $route = $self->_merge_old_route( - db => $db, - uid => $uid, - route => $route - ); my $json = JSON->new; @@ -474,7 +746,6 @@ sub set_arrival { arr_platform => $train->platform, sched_arrival => $train->sched_arrival, real_arrival => $train->arrival, - route => $json->encode($route), messages => $json->encode( [ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ] ) @@ -566,7 +837,8 @@ sub set_polyline { $self->set_polyline_id( uid => $uid, db => $db, - polyline_id => $polyline_id + polyline_id => $polyline_id, + train_id => $opt{train_id}, ); } @@ -579,11 +851,13 @@ sub set_polyline_id { my $db = $opt{db} // $self->{pg}->db; my $polyline_id = $opt{polyline_id}; - $db->update( - 'in_transit', - { polyline_id => $polyline_id }, - { user_id => $uid } - ); + my %where = ( user_id => $uid ); + + if ( $opt{train_id} ) { + $where{train_id} = $opt{train_id}; + } + + $db->update( 'in_transit', { polyline_id => $polyline_id }, \%where ); } sub set_route_data { @@ -596,6 +870,12 @@ sub set_route_data { my $qos_msg = $opt{qos_messages}; my $him_msg = $opt{him_messages}; + my %where = ( user_id => $uid ); + + if ( $opt{train_id} ) { + $where{train_id} = $opt{train_id}; + } + my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } ) ->expand->hash; @@ -612,7 +892,7 @@ sub set_route_data { route => JSON->new->encode($route), data => JSON->new->encode($data) }, - { user_id => $uid } + \%where ); } @@ -699,6 +979,128 @@ sub update_departure_cancelled { return $rows; } +sub update_departure_dbris { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h = $db->select( 'in_transit', [ 'data', 'user_data' ], + { user_id => $uid } )->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + my $persistent_data = $res_h ? $res_h->{user_data} : {}; + + if ( $stop->{rt_dep} ) { + $ephemeral_data->{rt} = 1; + } + + $ephemeral_data->{him_msg} = []; + $persistent_data->{him_msg} = []; + for my $msg ( $journey->messages ) { + if ( not $msg->{ueberschrift} ) { + push( + @{ $ephemeral_data->{him_msg} }, + { + header => q{}, + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + push( + @{ $persistent_data->{him_msg} }, + { + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + } + } + + # selecting on user_id and train_no avoids a race condition if a user checks + # into a new train while we are fetching data for their previous journey. In + # this case, the new train would receive data from the previous journey. + $db->update( + 'in_transit', + { + real_departure => $stop->{rt_dep}, + data => $json->encode($ephemeral_data), + user_data => $json->encode($persistent_data), + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + +sub update_departure_efa { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + if ( $stop->rt_dep ) { + $ephemeral_data->{rt} = 1; + } + + # selecting on user_id and train_no avoids a race condition if a user checks + # into a new train while we are fetching data for their previous journey. In + # this case, the new train would receive data from the previous journey. + $db->update( + 'in_transit', + { + data => $json->encode($ephemeral_data), + real_departure => $stop->rt_dep, + }, + { + user_id => $uid, + train_id => $opt{trip_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + +sub update_departure_motis { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stopover = $opt{stopover}; + my $json = JSON->new; + + # selecting on user_id and train_no avoids a race condition if a user checks + # into a new train while we are fetching data for their previous journey. In + # this case, the new train would receive data from the previous journey. + $db->update( + 'in_transit', + { + real_departure => $stopover->{realtime_departure}, + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_departure_hafas { my ( $self, %opt ) = @_; my $uid = $opt{uid}; @@ -709,12 +1111,20 @@ sub update_departure_hafas { my $stop = $opt{stop}; my $json = JSON->new; + my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + if ( $stop->{rt_dep} ) { + $ephemeral_data->{rt} = 1; + } + # selecting on user_id and train_no avoids a race condition if a user checks # into a new train while we are fetching data for their previous journey. In # this case, the new train would receive data from the previous journey. $db->update( 'in_transit', { + data => $json->encode($ephemeral_data), real_departure => $stop->{rt_dep}, }, { @@ -768,6 +1178,210 @@ sub update_arrival { return $rows; } +sub update_arrival_dbris { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h = $db->select( 'in_transit', [ 'data', 'user_data' ], + { user_id => $uid } )->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + my $persistent_data = $res_h ? $res_h->{user_data} : {}; + + if ( $stop->{rt_arr} ) { + $ephemeral_data->{rt} = 1; + } + + $ephemeral_data->{him_msg} = []; + $persistent_data->{him_msg} = []; + for my $msg ( $journey->messages ) { + if ( not $msg->{ueberschrift} ) { + push( + @{ $ephemeral_data->{him_msg} }, + { + header => q{}, + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + push( + @{ $persistent_data->{him_msg} }, + { + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + } + } + + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $j_stop->name, + $j_stop->eva, + { + sched_arr => _epoch( $j_stop->sched_arr ), + sched_dep => _epoch( $j_stop->sched_dep ), + rt_arr => _epoch( $j_stop->rt_arr ), + rt_dep => _epoch( $j_stop->rt_dep ), + platform => $j_stop->platform, + isCancelled => $j_stop->is_cancelled, + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + load => { + FIRST => $j_stop->occupancy_first, + SECOND => $j_stop->occupancy_second + }, + lat => $j_stop->lat, + lon => $j_stop->lon, + } + ] + ); + } + + # selecting on user_id and train_no avoids a race condition if a user checks + # into a new train while we are fetching data for their previous journey. In + # this case, the new train would receive data from the previous journey. + $db->update( + 'in_transit', + { + real_arrival => $stop->rt_arr, + arr_platform => $stop->platform, + route => $json->encode( [@route] ), + data => $json->encode($ephemeral_data), + user_data => $json->encode($persistent_data), + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + +sub update_arrival_efa { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h + = $db->select( 'in_transit', [ 'data', 'route' ], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + my $old_route = $res_h ? $res_h->{route} : []; + + if ( $stop->rt_arr ) { + $ephemeral_data->{rt} = 1; + } + + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $j_stop->full_name, + $j_stop->id_num, + { + sched_arr => _epoch( $j_stop->sched_arr ), + sched_dep => _epoch( $j_stop->sched_dep ), + rt_arr => _epoch( $j_stop->rt_arr ), + rt_dep => _epoch( $j_stop->rt_dep ), + isCancelled => $j_stop->is_cancelled, + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + efa_load => $j_stop->occupancy, + lat => $j_stop->latlon->[0], + lon => $j_stop->latlon->[1], + } + ] + ); + } + + # selecting on user_id and train_no avoids a race condition if a user checks + # into a new train while we are fetching data for their previous journey. In + # this case, the new train would receive data from the previous journey. + $db->update( + 'in_transit', + { + data => $json->encode($ephemeral_data), + real_arrival => $stop->rt_arr, + arr_platform => $stop->platform, + route => $json->encode( [@route] ), + }, + { + user_id => $uid, + train_id => $opt{trip_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + +sub update_arrival_motis { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stopover = $opt{stopover}; + my $json = JSON->new; + + my @route; + for my $journey_stopover ( $journey->stopovers ) { + push( + @route, + [ + $journey_stopover->stop->name, + $journey_stopover->stop->{eva} + // die('eva not set for stopover'), + { + sched_arr => _epoch( $journey_stopover->scheduled_arrival ), + sched_dep => + _epoch( $journey_stopover->scheduled_departure ), + rt_arr => _epoch( $journey_stopover->realtime_arrival ), + rt_dep => _epoch( $journey_stopover->realtime_departure ), + arr_delay => $journey_stopover->arrival_delay, + dep_delay => $journey_stopover->departure_delay, + lat => $journey_stopover->stop->lat, + lon => $journey_stopover->stop->lon, + } + ] + ); + } + + # selecting on user_id and train_no avoids a race condition if a user checks + # into a new train while we are fetching data for their previous journey. In + # this case, the new train would receive data from the previous journey. + $db->update( + 'in_transit', + { + real_arrival => $stopover->realtime_arrival, + arr_platform => $stopover->track, + route => $json->encode( [@route] ), + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_arrival_hafas { my ( $self, %opt ) = @_; my $uid = $opt{uid}; @@ -778,7 +1392,16 @@ sub update_arrival_hafas { my $stop = $opt{stop}; my $json = JSON->new; - # TODO use old rt data if available + my $res_h + = $db->select( 'in_transit', [ 'data', 'route' ], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + my $old_route = $res_h ? $res_h->{route} : []; + + if ( $stop->{rt_arr} ) { + $ephemeral_data->{rt} = 1; + } + my @route; for my $j_stop ( $journey->route ) { push( @@ -793,7 +1416,9 @@ sub update_arrival_hafas { rt_dep => _epoch( $j_stop->rt_dep ), arr_delay => $j_stop->arr_delay, dep_delay => $j_stop->dep_delay, - load => $j_stop->load + load => $j_stop->load, + lat => $j_stop->loc->lat, + lon => $j_stop->loc->lon, } ] ); @@ -802,10 +1427,6 @@ sub update_arrival_hafas { } } - my $res_h = $db->select( 'in_transit', ['route'], { user_id => $uid } ) - ->expand->hash; - my $old_route = $res_h ? $res_h->{route} : []; - for my $i ( 0 .. $#route ) { if ( $old_route->[$i] and $old_route->[$i][1] == $route[$i][1] ) { for my $k (qw(rt_arr rt_dep arr_delay dep_delay)) { @@ -820,7 +1441,9 @@ sub update_arrival_hafas { $db->update( 'in_transit', { - real_arrival => $stop->{rt_arr}, + data => $json->encode($ephemeral_data), + real_arrival => $stop->rt_arr, + arr_platform => $stop->platform, route => $json->encode( [@route] ), }, { @@ -839,6 +1462,12 @@ sub update_data { my $db = $opt{db} // $self->{pg}->db; my $new_data = $opt{data} // {}; + my %where = ( user_id => $uid ); + + if ( $opt{train_id} ) { + $where{train_id} = $opt{train_id}; + } + my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } ) ->expand->hash; @@ -848,11 +1477,7 @@ sub update_data { $data->{$k} = $v; } - $db->update( - 'in_transit', - { data => JSON->new->encode($data) }, - { user_id => $uid } - ); + $db->update( 'in_transit', { data => JSON->new->encode($data) }, \%where ); } sub update_user_data { @@ -862,6 +1487,12 @@ sub update_user_data { my $db = $opt{db} // $self->{pg}->db; my $new_data = $opt{user_data} // {}; + my %where = ( user_id => $uid ); + + if ( $opt{train_id} ) { + $where{train_id} = $opt{train_id}; + } + my $res_h = $db->select( 'in_transit', ['user_data'], { user_id => $uid } ) ->expand->hash; @@ -871,11 +1502,8 @@ sub update_user_data { $data->{$k} = $v; } - $db->update( - 'in_transit', - { user_data => JSON->new->encode($data) }, - { user_id => $uid } - ); + $db->update( 'in_transit', + { user_data => JSON->new->encode($data) }, \%where ); } sub update_visibility { diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index 97c4681..b07511a 100755 --- a/lib/Travelynx/Model/Journeys.pm +++ b/lib/Travelynx/Model/Journeys.pm @@ -4,16 +4,16 @@ package Travelynx::Model::Journeys; # # SPDX-License-Identifier: AGPL-3.0-or-later -use GIS::Distance; -use List::MoreUtils qw(after_incl before_incl); - use strict; use warnings; use 5.020; use utf8; use DateTime; +use DateTime::Format::Strptime; +use GIS::Distance; use JSON; +use List::MoreUtils qw(after_incl before_incl); my %visibility_itoa = ( 100 => 'public', @@ -118,8 +118,10 @@ sub add { my $db = $opt{db}; my $uid = $opt{uid}; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); - my $dep_station = $self->{stations}->search( $opt{dep_station} ); - my $arr_station = $self->{stations}->search( $opt{arr_station} ); + my $dep_station = $self->{stations} + ->search( $opt{dep_station}, backend_id => $opt{backend_id} ); + my $arr_station = $self->{stations} + ->search( $opt{arr_station}, backend_id => $opt{backend_id} ); if ( not $dep_station ) { return ( undef, 'Unbekannter Startbahnhof' ); @@ -167,16 +169,60 @@ sub add { my @route; if ( not $route_has_start ) { - push( @route, [ $dep_station->{name}, $dep_station->{eva}, {} ] ); + push( + @route, + [ + $dep_station->{name}, + $dep_station->{eva}, + { + lat => $dep_station->{lat}, + lon => $dep_station->{lon}, + } + ] + ); } if ( $opt{route} ) { + my $parser = DateTime::Format::Strptime->new( + pattern => '%d.%m.%Y %H:%M', + locale => 'de_DE', + time_zone => 'Europe/Berlin' + ); my @unknown_stations; + my $prev_epoch = 0; + for my $station ( @{ $opt{route} } ) { - my $station_info = $self->{stations}->search($station); + my $ts; + my %station_data; + if ( $station + =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x ) + { + $station = $+{stop}; + $ts = $parser->parse_datetime( $+{timestamp} ); + if ($ts) { + my $epoch = $ts->epoch; + if ( $epoch < $prev_epoch ) { + return ( undef, +'Zeitstempel der Unterwegshalte müssen monoton steigend sein (keine Zeitreisen und keine Portale)' + ); + } + $station_data{sched_arr} = $epoch; + $station_data{sched_dep} = $epoch; + $prev_epoch = $epoch; + } + } + my $station_info = $self->{stations} + ->search( $station, backend_id => $opt{backend_id} ); if ($station_info) { - push( @route, - [ $station_info->{name}, $station_info->{eva}, {} ] ); + $station_data{lat} = $station_info->{lat}; + $station_data{lon} = $station_info->{lon}; + push( + @route, + [ + $station_info->{name}, $station_info->{eva}, + \%station_data, + ] + ); } else { push( @route, [ $station, undef, {} ] ); @@ -198,7 +244,17 @@ sub add { } if ( not $route_has_stop ) { - push( @route, [ $arr_station->{name}, $arr_station->{eva}, {} ] ); + push( + @route, + [ + $arr_station->{name}, + $arr_station->{eva}, + { + lat => $arr_station->{lat}, + lon => $arr_station->{lon}, + } + ] + ); } my $entry = { @@ -218,6 +274,7 @@ sub add { edited => 0x3fff, cancelled => $opt{cancelled} ? 1 : 0, route => JSON->new->encode( \@route ), + backend_id => $opt{backend_id}, }; if ( $opt{comment} ) { @@ -250,8 +307,14 @@ sub add_from_in_transit { my $db = $opt{db}; my $journey = $opt{journey}; + if ( $journey->{train_id} eq 'manual' ) { + $journey->{edited} = 0x3fff; + } + else { + $journey->{edited} = 0; + } + delete $journey->{data}; - $journey->{edited} = 0; $journey->{checkout_time} = DateTime->now( time_zone => 'Europe/Berlin' ); return $db->insert( 'journeys', $journey, { returning => 'id' } ) @@ -268,10 +331,11 @@ sub update { my $rows; my $journey = $self->get_single( - uid => $uid, - db => $db, - journey_id => $journey_id, - with_datetime => 1, + uid => $uid, + db => $db, + journey_id => $journey_id, + with_datetime => 1, + with_route_datetime => 1, ); eval { @@ -515,7 +579,7 @@ sub get { my @select = ( - qw(journey_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility) + qw(journey_id is_dbris is_iris is_hafas is_motis backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_platform dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_platform arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility) ); my %where = ( user_id => $uid, @@ -573,12 +637,19 @@ sub get { my $ref = { id => $entry->{journey_id}, - type => $entry->{train_type}, + is_dbris => $entry->{is_dbris}, + is_iris => $entry->{is_iris}, + is_hafas => $entry->{is_hafas}, + is_motis => $entry->{is_motis}, + backend_name => $entry->{backend_name}, + backend_id => $entry->{backend_id}, + type => $entry->{train_type} =~ s{ \s+ $ }{}rx, line => $entry->{train_line}, no => $entry->{train_no}, from_eva => $entry->{dep_eva}, from_ds100 => $entry->{dep_ds100}, from_name => $entry->{dep_name}, + from_platform => $entry->{dep_platform}, from_latlon => [ $entry->{dep_lat}, $entry->{dep_lon} ], checkin_ts => $entry->{checkin_ts}, sched_dep_ts => $entry->{sched_dep_ts}, @@ -586,6 +657,7 @@ sub get { to_eva => $entry->{arr_eva}, to_ds100 => $entry->{arr_ds100}, to_name => $entry->{arr_name}, + to_platform => $entry->{arr_platform}, to_latlon => [ $entry->{arr_lat}, $entry->{arr_lon} ], checkout_ts => $entry->{checkout_ts}, sched_arr_ts => $entry->{sched_arr_ts}, @@ -619,6 +691,14 @@ sub get { $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 ( $ref->{rt_dep_ts} and $ref->{sched_dep_ts} ) { + $ref->{delay_dep} = $ref->{rt_dep_ts} - $ref->{sched_dep_ts}; + } + if ( $ref->{rt_arr_ts} and $ref->{sched_arr_ts} ) { + $ref->{delay_arr} = $ref->{rt_arr_ts} - $ref->{sched_arr_ts}; + } + } + if ( $opt{with_route_datetime} ) { for my $stop ( @{ $ref->{route} } ) { for my $k (qw(rt_arr rt_dep sched_arr sched_dep)) { if ( $stop->[2]{$k} ) { @@ -632,7 +712,10 @@ sub get { my $rename = $self->{renamed_station}; for my $stop ( @{ $ref->{route} } ) { if ( $stop->[0] =~ m{^Betriebsstelle nicht bekannt (\d+)$} ) { - if ( my $s = $self->{stations}->get_by_eva($1) ) { + if ( my $s + = $self->{stations} + ->get_by_eva( $1, backend_id => $ref->{backend_id} ) ) + { $stop->[0] = $s->{name}; } } @@ -767,14 +850,40 @@ sub get_oldest_ts { return undef; } -sub get_latest_checkout_station_id { +sub get_latest_checkout_latlon { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + + my $res_h = $db->select( + 'journeys_str', + [ 'arr_lat', 'arr_lon', ], + { + user_id => $uid, + cancelled => 0 + }, + { + limit => 1, + order_by => { -desc => 'journey_id' } + } + )->hash; + + if ( not $res_h ) { + return; + } + + return $res_h->{arr_lat}, $res_h->{arr_lon}; + +} + +sub get_latest_checkout_ids { my ( $self, %opt ) = @_; my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; my $res_h = $db->select( 'journeys', - ['checkout_station_id'], + [ 'checkout_station_id', 'backend_id', ], { user_id => $uid, cancelled => 0 @@ -789,7 +898,7 @@ sub get_latest_checkout_station_id { return; } - return $res_h->{checkout_station_id}; + return $res_h->{checkout_station_id}, $res_h->{backend_id}; } sub get_latest_checkout_stations { @@ -800,7 +909,13 @@ sub get_latest_checkout_stations { my $res = $db->select( 'journeys_str', - [ 'arr_name', 'arr_eva', 'train_id' ], + [ + 'arr_name', 'arr_eva', + 'arr_external_id', 'train_id', + 'backend_id', 'backend_name', + 'is_dbris', 'is_efa', + 'is_hafas', 'is_motis' + ], { user_id => $uid, cancelled => 0 @@ -821,9 +936,15 @@ sub get_latest_checkout_stations { push( @ret, { - name => $row->{arr_name}, - eva => $row->{arr_eva}, - hafas => ( $row->{train_id} =~ m{[|]} ? 1 : 0 ), + name => $row->{arr_name}, + eva => $row->{arr_eva}, + external_id_or_eva => $row->{arr_external_id} + // $row->{arr_eva}, + dbris => $row->{is_dbris} ? $row->{backend_name} : 0, + efa => $row->{is_efa} ? $row->{backend_name} : 0, + hafas => $row->{is_hafas} ? $row->{backend_name} : 0, + motis => $row->{is_motis} ? $row->{backend_name} : 0, + backend_id => $row->{backend_id}, } ); } @@ -1021,32 +1142,33 @@ sub sanity_check { if ( defined $journey->{sched_duration} and $journey->{sched_duration} <= 0 ) { - return -'Die geplante Dauer dieser Fahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.'; + return 'Die geplante Dauer dieser Fahrt ist ≤ 0.' + . ' Teleportation und Zeitreisen werden in diesem Universum nicht unterstützt.'; } if ( defined $journey->{rt_duration} and $journey->{rt_duration} <= 0 ) { - return -'Die Dauer dieser Fahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.'; + return 'Die Dauer dieser Fahrt ist ≤ 0.' + . ' Teleportation und Zeitreisen werden in diesem Universum nicht unterstützt.'; } if ( $journey->{sched_duration} - and $journey->{sched_duration} > 60 * 60 * 24 ) + and $journey->{sched_duration} > 60 * 60 * 72 ) { - return 'Die Fahrt ist länger als 24 Stunden.'; + return 'Die Fahrt ist länger als drei Tage.'; } if ( $journey->{rt_duration} - and $journey->{rt_duration} > 60 * 60 * 24 ) + and $journey->{rt_duration} > 60 * 60 * 72 ) { - return 'Die Fahrt ist länger als 24 Stunden.'; + return 'Die Fahrt ist länger als drei Tage.'; } if ( $journey->{kmh_route} > 500 or $journey->{kmh_beeline} > 500 ) { - return 'Fahrten mit über 500 km/h? Schön wär\'s.'; + return 'Die berechnete Geschwindigkeit beträgt über 500 km/h.' + . ' Das wirkt unrealistisch.'; } if ( $journey->{route} and @{ $journey->{route} } > 199 ) { my $stop_count = @{ $journey->{route} }; - return -"Die Fahrt hat $stop_count Unterwegshalte. Also ich weiß ja nicht so recht."; + return "Die Fahrt hat $stop_count Unterwegshalte. " + . ' Stimmt das wirklich?'; } if ( $journey->{edited} & 0x0010 and not $lax ) { my @unknown_stations @@ -1082,19 +1204,62 @@ sub get_travel_distance { ->warn("Journey $journey->{id} has no from_name for EVA $from_eva"); } + # Work around inconsistencies caused by a multiple EVA IDs mapping to the same station name + if ( + @{ $polyline_ref // [] } + and not List::MoreUtils::any { $_->[2] and $_->[2] == $from_eva } + @{ $polyline_ref // [] } + ) + { + $self->{log}->debug( +"Journey $journey->{id} from_eva ($from_eva) is not part of polyline" + ); + for my $entry ( @{$route_ref} ) { + if ( $entry->[0] eq $from and $entry->[1] ) { + $from_eva = $entry->[1]; + $self->{log}->debug("... setting to $from_eva"); + last; + } + } + } + if ( + @{ $polyline_ref // [] } + and not List::MoreUtils::any { $_->[2] and $_->[2] == $to_eva } + @{ $polyline_ref // [] } + ) + { + $self->{log}->debug( + "Journey $journey->{id} to_eva ($to_eva) is not part of polyline"); + for my $entry ( @{$route_ref} ) { + if ( $entry->[0] eq $to and $entry->[1] ) { + $to_eva = $entry->[1]; + $self->{log}->debug("... setting to $to_eva"); + last; + } + } + } + my $distance_polyline = 0; my $distance_intermediate = 0; - my $distance_beeline = 0; - my $skipped = 0; my $geo = GIS::Distance->new(); - my @stations = map { $_->[0] } @{$route_ref}; - my @route = after_incl { $_ eq $from } @stations; - @route = before_incl { $_ eq $to } @route; + my $distance_beeline + = $geo->distance_metal( @{$from_latlon}, @{$to_latlon} ); + my @route + = after_incl { ( $_->[1] and $_->[1] == $from_eva ) or $_->[0] eq $from } + @{$route_ref}; + @route + = before_incl { ( $_->[1] and $_->[1] == $to_eva ) or $_->[0] eq $to } + @route; - if ( @route < 2 ) { + if ( + @route < 2 + or ( $route[-1][0] ne $to + and ( not $route[-1][1] or $route[-1][1] != $to_eva ) ) + ) + { # I AM ERROR - return ( 0, 0, 0 ); + return ( 0, 0, $distance_beeline ); } my @polyline = after_incl { $_->[2] and $_->[2] == $from_eva } @@ -1102,34 +1267,32 @@ sub get_travel_distance { @polyline = before_incl { $_->[2] and $_->[2] == $to_eva } @polyline; - my $prev_station = shift @polyline; - for my $station (@polyline) { - $distance_polyline += $geo->distance_metal( - $prev_station->[1], $prev_station->[0], - $station->[1], $station->[0] - ); - $prev_station = $station; - } - - $prev_station = $self->{latlon_by_station}->{ shift @route }; - if ( not $prev_station ) { - return ( $distance_polyline, 0, 0 ); - } - - for my $station_name (@route) { - if ( my $station = $self->{latlon_by_station}->{$station_name} ) { - $distance_intermediate += $geo->distance_metal( - $prev_station->[0], $prev_station->[1], - $station->[0], $station->[1] + # ensure that before_incl matched -- otherwise, @polyline is too long + if ( @polyline and $polyline[-1][2] == $to_eva ) { + my $prev_station = shift @polyline; + for my $station (@polyline) { + $distance_polyline += $geo->distance_metal( + $prev_station->[1], $prev_station->[0], + $station->[1], $station->[0] ); $prev_station = $station; } } - $distance_beeline = $geo->distance_metal( @{$from_latlon}, @{$to_latlon} ); + if ( defined $route[0][2]{lat} and defined $route[0][2]{lon} ) { + my $prev_station = shift @route; + for my $station (@route) { + if ( defined $station->[2]{lat} and defined $station->[2]{lon} ) { + $distance_intermediate += $geo->distance_metal( + $prev_station->[2]{lat}, $prev_station->[2]{lon}, + $station->[2]{lat}, $station->[2]{lon} + ); + $prev_station = $station; + } + } + } - return ( $distance_polyline, $distance_intermediate, - $distance_beeline, $skipped ); + return ( $distance_polyline, $distance_intermediate, $distance_beeline ); } sub grep_single { @@ -1548,7 +1711,10 @@ sub compute_stats { @inconsistencies, { conflict => { - train => $journey->{type} . ' ' + train => ( + $journey->{is_motis} ? '' : $journey->{type} + ) + . ' ' . ( $journey->{line} // $journey->{no} ), arr => epoch_to_dt( $journey->{rt_arr_ts} ) ->strftime('%d.%m.%Y %H:%M'), @@ -1574,7 +1740,8 @@ sub compute_stats { $next_departure = $journey->{rt_dep_ts}; $next_id = $journey->{id}; $next_train - = $journey->{type} . ' ' . ( $journey->{line} // $journey->{no} ),; + = ( $journey->{is_motis} ? '' : $journey->{type} ) . ' ' + . ( $journey->{line} // $journey->{no} ),; } my $ret = { km_route => $km_route, @@ -1607,6 +1774,8 @@ sub compute_stats { sub get_stats { my ( $self, %opt ) = @_; + $self->{log}->debug("get_stats"); + if ( $opt{cancelled} ) { $self->{log} ->warn('get_journey_stats called with illegal option cancelled => 1'); @@ -1633,9 +1802,12 @@ sub get_stats { ) ) { + $self->{log}->debug("got cached journey stats for $year/$month"); return $stats; } + $self->{log}->debug("computing journey stats for $year/$month"); + my $interval_start = DateTime->new( time_zone => 'Europe/Berlin', year => 2000, @@ -1694,28 +1866,29 @@ sub get_stats { return $stats; } -sub get_latest_dest_id { +sub get_latest_dest_ids { my ( $self, %opt ) = @_; my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; if ( - my $id = $self->{in_transit}->get_checkout_station_id( + my ( $id, $backend_id ) = $self->{in_transit}->get_checkout_ids( uid => $uid, db => $db ) ) { - return $id; + return ( $id, $backend_id ); } - return $self->get_latest_checkout_station_id( + return $self->get_latest_checkout_ids( uid => $uid, db => $db ); } +# Returns a listref of {eva, name} hashrefs for the specified backend. sub get_connection_targets { my ( $self, %opt ) = @_; @@ -1724,21 +1897,32 @@ sub get_connection_targets { // 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}; if ( $opt{destination_name} ) { - return ( - [], - [ { eva => $opt{eva}, name => $opt{destination_name} } ] - ); + return { + eva => $opt{eva}, + name => $opt{destination_name} + }; } - my $dest_id = $opt{eva} // $self->get_latest_dest_id(%opt); + my $backend_id = $opt{backend_id}; if ( not $dest_id ) { - return ( [], [] ); + ( $dest_id, $backend_id ) = $self->get_latest_dest_ids(%opt); } - my $dest_ids = [ $dest_id, $self->{stations}->get_meta( eva => $dest_id ) ]; + if ( not $dest_id ) { + return; + } + + my $dest_ids = [ + $dest_id, + $self->{stations}->get_meta( + eva => $dest_id, + backend_id => $backend_id, + ) + ]; my $res = $db->select( 'journeys', @@ -1746,7 +1930,8 @@ sub get_connection_targets { { user_id => $uid, checkin_station_id => $dest_ids, - real_departure => { '>', $threshold } + real_departure => { '>', $threshold }, + backend_id => $opt{backend_id}, }, { group_by => ['checkout_station_id'], @@ -1755,9 +1940,13 @@ sub get_connection_targets { ); my @destinations = $res->hashes->grep( sub { shift->{count} >= $min_count } ) - ->map( sub { shift->{dest} } )->each; - @destinations = $self->{stations}->get_by_evas(@destinations); - return ( $dest_ids, \@destinations ); + ->map( sub { shift->{dest} } ) + ->each; + @destinations = $self->{stations}->get_by_evas( + backend_id => $opt{backend_id}, + evas => [@destinations] + ); + return @destinations; } sub update_visibility { diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm index ac4019c..c6d9730 100644 --- a/lib/Travelynx/Model/Stations.pm +++ b/lib/Travelynx/Model/Stations.pm @@ -1,6 +1,7 @@ package Travelynx::Model::Stations; # Copyright (C) 2022 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -14,56 +15,319 @@ sub new { return bless( \%opt, $class ); } +sub get_backend_id { + my ( $self, %opt ) = @_; + + if ( $opt{iris} ) { + + # special case + return 0; + } + if ( $opt{dbris} and $self->{backend_id}{dbris}{ $opt{dbris} } ) { + return $self->{backend_id}{dbris}{ $opt{dbris} }; + } + if ( $opt{efa} and $self->{backend_id}{efa}{ $opt{efa} } ) { + return $self->{backend_id}{efa}{ $opt{efa} }; + } + if ( $opt{hafas} and $self->{backend_id}{hafas}{ $opt{hafas} } ) { + return $self->{backend_id}{hafas}{ $opt{hafas} }; + } + if ( $opt{motis} and $self->{backend_id}{motis}{ $opt{motis} } ) { + return $self->{backend_id}{motis}{ $opt{motis} }; + } + + my $db = $opt{db} // $self->{pg}->db; + my $backend_id = 0; + + if ( $opt{dbris} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + dbris => 1, + name => $opt{dbris} + } + )->hash->{id}; + $self->{backend_id}{dbris}{ $opt{dbris} } = $backend_id; + } + elsif ( $opt{efa} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + efa => 1, + name => $opt{efa} + } + )->hash->{id}; + $self->{backend_id}{efa}{ $opt{efa} } = $backend_id; + } + elsif ( $opt{hafas} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + hafas => 1, + name => $opt{hafas} + } + )->hash->{id}; + $self->{backend_id}{hafas}{ $opt{hafas} } = $backend_id; + } + elsif ( $opt{motis} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + motis => 1, + name => $opt{motis} + } + )->hash->{id}; + $self->{backend_id}{motis}{ $opt{motis} } = $backend_id; + } + + return $backend_id; +} + +sub get_backend { + my ( $self, %opt ) = @_; + + if ( $self->{backend_cache}{ $opt{backend_id} } ) { + return $self->{backend_cache}{ $opt{backend_id} }; + } + + my $db = $opt{db} // $self->{pg}->db; + my $ret = $db->select( + 'backends', + '*', + { + id => $opt{backend_id}, + } + )->hash; + + $self->{backend_cache}{ $opt{backend_id} } = $ret; + + return $ret; +} + +sub get_backends { + my ( $self, %opt ) = @_; + + $opt{db} //= $self->{pg}->db; + + my $res = $opt{db}->select( 'backends', + [ 'id', 'name', 'dbris', 'efa', 'hafas', 'iris', 'motis' ] ); + my @ret; + + while ( my $row = $res->hash ) { + push( + @ret, + { + id => $row->{id}, + name => $row->{name}, + dbris => $row->{dbris}, + efa => $row->{efa}, + hafas => $row->{hafas}, + iris => $row->{iris}, + motis => $row->{motis}, + } + ); + } + + return @ret; +} + +# Slow for MOTIS backends sub add_or_update { my ( $self, %opt ) = @_; - my $stop = $opt{stop}; - my $loc = $stop->loc; - my $source = 1; - my $db = $opt{db} // $self->{pg}->db; + my $stop = $opt{stop}; + $opt{db} //= $self->{pg}->db; + + $opt{backend_id} //= $self->get_backend_id(%opt); + + if ( $opt{dbris} ) { + if ( + my $s = $self->get_by_eva( + $stop->eva, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( + 'stations', + { + name => $stop->name, + lat => $stop->lat, + lon => $stop->lon, + archived => 0 + }, + { + eva => $stop->eva, + source => $opt{backend_id} + } + ); + return; + } + $opt{db}->insert( + 'stations', + { + eva => $stop->eva, + name => $stop->name, + lat => $stop->lat, + lon => $stop->lon, + source => $opt{backend_id}, + archived => 0 + } + ); + return; + } + + if ( $opt{efa} ) { + if ( + my $s = $self->get_by_eva( + $stop->id_num, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( + 'stations', + { + name => $stop->full_name, + lat => $stop->latlon->[0], + lon => $stop->latlon->[1], + archived => 0 + }, + { + eva => $stop->id_num, + source => $opt{backend_id} + } + ); + return; + } + if (not $stop->latlon) { + die('Backend Error: Stop "' . $stop->full_name . '" has no geo coordinates'); + } + $opt{db}->insert( + 'stations', + { + eva => $stop->id_num, + name => $stop->full_name, + lat => $stop->latlon->[0], + lon => $stop->latlon->[1], + source => $opt{backend_id}, + archived => 0 + } + ); + return; + } + + if ( $opt{motis} ) { + if ( + my $s = $self->get_by_external_id( + external_id => $stop->id, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( + 'stations', + { + name => $stop->name, + lat => $stop->lat, + lon => $stop->lon, + archived => 0 + }, + { + eva => $s->{eva}, + source => $opt{backend_id} + } + ); - if ( my $s = $self->get_by_eva( $loc->eva, db => $db ) ) { - if ( $source == 1 and $s->{source} == 0 and not $s->{archived} ) { + # MOTIS backends do not provide a numeric ID, so we set our ID here. + $stop->{eva} = $s->{eva}; return; } - $db->update( + + my $s = $opt{db}->query( + qq { + with new_station as ( + insert into stations_external_ids (backend_id, external_id) + values (?, ?) + returning eva, backend_id + ) + + insert into stations (eva, name, lat, lon, source, archived) + values ((select eva from new_station), ?, ?, ?, (select backend_id from new_station), ?) + returning * + }, + ( + $opt{backend_id}, $stop->id, $stop->name, + $stop->lat, $stop->lon, 0, + ) + ); + + # MOTIS backends do not provide a numeric ID, so we set our ID here. + $stop->{eva} = $s->hash->{eva}; + return; + } + + my $loc = $stop->loc; + if ( + my $s = $self->get_by_eva( + $loc->eva, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( 'stations', { name => $loc->name, lat => $loc->lat, lon => $loc->lon, - source => $source, archived => 0 }, - { eva => $loc->eva } + { + eva => $loc->eva, + source => $opt{backend_id} + } ); return; } - $db->insert( + $opt{db}->insert( 'stations', { eva => $loc->eva, name => $loc->name, lat => $loc->lat, lon => $loc->lon, - source => $source, + source => $opt{backend_id}, archived => 0 } ); + + return; } sub add_meta { my ( $self, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; my $eva = $opt{eva}; my @meta = @{ $opt{meta} }; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); + for my $meta (@meta) { if ( $meta != $eva ) { - $db->insert( + $opt{db}->insert( 'related_stations', { - eva => $eva, - meta => $meta + eva => $eva, + meta => $meta, + backend_id => $opt{backend_id}, }, { on_conflict => undef } ); @@ -74,7 +338,7 @@ sub add_meta { sub get_db_iterator { my ($self) = @_; - return $self->{pg}->db->select( 'stations', '*' ); + return $self->{pg}->db->select( 'stations_str', '*' ); } sub get_meta { @@ -82,7 +346,16 @@ sub get_meta { my $db = $opt{db} // $self->{pg}->db; my $eva = $opt{eva}; - my $res = $db->select( 'related_stations', ['meta'], { eva => $eva } ); + $opt{backend_id} //= $self->get_backend_id( %opt, db => $db ); + + my $res = $db->select( + 'related_stations', + ['meta'], + { + eva => $eva, + backend_id => $opt{backend_id} + } + ); my @ret; while ( my $row = $res->hash ) { @@ -93,9 +366,12 @@ sub get_meta { } sub get_for_autocomplete { - my ($self) = @_; + my ( $self, %opt ) = @_; - my $res = $self->{pg}->db->select( 'stations', ['name'] ); + $opt{backend_id} //= $self->get_backend_id(%opt); + + my $res = $self->{pg} + ->db->select( 'stations', ['name'], { source => $opt{backend_id} } ); my %ret; while ( my $row = $res->hash ) { @@ -113,43 +389,74 @@ sub get_by_eva { return; } - my $db = $opt{db} // $self->{pg}->db; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); - return $db->select( 'stations', '*', { eva => $eva } )->hash; + return $opt{db}->select( + 'stations', + '*', + { + eva => $eva, + source => $opt{backend_id} + } + )->hash; } -# Fast -sub get_by_evas { - my ( $self, @evas ) = @_; +# Slow +sub get_by_external_id { + my ( $self, %opt ) = @_; - my @ret - = $self->{pg}->db->select( 'stations', '*', { eva => { '=', \@evas } } ) - ->hashes->each; - return @ret; + if ( not $opt{external_id} ) { + return; + } + + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); + + return $opt{db}->select( + 'stations_with_external_ids', + '*', + { + external_id => $opt{external_id}, + source => $opt{backend_id}, + } + )->hash; } -# Slow -sub get_latlon_by_name { +# Fast +sub get_by_evas { my ( $self, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); - my %location; - my $res = $db->select( 'stations', [ 'name', 'lat', 'lon' ] ); - while ( my $row = $res->hash ) { - $location{ $row->{name} } = [ $row->{lat}, $row->{lon} ]; - } - return \%location; + my @ret = $self->{pg}->db->select( + 'stations', + '*', + { + eva => { '=', $opt{evas} }, + source => $opt{backend_id} + } + )->hashes->each; + return @ret; } # Slow sub get_by_name { my ( $self, $name, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); - return $db->select( 'stations', '*', { name => $name }, { limit => 1 } ) - ->hash; + return $opt{db}->select( + 'stations', + '*', + { + name => $name, + source => $opt{backend_id} + }, + { limit => 1 } + )->hash; } # Slow @@ -166,16 +473,27 @@ sub get_by_names { sub get_by_ds100 { my ( $self, $ds100, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); - return $db->select( 'stations', '*', { ds100 => $ds100 }, { limit => 1 } ) - ->hash; + return $opt{db}->select( + 'stations', + '*', + { + ds100 => $ds100, + source => $opt{backend_id} + }, + { limit => 1 } + )->hash; } # Can be slow sub search { my ( $self, $identifier, %opt ) = @_; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); + if ( $identifier =~ m{ ^ \d+ $ }x ) { return $self->get_by_eva( $identifier, %opt ) // $self->get_by_ds100( $identifier, %opt ) diff --git a/lib/Travelynx/Model/Traewelling.pm b/lib/Travelynx/Model/Traewelling.pm index 25648cc..608da15 100644 --- a/lib/Travelynx/Model/Traewelling.pm +++ b/lib/Travelynx/Model/Traewelling.pm @@ -224,6 +224,7 @@ sub get_pushable_accounts { join in_transit_str as i on t.user_id = i.user_id where t.push_sync = True and i.arr_eva is not null + and i.backend_id = (select id from backends where dbris = true and name = 'bahn.de') and i.cancelled = False } ); diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 4602fa2..be9e80b 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -40,14 +40,6 @@ my %predicate_atoi = ( is_blocked_by => 3, ); -my @sb_templates = ( - undef, - [ 'DBF', 'https://dbf.finalrewind.org/{name}?rt=1#{tt}{tn}' ], - [ 'bahn.expert', 'https://bahn.expert/{name}#{id}' ], - [ 'DBF HAFAS', 'https://dbf.finalrewind.org/{name}?rt=1&hafas=1#{tt}{tn}' ], - [ 'bahn.expert/regional', 'https://bahn.expert/regional/{name}#{id}' ], -); - my %token_id = ( status => 1, history => 2, @@ -213,6 +205,17 @@ sub get_privacy_by { return; } +sub set_backend { + my ( $self, %opt ) = @_; + $opt{db} //= $self->{pg}->db; + + $opt{db}->update( + 'users', + { backend_id => $opt{backend_id} }, + { id => $opt{uid} } + ); +} + sub set_privacy { my ( $self, %opt ) = @_; my $db = $opt{db} // $self->{pg}->db; @@ -409,12 +412,13 @@ sub get { my $uid = $opt{uid}; my $user = $db->select( - 'users', + 'users_with_backend', 'id, name, status, public_level, email, ' - . 'external_services, accept_follows, notifications, ' + . 'accept_follows, notifications, ' . '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', + . 'extract(epoch from deletion_requested) as deletion_requested_ts, ' + . 'backend_id, backend_name, dbris, efa, hafas, motis', { id => $uid } )->hash; if ($user) { @@ -435,12 +439,8 @@ sub get { past_status => $user->{public_level} & 0x08000 ? 1 : 0, past_all => $user->{public_level} & 0x10000 ? 1 : 0, email => $user->{email}, - sb_name => $user->{external_services} - ? $sb_templates[ $user->{external_services} & 0x07 ][0] - : undef, - sb_template => $user->{external_services} - ? $sb_templates[ $user->{external_services} & 0x07 ][1] - : undef, + sb_template => +'https://dbf.finalrewind.org/{name}?dbris={dbris}&efa={efa}&hafas={hafas}&motis={motis}#{id_or_tttn}', registered_at => DateTime->from_epoch( epoch => $user->{registered_at_ts}, time_zone => 'Europe/Berlin' @@ -455,6 +455,12 @@ sub get { time_zone => 'Europe/Berlin' ) : undef, + backend_id => $user->{backend_id}, + backend_name => $user->{backend_name}, + backend_dbris => $user->{dbris}, + backend_efa => $user->{efa}, + backend_hafas => $user->{hafas}, + backend_motis => $user->{motis}, }; } return undef; @@ -659,24 +665,6 @@ sub use_history { } } -sub use_external_services { - my ( $self, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; - my $uid = $opt{uid}; - my $value = $opt{set}; - - if ( defined $value ) { - if ( $value < 0 or $value > 4 ) { - $value = 0; - } - $db->update( 'users', { external_services => $value }, { id => $uid } ); - } - else { - return $db->select( 'users', ['external_services'], { id => $uid } ) - ->hash->{external_services}; - } -} - sub get_webhook { my ( $self, %opt ) = @_; my $db = $opt{db} // $self->{pg}->db; |