summaryrefslogtreecommitdiff
path: root/lib/Travelynx/Helper
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx/Helper')
-rw-r--r--lib/Travelynx/Helper/DBDB.pm36
-rw-r--r--lib/Travelynx/Helper/HAFAS.pm388
-rw-r--r--lib/Travelynx/Helper/IRIS.pm145
-rw-r--r--lib/Travelynx/Helper/Sendmail.pm33
-rw-r--r--lib/Travelynx/Helper/Traewelling.pm235
5 files changed, 500 insertions, 337 deletions
diff --git a/lib/Travelynx/Helper/DBDB.pm b/lib/Travelynx/Helper/DBDB.pm
index 4baf3ed..b98a372 100644
--- a/lib/Travelynx/Helper/DBDB.pm
+++ b/lib/Travelynx/Helper/DBDB.pm
@@ -1,6 +1,6 @@
package Travelynx::Helper::DBDB;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -30,11 +30,11 @@ sub has_wagonorder_p {
my ( $self, $ts, $train_no ) = @_;
my $api_ts = $ts->strftime('%Y%m%d%H%M');
my $url
- = "https://lib.finalrewind.org/dbdb/has_wagonorder/${train_no}/${api_ts}";
+ = "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}";
my $cache = $self->{cache};
my $promise = Mojo::Promise->new;
- if ( my $content = $cache->get($url) ) {
+ if ( my $content = $cache->get("HEAD $url") ) {
if ( $content eq 'n' ) {
return $promise->reject;
}
@@ -43,24 +43,23 @@ sub has_wagonorder_p {
}
}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
+ $self->{user_agent}->request_timeout(5)->head_p( $url => $self->{header} )
->then(
sub {
my ($tx) = @_;
if ( $tx->result->is_success ) {
- my $body = $tx->result->body;
- $cache->set( $url, $body );
- $promise->resolve($body);
+ $cache->set( "HEAD $url", 'a' );
+ $promise->resolve('a');
}
else {
- $cache->set( $url, 'n' );
+ $cache->set( "HEAD $url", 'n' );
$promise->reject;
}
return;
}
)->catch(
sub {
- $cache->set( $url, 'n' );
+ $cache->set( "HEAD $url", 'n' );
$promise->reject;
return;
}
@@ -74,11 +73,6 @@ sub get_wagonorder_p {
my $url
= "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}";
- if ( $api !~ m{i} and $api =~ m{a} ) {
- $url
- = "https://www.apps-bahn.de/wr/wagenreihung/1.0/${train_no}/${api_ts}";
- }
-
my $cache = $self->{cache};
my $promise = Mojo::Promise->new;
@@ -91,11 +85,17 @@ sub get_wagonorder_p {
->then(
sub {
my ($tx) = @_;
- my $body = decode( 'utf-8', $tx->res->body );
- my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
- $promise->resolve($json);
+ if ( $tx->result->is_success ) {
+ my $body = decode( 'utf-8', $tx->res->body );
+ my $json = JSON->new->decode($body);
+ $cache->freeze( $url, $json );
+ $promise->resolve($json);
+ }
+ else {
+ my $code = $tx->code;
+ $promise->reject("HTTP ${code}");
+ }
return;
}
)->catch(
diff --git a/lib/Travelynx/Helper/HAFAS.pm b/lib/Travelynx/Helper/HAFAS.pm
index 6fd5c71..7671d78 100644
--- a/lib/Travelynx/Helper/HAFAS.pm
+++ b/lib/Travelynx/Helper/HAFAS.pm
@@ -1,6 +1,6 @@
package Travelynx::Helper::HAFAS;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -12,7 +12,13 @@ use DateTime;
use Encode qw(decode);
use JSON;
use Mojo::Promise;
-use XML::LibXML;
+use Travel::Status::DE::HAFAS;
+
+sub _epoch {
+ my ($dt) = @_;
+
+ return $dt ? $dt->epoch : 0;
+}
sub new {
my ( $class, %opt ) = @_;
@@ -27,15 +33,16 @@ sub new {
return bless( \%opt, $class );
}
-sub get_polyline_p {
- my ( $self, $train, $trip_id ) = @_;
+sub get_json_p {
+ my ( $self, $url, %opt ) = @_;
- my $line = $train->line // 0;
- my $url
- = "https://v5.db.transport.rest/trips/${trip_id}?lineName=${line}&polyline=true";
my $cache = $self->{main_cache};
my $promise = Mojo::Promise->new;
- my $version = $self->{version};
+
+ if ( $opt{realtime} ) {
+ $cache = $self->{realtime_cache};
+ }
+ $opt{encoding} //= 'ISO-8859-15';
if ( my $content = $cache->thaw($url) ) {
return $promise->resolve($content);
@@ -48,241 +55,260 @@ sub get_polyline_p {
if ( my $err = $tx->error ) {
$promise->reject(
-"hafas->get_polyline_p($url) returned HTTP $err->{code} $err->{message}"
+"hafas->get_json_p($url) returned HTTP $err->{code} $err->{message}"
);
return;
}
- my $body = decode( 'utf-8', $tx->res->body );
- my $json = JSON->new->decode($body);
- my @station_list;
- my @coordinate_list;
-
- for my $feature ( @{ $json->{polyline}{features} } ) {
- if ( exists $feature->{geometry}{coordinates} ) {
- my $coord = $feature->{geometry}{coordinates};
- if ( exists $feature->{properties}{type}
- and $feature->{properties}{type} eq 'stop' )
- {
- push( @{$coord}, $feature->{properties}{id} );
- push( @station_list, $feature->{properties}{name} );
- }
- push( @coordinate_list, $coord );
- }
- }
+ my $body = decode( $opt{encoding}, $tx->res->body );
- my $ret = {
- name => $json->{line}{name} // '?',
- polyline => [@coordinate_list],
- raw => $json,
- };
-
- $cache->freeze( $url, $ret );
-
- # borders ("(Gr)" as in "Grenze") are only returned by HAFAS.
- # They are not stations.
- my $iris_stations = join( '|', $train->route );
- my $hafas_stations
- = join( '|', grep { $_ !~ m{\(Gr\)$} } @station_list );
-
- # Do not return polyline if it belongs to an entirely different
- # train. Trains with longer routes (e.g. due to train number
- # changes, which are handled by HAFAS but left out in IRIS)
- # are okay though.
- if ( $iris_stations ne $hafas_stations
- and index( $hafas_stations, $iris_stations ) == -1 )
- {
- $self->{log}->info( 'Ignoring polyline for '
- . $train->line
- . ": IRIS route does not agree with HAFAS route: $iris_stations != $hafas_stations"
- );
- $promise->reject(
- "hafas->get_polyline_p($url): polyline route mismatch");
- }
- else {
- $promise->resolve($ret);
- }
+ $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) = @_;
- $promise->reject("hafas->get_polyline_p($url): $err");
+ $self->{log}->info("hafas->get_json_p($url): $err");
+ $promise->reject("hafas->get_json_p($url): $err");
return;
}
)->wait;
-
return $promise;
}
-sub get_json_p {
- my ( $self, $url ) = @_;
+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::HAFAS->new_p(
+ 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),
+ );
+}
- my $cache = $self->{main_cache};
- my $promise = Mojo::Promise->new;
+sub search_location_p {
+ my ( $self, %opt ) = @_;
- if ( my $content = $cache->thaw($url) ) {
- return $promise->resolve($content);
- }
+ return Travel::Status::DE::HAFAS->new_p(
+ locationSearch => $opt{query},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(5),
+ );
+}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
- ->then(
+sub get_tripid_p {
+ my ( $self, %opt ) = @_;
+
+ my $promise = Mojo::Promise->new;
+
+ my $train = $opt{train};
+ my $train_desc = $train->type . ' ' . $train->train_no;
+ $train_desc =~ s{^- }{};
+
+ Travel::Status::DE::HAFAS->new_p(
+ journeyMatch => $train_desc,
+ datetime => $train->start,
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(10),
+ )->then(
sub {
- my ($tx) = @_;
+ my ($hafas) = @_;
+ my @results = $hafas->results;
- if ( my $err = $tx->error ) {
+ if ( not @results ) {
$promise->reject(
-"hafas->get_json_p($url) returned HTTP $err->{code} $err->{message}"
- );
+ "journeyMatch($train_desc) returned no results");
return;
}
- my $body = decode( 'ISO-8859-15', $tx->res->body );
+ my $result = $results[0];
+ if ( @results > 1 ) {
+ for my $journey (@results) {
+ if ( ( $journey->route )[0]->loc->name eq $train->origin ) {
+ $result = $journey;
+ last;
+ }
+ }
+ }
- $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);
+ $promise->resolve( $result->id );
return;
}
)->catch(
sub {
my ($err) = @_;
- $self->{log}->info("hafas->get_json_p($url): $err");
- $promise->reject("hafas->get_json_p($url): $err");
+ $promise->reject($err);
return;
}
)->wait;
+
return $promise;
}
-sub get_xml_p {
- my ( $self, $url ) = @_;
+sub get_journey_p {
+ my ( $self, %opt ) = @_;
- my $cache = $self->{realtime_cache};
my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- return $promise->resolve($content);
- }
-
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
- ->then(
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ Travel::Status::DE::HAFAS->new_p(
+ 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),
+ )->then(
sub {
- my ($tx) = @_;
+ my ($hafas) = @_;
+ my $journey = $hafas->result;
- if ( my $err = $tx->error ) {
- $promise->reject(
-"hafas->get_xml_p($url) returned HTTP $err->{code} $err->{message}"
- );
+ if ($journey) {
+ $promise->resolve($journey);
return;
}
+ $promise->reject('no journey');
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
- my $body = decode( 'ISO-8859-15', $tx->res->body );
- my $tree;
-
- my $traininfo = {
- station => {},
- messages => [],
- };
-
- # <SDay text="... &gt; ..."> is invalid XML, but present in
- # regardless. As it is the last tag, we just throw it away.
- $body =~ s{<SDay [^>]*/>}{}s;
-
- # More fixes for invalid XML
- $body =~ s{P&R}{P&amp;R};
- $body =~ s{Wagen \d+ \K&}{&amp;};
- $body =~ s{Wagen \d+, \d+ \K&}{&amp;};
-
- # <Attribute [...] text="[...]"[...]"" /> is invalid XML.
- # Work around it.
- $body
- =~ s{<Attribute([^>]+)text="([^"]*)"([^"=>]*)""}{<Attribute$1text="$2&#042;$3&#042;"}s;
-
- # Same for <HIMMessage lead="[...]"[...]"[...]" />
- $body
- =~ s{<HIMMessage([^>]+)lead="([^"]*)"([^"=>]*)"([^"]*)"}{<Attribute$1text="$2&#042;$3&#042;$4"}s;
-
- # ... and <HIMMessage [...] lead="[...]<>[...]">
- # (replace <> with t$t)
- while ( $body
- =~ s{<HIMMessage([^>]+)lead="([^"]*)<>([^"=]*)"}{<HIMMessage$1lead="$2&#11020;$3"}gis
- )
- {
- }
-
- # Dito for <HIMMessage [...] lead="[...]<br>[...]">.
- while ( $body
- =~ s{<HIMMessage([^>]+)lead="([^"]*)<br/?>([^"=]*)"}{<HIMMessage$1lead="$2 $3"}is
- )
- {
- }
-
- # ... and any other HTML tag inside an XML attribute
- while ( $body
- =~ s{<HIMMessage([^>]+)lead="([^"]*)<[^>]+>([^"=]*)"}{<HIMMessage$1lead="$2$3"}is
- )
- {
- }
+ return $promise;
+}
- eval { $tree = XML::LibXML->load_xml( string => $body ) };
- if ( my $err = $@ ) {
- if ( $err =~ m{extra content at the end}i ) {
+sub get_route_timestamps_p {
+ my ( $self, %opt ) = @_;
- # We requested XML, but received an HTML error page
- # (which was returned with HTTP 200 OK).
- $self->{log}->debug("load_xml($url): $err");
+ my $promise = Mojo::Promise->new;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ Travel::Status::DE::HAFAS->new_p(
+ journey => {
+ id => $opt{trip_id},
+
+ # name => $opt{train_no},
+ },
+ with_polyline => $opt{with_polyline},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(10),
+ )->then(
+ sub {
+ my ($hafas) = @_;
+ my $journey = $hafas->result;
+ 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 } = {
+ name => $stop->loc->name,
+ eva => $stop->loc->eva,
+ sched_arr => _epoch( $stop->sched_arr ),
+ sched_dep => _epoch( $stop->sched_dep ),
+ rt_arr => _epoch( $stop->rt_arr ),
+ rt_dep => _epoch( $stop->rt_dep ),
+ arr_delay => $stop->arr_delay,
+ dep_delay => $stop->dep_delay,
+ load => $stop->load
+ };
+ if ( $stop->tz_offset ) {
+ $ret->{$name}{tz_offset} = $stop->tz_offset;
}
- else {
- # There is invalid XML which we might be able to fix via
- # regular expressions, so dump it into the production log.
- $self->{log}->info("load_xml($url): $err");
+ if ( ( $stop->arr_cancelled or not $stop->sched_arr )
+ and ( $stop->dep_cancelled or not $stop->sched_dep ) )
+ {
+ $ret->{$name}{isCancelled} = 1;
}
- $cache->freeze( $url, $traininfo );
- $promise->reject("hafas->get_xml_p($url): $err");
- return;
+ if (
+ $station_is_past
+ and not $ret->{$name}{isCancelled}
+ and $now->epoch < (
+ $ret->{$name}{rt_arr} // $ret->{$name}{rt_dep}
+ // $ret->{$name}{sched_arr}
+ // $ret->{$name}{sched_dep} // $now->epoch
+ )
+ )
+ {
+ $station_is_past = 0;
+ }
+ $ret->{$name}{isPast} = $station_is_past;
}
- for my $station ( $tree->findnodes('/Journey/St') ) {
- my $name = $station->getAttribute('name');
- my $adelay = $station->getAttribute('adelay');
- my $ddelay = $station->getAttribute('ddelay');
- $traininfo->{station}{$name} = {
- adelay => $adelay,
- ddelay => $ddelay,
- };
- }
+ if ( $journey->polyline ) {
+ my @station_list;
+ my @coordinate_list;
- for my $message ( $tree->findnodes('/Journey/HIMMessage') ) {
- my $header = $message->getAttribute('header');
- my $lead = $message->getAttribute('lead');
- my $display = $message->getAttribute('display');
- push(
- @{ $traininfo->{messages} },
- {
- header => $header,
- lead => $lead,
- display => $display
+ for my $coord ( $journey->polyline ) {
+ if ( $coord->{name} ) {
+ push( @coordinate_list,
+ [ $coord->{lon}, $coord->{lat}, $coord->{eva} ] );
+ push( @station_list, $coord->{name} );
}
- );
+ else {
+ push( @coordinate_list,
+ [ $coord->{lon}, $coord->{lat} ] );
+ }
+ }
+ my $iris_stations = join( '|', $opt{train}->route );
+
+ # borders (Gr" as in "Grenze") are only returned by HAFAS.
+ # They are not stations.
+ my $hafas_stations
+ = join( '|', grep { $_ !~ m{(\(Gr\)|\)Gr)$} } @station_list );
+
+ if ( $iris_stations eq $hafas_stations
+ or index( $hafas_stations, $iris_stations ) != -1 )
+ {
+ $polyline = {
+ from_eva => ( $journey->route )[0]->loc->eva,
+ to_eva => ( $journey->route )[-1]->loc->eva,
+ coords => \@coordinate_list,
+ };
+ }
+ else {
+ $self->{log}->debug( 'Ignoring polyline for '
+ . $opt{train}->line
+ . ": IRIS route does not agree with HAFAS route: $iris_stations != $hafas_stations"
+ );
+ }
}
- $cache->freeze( $url, $traininfo );
- $promise->resolve($traininfo);
+ $promise->resolve( $ret, $journey, $polyline );
return;
}
)->catch(
sub {
my ($err) = @_;
- $self->{log}->info("hafas->get_xml_p($url): $err");
- $promise->reject("hafas->get_xml_p($url): $err");
+ $promise->reject($err);
return;
}
)->wait;
+
return $promise;
}
diff --git a/lib/Travelynx/Helper/IRIS.pm b/lib/Travelynx/Helper/IRIS.pm
index 3c4fba1..deed79a 100644
--- a/lib/Travelynx/Helper/IRIS.pm
+++ b/lib/Travelynx/Helper/IRIS.pm
@@ -1,6 +1,6 @@
package Travelynx::Helper::IRIS;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -10,7 +10,10 @@ use 5.020;
use utf8;
+use Mojo::Promise;
+use Mojo::UserAgent;
use Travel::Status::DE::IRIS;
+use Travel::Status::DE::IRIS::Stations;
sub new {
my ( $class, %opt ) = @_;
@@ -21,10 +24,20 @@ sub new {
sub get_departures {
my ( $self, %opt ) = @_;
my $station = $opt{station};
- my $lookbehind = $opt{lookbehind} // 180;
- my $lookahead = $opt{lookahead} // 30;
+ my $lookbehind = $opt{lookbehind} // 180;
+ my $lookahead = $opt{lookahead} // 30;
my $with_related = $opt{with_related} // 0;
+ # Berlin Hbf exists twice:
+ # - BLS / 8011160
+ # - BL / 8098160 (formerly "Berlin Hbf (tief)")
+ # Right now, travelynx assumes that station name -> EVA / DS100 is a unique
+ # map. This is not the case. Work around it here until travelynx has been
+ # adjusted properly.
+ if ( $station eq 'Berlin Hbf' or $station eq '8011160' ) {
+ $with_related = 1;
+ }
+
my @station_matches
= Travel::Status::DE::IRIS::Stations::get_station($station);
@@ -48,8 +61,8 @@ sub get_departures {
with_related => $with_related,
);
return {
- results => [ $status->results ],
- errstr => $status->errstr,
+ results => [ $status->results ],
+ errstr => $status->errstr,
station_ds100 =>
( $status->station ? $status->station->{ds100} : undef ),
station_eva =>
@@ -62,7 +75,8 @@ sub get_departures {
elsif ( @station_matches > 1 ) {
return {
results => [],
- errstr => 'Mehrdeutiger Stationsname. Mögliche Eingaben: '
+ errstr =>
+ "Mehrdeutiger Stationsname: '$station'. Mögliche Eingaben: "
. join( q{, }, map { $_->[1] } @station_matches ),
};
}
@@ -74,6 +88,111 @@ sub get_departures {
}
}
+sub get_departures_p {
+ my ( $self, %opt ) = @_;
+ my $station = $opt{station};
+ my $lookbehind = $opt{lookbehind} // 180;
+ my $lookahead = $opt{lookahead} // 30;
+ my $with_related = $opt{with_related} // 0;
+
+ # Berlin Hbf exists twice:
+ # - BLS / 8011160
+ # - BL / 8098160 (formerly "Berlin Hbf (tief)")
+ # Right now, travelynx assumes that station name -> EVA / DS100 is a unique
+ # map. This is not the case. Work around it here until travelynx has been
+ # adjusted properly.
+ if ( $station eq 'Berlin Hbf' or $station eq '8011160' ) {
+ $with_related = 1;
+ }
+
+ my @station_matches
+ = Travel::Status::DE::IRIS::Stations::get_station($station);
+
+ if ( @station_matches == 1 ) {
+ $station = $station_matches[0][0];
+ my $promise = Mojo::Promise->new;
+ Travel::Status::DE::IRIS->new_p(
+ station => $station,
+ main_cache => $self->{main_cache},
+ realtime_cache => $self->{realtime_cache},
+ keep_transfers => 1,
+ lookbehind => 20,
+ datetime => DateTime->now( time_zone => 'Europe/Berlin' )
+ ->subtract( minutes => $lookbehind ),
+ lookahead => $lookbehind + $lookahead,
+ lwp_options => {
+ timeout => 10,
+ agent => 'travelynx/'
+ . $self->{version}
+ . ' +https://travelynx.de',
+ },
+ with_related => $with_related,
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ get_station => \&Travel::Status::DE::IRIS::Stations::get_station,
+ meta => Travel::Status::DE::IRIS::Stations::get_meta(),
+ )->then(
+ sub {
+ my ($status) = @_;
+ $promise->resolve(
+ {
+ results => [ $status->results ],
+ errstr => $status->errstr,
+ station_ds100 => (
+ $status->station
+ ? $status->station->{ds100}
+ : undef
+ ),
+ station_eva => (
+ $status->station ? $status->station->{uic} : undef
+ ),
+ station_name => (
+ $status->station ? $status->station->{name} : undef
+ ),
+ related_stations => [ $status->related_stations ],
+ }
+ );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject(
+ $err,
+ {
+ results => [],
+ errstr => "Error in promise: $err",
+ }
+ );
+ return;
+ }
+ )->wait;
+ return $promise;
+ }
+ elsif ( @station_matches > 1 ) {
+ return Mojo::Promise->reject(
+ 'ambiguous station name',
+ {
+ results => [],
+ errstr => "Mehrdeutiger Stationsname: '$station'",
+ suggestions => [
+ map { { name => $_->[1], eva => $_->[2] } }
+ @station_matches
+ ],
+ }
+ );
+ }
+ else {
+ return Mojo::Promise->reject(
+ 'unknown station',
+ {
+ results => [],
+ errstr => 'Unbekannte Station',
+ }
+ );
+ }
+}
+
sub route_diff {
my ( $self, $train ) = @_;
my @json_route;
@@ -85,27 +204,31 @@ sub route_diff {
while ( $route_idx <= $#route and $sched_idx <= $#sched_route ) {
if ( $route[$route_idx] eq $sched_route[$sched_idx] ) {
- push( @json_route, [ $route[$route_idx], {}, undef ] );
+ push( @json_route, [ $route[$route_idx], undef, {} ] );
$route_idx++;
$sched_idx++;
}
# this branch is inefficient, but won't be taken frequently
elsif ( not( grep { $_ eq $route[$route_idx] } @sched_route ) ) {
- push( @json_route, [ $route[$route_idx], {}, 'additional' ], );
+ push( @json_route,
+ [ $route[$route_idx], undef, { isAdditional => 1 } ], );
$route_idx++;
}
else {
- push( @json_route, [ $sched_route[$sched_idx], {}, 'cancelled' ], );
+ push( @json_route,
+ [ $sched_route[$sched_idx], undef, { isCancelled => 1 } ], );
$sched_idx++;
}
}
while ( $route_idx <= $#route ) {
- push( @json_route, [ $route[$route_idx], {}, 'additional' ], );
+ push( @json_route,
+ [ $route[$route_idx], undef, { isAdditional => 1 } ], );
$route_idx++;
}
while ( $sched_idx <= $#sched_route ) {
- push( @json_route, [ $sched_route[$sched_idx], {}, 'cancelled' ], );
+ push( @json_route,
+ [ $sched_route[$sched_idx], undef, { isCancelled => 1 } ], );
$sched_idx++;
}
return @json_route;
diff --git a/lib/Travelynx/Helper/Sendmail.pm b/lib/Travelynx/Helper/Sendmail.pm
index 8a7b1f1..baa1156 100644
--- a/lib/Travelynx/Helper/Sendmail.pm
+++ b/lib/Travelynx/Helper/Sendmail.pm
@@ -1,5 +1,6 @@
package Travelynx::Helper::Sendmail;
-# Copyright (C) 2020 Daniel Friesel
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -41,4 +42,34 @@ sub custom {
return try_to_sendmail($reg_mail);
}
+sub age_deletion_notification {
+ my ( $self, %opt ) = @_;
+ my $name = $opt{name};
+ my $email = $opt{email};
+ my $last_seen = $opt{last_seen};
+ my $login_url = $opt{login_url};
+ my $account_url = $opt{account_url};
+ my $imprint_url = $opt{imprint_url};
+
+ my $body = "Hallo ${name},\n\n";
+ $body
+ .= "Dein travelynx-Account wurde seit dem ${last_seen} nicht verwendet.\n";
+ $body
+ .= "Im Sinne der Datensparsamkeit wird er daher in vier Wochen gelöscht.\n";
+ $body
+ .= "Falls du den Account weiterverwenden möchtest, kannst du dich unter\n";
+ $body .= "<$login_url> anmelden.\n";
+ $body
+ .= "Durch die Anmeldung wird die Löschung automatisch abgebrochen.\n\n";
+ $body
+ .= "Falls du den Account löschen, aber zuvor deine Daten exportieren möchtest,\n";
+ $body .= "kannst du dich unter obiger URL anmelden, unter <$account_url>\n";
+ $body
+ .= "deine Daten exportieren und anschließend den Account löschen lassen.\n\n\n";
+ $body .= "Impressum: ${imprint_url}\n";
+
+ return $self->custom( $email,
+ 'travelynx: Löschung deines Accounts', $body );
+}
+
1;
diff --git a/lib/Travelynx/Helper/Traewelling.pm b/lib/Travelynx/Helper/Traewelling.pm
index 88b91a0..d688004 100644
--- a/lib/Travelynx/Helper/Traewelling.pm
+++ b/lib/Travelynx/Helper/Traewelling.pm
@@ -1,12 +1,14 @@
package Travelynx::Helper::Traewelling;
-# Copyright (C) 2020 Daniel Friesel
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2023 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use strict;
use warnings;
use 5.020;
+use utf8;
use DateTime;
use DateTime::Format::Strptime;
@@ -74,54 +76,61 @@ sub get_status_p {
};
$self->{user_agent}->request_timeout(20)
- ->get_p( "https://traewelling.de/api/v0/user/${username}" => $header )
- ->then(
+ ->get_p(
+ "https://traewelling.de/api/v1/user/${username}/statuses?limit=1" =>
+ $header )->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
- my $err_msg = "HTTP $err->{code} $err->{message}";
- $promise->reject($err_msg);
+ my $err_msg
+ = "v1/user/${username}/statuses: HTTP $err->{code} $err->{message}";
+ $promise->reject( { http => $err->{code}, text => $err_msg } );
return;
}
else {
- if ( my $status = $tx->result->json->{statuses}{data}[0] ) {
+ if ( my $status = $tx->result->json->{data}[0] ) {
my $status_id = $status->{id};
my $message = $status->{body};
my $checkin_at
- = $self->parse_datetime( $status->{created_at} );
+ = $self->parse_datetime( $status->{createdAt} );
my $dep_dt = $self->parse_datetime(
- $status->{train_checkin}{departure} );
+ $status->{train}{origin}{departurePlanned} );
my $arr_dt = $self->parse_datetime(
- $status->{train_checkin}{arrival} );
+ $status->{train}{destination}{arrivalPlanned} );
my $dep_eva
- = $status->{train_checkin}{origin}{ibnr};
+ = $status->{train}{origin}{evaIdentifier};
my $arr_eva
- = $status->{train_checkin}{destination}{ibnr};
+ = $status->{train}{destination}{evaIdentifier};
+
+ my $dep_ds100
+ = $status->{train}{origin}{rilIdentifier};
+ my $arr_ds100
+ = $status->{train}{destination}{rilIdentifier};
my $dep_name
- = $status->{train_checkin}{origin}{name};
+ = $status->{train}{origin}{name};
my $arr_name
- = $status->{train_checkin}{destination}{name};
-
- my $category
- = $status->{train_checkin}{hafas_trip}{category};
- my $trip_id
- = $status->{train_checkin}{hafas_trip}{trip_id};
- my $linename
- = $status->{train_checkin}{hafas_trip}{linename};
+ = $status->{train}{destination}{name};
+
+ my $category = $status->{train}{category};
+ my $linename = $status->{train}{lineName};
+ my $trip_id = $status->{train}{hafasId};
my ( $train_type, $train_line ) = split( qr{ }, $linename );
$promise->resolve(
{
+ http => $tx->res->code,
status_id => $status_id,
message => $message,
checkin => $checkin_at,
dep_dt => $dep_dt,
dep_eva => $dep_eva,
+ dep_ds100 => $dep_ds100,
dep_name => $dep_name,
arr_dt => $arr_dt,
arr_eva => $arr_eva,
+ arr_ds100 => $arr_ds100,
arr_name => $arr_name,
trip_id => $trip_id,
train_type => $train_type,
@@ -133,7 +142,8 @@ sub get_status_p {
return;
}
else {
- $promise->reject("unknown error");
+ $promise->reject(
+ { text => "v1/${username}/statuses: unknown error" } );
return;
}
}
@@ -141,7 +151,7 @@ sub get_status_p {
)->catch(
sub {
my ($err) = @_;
- $promise->reject($err);
+ $promise->reject( { text => "v1/${username}/statuses: $err" } );
return;
}
)->wait;
@@ -160,21 +170,20 @@ sub get_user_p {
};
my $promise = Mojo::Promise->new;
- $ua->get_p( "https://traewelling.de/api/v0/getuser" => $header )->then(
+ $ua->get_p( "https://traewelling.de/api/v1/auth/user" => $header )->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
- my $err_msg
- = "HTTP $err->{code} $err->{message} bei Abfrage der Nutzerdaten";
+ my $err_msg = "v1/auth/user: HTTP $err->{code} $err->{message}";
$promise->reject($err_msg);
return;
}
else {
- my $user_data = $tx->result->json;
+ my $user_data = $tx->result->json->{data};
$self->{model}->set_user(
uid => $uid,
trwl_id => $user_data->{id},
- screen_name => $user_data->{name},
+ screen_name => $user_data->{displayName},
user_name => $user_data->{username},
);
$promise->resolve;
@@ -184,84 +193,7 @@ sub get_user_p {
)->catch(
sub {
my ($err) = @_;
- $promise->reject("$err bei Abfrage der Nutzerdaten");
- return;
- }
- )->wait;
-
- return $promise;
-}
-
-sub login_p {
- my ( $self, %opt ) = @_;
-
- my $uid = $opt{uid};
- my $email = $opt{email};
- my $password = $opt{password};
-
- my $ua = $self->{user_agent}->request_timeout(20);
-
- my $request = {
- email => $email,
- password => $password,
- };
-
- my $promise = Mojo::Promise->new;
- my $token;
-
- $ua->post_p(
- "https://traewelling.de/api/v0/auth/login" => $self->{header},
- json => $request
- )->then(
- sub {
- my ($tx) = @_;
- if ( my $err = $tx->error ) {
- my $err_msg = "HTTP $err->{code} $err->{message} bei Login";
- $promise->reject($err_msg);
- return;
- }
- else {
- my $res = $tx->result->json;
- $token = $res->{token};
- my $expiry_dt = $self->parse_datetime( $res->{expires_at} );
-
- # Fall back to one year expiry
- $expiry_dt //= DateTime->now( time_zone => 'Europe/Berlin' )
- ->add( years => 1 );
- $self->{model}->link(
- uid => $uid,
- email => $email,
- token => $token,
- expires => $expiry_dt
- );
- return $self->get_user_p( $uid, $token );
- }
- }
- )->then(
- sub {
- $promise->resolve;
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- if ($token) {
-
- # We have a token, but couldn't complete the login. For now, we
- # solve this by logging out and invalidating the token.
- $self->logout_p(
- uid => $uid,
- token => $token
- )->finally(
- sub {
- $promise->reject($err);
- return;
- }
- );
- }
- else {
- $promise->reject($err);
- }
+ $promise->reject("v1/auth/user: $err");
return;
}
)->wait;
@@ -289,12 +221,13 @@ sub logout_p {
my $promise = Mojo::Promise->new;
$ua->post_p(
- "https://traewelling.de/api/v0/auth/logout" => $header => json =>
+ "https://traewelling.de/api/v1/auth/logout" => $header => json =>
$request )->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
- my $err_msg = "HTTP $err->{code} $err->{message}";
+ my $err_msg
+ = "v1/auth/logout: HTTP $err->{code} $err->{message}";
$promise->reject($err_msg);
return;
}
@@ -306,7 +239,7 @@ sub logout_p {
)->catch(
sub {
my ($err) = @_;
- $promise->reject($err);
+ $promise->reject("v1/auth/logout: $err");
return;
}
)->wait;
@@ -314,7 +247,34 @@ sub logout_p {
return $promise;
}
-sub checkin {
+sub convert_travelynx_to_traewelling_visibility {
+ my ($travelynx_visibility) = @_;
+
+ my %visibilities = (
+
+ # public => StatusVisibility::PUBLIC
+ 100 => 0,
+
+ # travelynx => StatusVisibility::AUTHENTICATED
+ # (only visible for logged in users)
+ 80 => 4,
+
+ # followers => StatusVisibility::FOLLOWERS
+ 60 => 2,
+
+ # unlisted => StatusVisibility::PRIVATE
+ # (there is no träwelling equivalent to unlisted, their
+ # StatusVisibility::UNLISTED shows the journey on the profile)
+ 30 => 3,
+
+ # private => StatusVisibility::PRIVATE
+ 10 => 3,
+ );
+
+ return $visibilities{$travelynx_visibility};
+}
+
+sub checkin_p {
my ( $self, %opt ) = @_;
my $header = {
@@ -334,47 +294,63 @@ sub checkin {
}
my $request = {
- tripID => $opt{trip_id},
+ tripId => $opt{trip_id},
lineName => $opt{train_type} . ' '
. ( $opt{train_line} // $opt{train_no} ),
+ ibnr => \1,
start => q{} . $opt{dep_eva},
destination => q{} . $opt{arr_eva},
departure => $departure_ts,
arrival => $arrival_ts,
- toot => $opt{data}{toot} ? \1 : \0,
+ toot => $opt{data}{toot} ? \1 : \0,
tweet => $opt{data}{tweet} ? \1 : \0,
+ visibility =>
+ convert_travelynx_to_traewelling_visibility( $opt{visibility} )
};
if ( $opt{user_data}{comment} ) {
$request->{body} = $opt{user_data}{comment};
}
+ my $debug_prefix
+ = "v1/trains/checkin('$request->{lineName}' $request->{tripId} $request->{start} -> $request->{destination})";
+
+ my $promise = Mojo::Promise->new;
+
$self->{user_agent}->request_timeout(20)
- ->post_p( "https://traewelling.de/api/v0/trains/checkin" =>
- $header => json => $request )->then(
+ ->post_p(
+ "https://traewelling.de/api/v1/trains/checkin" => $header => json =>
+ $request )->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
my $err_msg = "HTTP $err->{code} $err->{message}";
- if ( $err->{code} != 409 and $err->{code} != 406 ) {
- $self->{log}->warn("Traewelling checkin error: $err_msg");
- }
- else {
- $self->{log}->debug("Traewelling checkin error: $err_msg");
+ if ( $tx->res->body ) {
+ if ( $err->{code} == 409 ) {
+ my $j = $tx->res->json;
+ $err_msg .= sprintf(
+': Bereits in %s eingecheckt: https://traewelling.de/status/%d',
+ $j->{message}{lineName},
+ $j->{message}{status_id}
+ );
+ }
+ else {
+ $err_msg .= ' ' . $tx->res->body;
+ }
}
+ $self->{log}
+ ->debug("Traewelling $debug_prefix error: $err_msg");
$self->{model}->log(
- uid => $opt{uid},
+ uid => $opt{uid},
message =>
- "Fehler bei $opt{train_type} $opt{train_no}: $err_msg",
+"Konnte $opt{train_type} $opt{train_no} nicht übertragen: $debug_prefix returned $err_msg",
is_error => 1
);
+ $promise->reject( { http => $err->{code} } );
return;
}
$self->{log}->debug( "... success! " . $tx->res->body );
- # As of 2020-10-04, traewelling.de checkins do not yet return
- # "statusId". The patch is present on the develop branch and waiting
- # for a merge into master.
$self->{model}->log(
uid => $opt{uid},
message => "Eingecheckt in $opt{train_type} $opt{train_no}",
@@ -384,21 +360,28 @@ sub checkin {
uid => $opt{uid},
ts => $opt{checkin_ts}
);
+ $promise->resolve( { http => $tx->res->code } );
# TODO store status_id in in_transit object so that it can be shown
# on the user status page
+ return;
}
)->catch(
sub {
my ($err) = @_;
- $self->{log}->debug("... error: $err");
+ $self->{log}->debug("... $debug_prefix error: $err");
$self->{model}->log(
- uid => $opt{uid},
- message => "Fehler bei $opt{train_type} $opt{train_no}: $err",
+ uid => $opt{uid},
+ message =>
+"Konnte $opt{train_type} $opt{train_no} nicht übertragen: $debug_prefix returned $err",
is_error => 1
);
+ $promise->reject( { connection => $err } );
+ return;
}
)->wait;
+
+ return $promise;
}
1;