summaryrefslogtreecommitdiff
path: root/lib/Travelynx/Controller/Api.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx/Controller/Api.pm')
-rwxr-xr-xlib/Travelynx/Controller/Api.pm591
1 files changed, 532 insertions, 59 deletions
diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm
index a442784..687243d 100755
--- a/lib/Travelynx/Controller/Api.pm
+++ b/lib/Travelynx/Controller/Api.pm
@@ -1,20 +1,54 @@
package Travelynx::Controller::Api;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
-use Travel::Status::DE::IRIS::Stations;
+use DateTime;
+use List::Util;
+use Mojo::JSON qw(encode_json);
use UUID::Tiny qw(:std);
+# Internal Helpers
+
sub make_token {
return create_uuid_as_string(UUID_V4);
}
+sub sanitize {
+ my ( $type, $value ) = @_;
+ if ( not defined $value ) {
+ return undef;
+ }
+ if ( $type eq '' ) {
+ return '' . $value;
+ }
+ if ( $value =~ m{ ^ [0-9.e]+ $ }x ) {
+ return 0 + $value;
+ }
+ return 0;
+}
+
+# Contollers
+
sub documentation {
my ($self) = @_;
- $self->render('api_documentation');
+ if ( $self->is_user_authenticated ) {
+ my $uid = $self->current_user->{id};
+ $self->render(
+ 'api_documentation',
+ uid => $uid,
+ api_token => $self->users->get_api_token( uid => $uid ),
+ );
+ }
+ else {
+ $self->render('api_documentation');
+ }
}
-sub get_v0 {
+sub get_v1 {
my ($self) = @_;
my $api_action = $self->stash('user_action');
@@ -37,8 +71,21 @@ sub get_v0 {
}
my $uid = $+{id};
$api_token = $+{token};
- my $token = $self->get_api_token($uid);
- if ( $api_token ne $token->{$api_action} ) {
+
+ if ( $uid > 2147483647 ) {
+ $self->render(
+ json => {
+ error => 'Malformed token',
+ },
+ );
+ return;
+ }
+
+ my $token = $self->users->get_api_token( uid => $uid );
+ if ( not $api_token
+ or not $token->{$api_action}
+ or $api_token ne $token->{$api_action} )
+ {
$self->render(
json => {
error => 'Invalid token',
@@ -47,75 +94,347 @@ sub get_v0 {
return;
}
if ( $api_action eq 'status' ) {
- my $status = $self->get_user_status($uid);
+ $self->render( json => $self->get_user_status_json_v1( uid => $uid ) );
+ }
+ else {
+ $self->render(
+ json => {
+ error => 'not implemented',
+ },
+ );
+ }
+}
- my @station_descriptions;
- my $station_eva = undef;
- my $station_lon = undef;
- my $station_lat = undef;
+sub travel_v1 {
+ my ($self) = @_;
- if ( $status->{arr_ds100} // $status->{dep_ds100} ) {
- @station_descriptions
- = Travel::Status::DE::IRIS::Stations::get_station(
- $status->{arr_ds100} // $status->{dep_ds100} );
- }
- if ( @station_descriptions == 1 ) {
- ( undef, undef, $station_eva, $station_lon, $station_lat )
- = @{ $station_descriptions[0] };
- }
+ my $payload = $self->req->json;
+
+ if ( not $payload or ref($payload) ne 'HASH' ) {
$self->render(
json => {
- deprecated => \1,
- checked_in => (
- $status->{checked_in}
- or $status->{cancelled}
- ) ? \1 : \0,
- station => {
- ds100 => $status->{arr_ds100} // $status->{dep_ds100},
- name => $status->{arr_name} // $status->{dep_name},
- uic => $station_eva,
- longitude => $station_lon,
- latitude => $station_lat,
- },
- train => {
- type => $status->{train_type},
- line => $status->{train_line},
- no => $status->{train_no},
- },
- actionTime => $status->{timestamp}->epoch,
- scheduledTime => $status->{sched_arrival}->epoch
- || $status->{sched_departure}->epoch,
- realTime => $status->{real_arrival}->epoch
- || $status->{real_departure}->epoch,
+ success => \0,
+ deprecated => \0,
+ error => 'Malformed JSON',
},
);
+ return;
}
- else {
+
+ my $api_token = $payload->{token} // '';
+
+ if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) {
$self->render(
json => {
- error => 'not implemented',
+ success => \0,
+ deprecated => \0,
+ error => 'Malformed token',
+ },
+ );
+ return;
+ }
+ my $uid = $+{id};
+ $api_token = $+{token};
+
+ if ( $uid > 2147483647 ) {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Malformed token',
},
);
+ return;
+ }
+
+ my $token = $self->users->get_api_token( uid => $uid );
+ if ( not $token->{'travel'} or $api_token ne $token->{'travel'} ) {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Invalid token',
+ },
+ );
+ return;
+ }
+
+ if ( not exists $payload->{action}
+ or $payload->{action} !~ m{^(checkin|checkout|undo)$} )
+ {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Missing or invalid action',
+ status => $self->get_user_status_json_v1( uid => $uid )
+ },
+ );
+ return;
+ }
+
+ if ( $payload->{action} eq 'checkin' ) {
+ 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;
+
+ if (
+ not(
+ $from_station
+ and ( ( $payload->{train}{type} and $payload->{train}{no} )
+ or $payload->{train}{id}
+ or $payload->{train}{journeyID} )
+ )
+ )
+ {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Missing fromStation or train data',
+ status => $self->get_user_status_json_v1( uid => $uid )
+ },
+ );
+ return;
+ }
+
+ if ( not $hafas and not $self->stations->search($from_station) ) {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Unknown fromStation',
+ status => $self->get_user_status_json_v1( uid => $uid )
+ },
+ );
+ return;
+ }
+
+ if ( $to_station
+ and not $hafas
+ and not $self->stations->search($to_station) )
+ {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Unknown toStation',
+ status => $self->get_user_status_json_v1( uid => $uid )
+ },
+ );
+ return;
+ }
+
+ my $train_p;
+
+ if ( exists $payload->{train}{journeyID} ) {
+ $train_p = Mojo::Promise->resolve(
+ sanitize( q{}, $payload->{train}{journeyID} ) );
+ }
+ elsif ( exists $payload->{train}{id} ) {
+ $train_p
+ = Mojo::Promise->resolve( sanitize( 0, $payload->{train}{id} ) );
+ }
+ else {
+ my $train_type = sanitize( q{}, $payload->{train}{type} );
+ my $train_no = sanitize( q{}, $payload->{train}{no} );
+
+ $train_p = $self->iris->get_departures_p(
+ station => $from_station,
+ lookbehind => 140,
+ lookahead => 40
+ )->then(
+ sub {
+ my ($status) = @_;
+ if ( $status->{errstr} ) {
+ return Mojo::Promise->reject(
+ 'Error requesting departures from fromStation: '
+ . $status->{errstr} );
+ }
+ my ($train) = List::Util::first {
+ $_->type eq $train_type and $_->train_no eq $train_no
+ }
+ @{ $status->{results} };
+ if ( not defined $train ) {
+ return Mojo::Promise->reject(
+ 'Train not found at fromStation');
+ }
+ return Mojo::Promise->resolve( $train->train_id );
+ }
+ );
+ }
+
+ $self->render_later;
+
+ $train_p->then(
+ sub {
+ my ($train_id) = @_;
+ return $self->checkin_p(
+ station => $from_station,
+ train_id => $train_id,
+ uid => $uid
+ );
+ }
+ )->then(
+ sub {
+ my ($train) = @_;
+ if ( $payload->{comment} ) {
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ user_data =>
+ { comment => sanitize( q{}, $payload->{comment} ) }
+ );
+ }
+ if ($to_station) {
+
+ # the user may not have provided the correct to_station, so
+ # request related stations for checkout.
+ return $self->checkout_p(
+ station => $to_station,
+ force => 0,
+ uid => $uid,
+ with_related => 1,
+ );
+ }
+ return Mojo::Promise->resolve;
+ }
+ )->then(
+ sub {
+ my ( undef, $error ) = @_;
+ if ($error) {
+ return Mojo::Promise->reject($error);
+ }
+ $self->render(
+ json => {
+ success => \1,
+ deprecated => \0,
+ status => $self->get_user_status_json_v1( uid => $uid )
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($error) = @_;
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Checkin/Checkout error: ' . $error,
+ status => $self->get_user_status_json_v1( uid => $uid )
+ }
+ );
+ }
+ )->wait;
+ }
+ elsif ( $payload->{action} eq 'checkout' ) {
+ my $to_station = sanitize( q{}, $payload->{toStation} );
+
+ if ( not $to_station ) {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Missing toStation',
+ status => $self->get_user_status_json_v1( uid => $uid )
+ },
+ );
+ return;
+ }
+
+ if ( $payload->{comment} ) {
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ user_data => { comment => sanitize( q{}, $payload->{comment} ) }
+ );
+ }
+
+ $self->render_later;
+
+ # the user may not have provided the correct to_station, so
+ # request related stations for checkout.
+ $self->checkout_p(
+ station => $to_station,
+ force => $payload->{force} ? 1 : 0,
+ uid => $uid,
+ with_related => 1,
+ )->then(
+ sub {
+ my ( $train, $error ) = @_;
+ if ($error) {
+ return Mojo::Promise->reject($error);
+ }
+ $self->render(
+ json => {
+ success => \1,
+ deprecated => \0,
+ status => $self->get_user_status_json_v1( uid => $uid )
+ }
+ );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'Checkout error: ' . $err,
+ status => $self->get_user_status_json_v1( uid => $uid )
+ }
+ );
+ }
+ )->wait;
+ }
+ elsif ( $payload->{action} eq 'undo' ) {
+ my $error = $self->undo( 'in_transit', $uid );
+ if ($error) {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => $error,
+ status => $self->get_user_status_json_v1( uid => $uid )
+ }
+ );
+ }
+ else {
+ $self->render(
+ json => {
+ success => \1,
+ deprecated => \0,
+ status => $self->get_user_status_json_v1( uid => $uid )
+ }
+ );
+ }
}
}
-sub get_v1 {
+sub import_v1 {
my ($self) = @_;
- my $api_action = $self->stash('user_action');
- my $api_token = $self->stash('token');
- if ( $api_action !~ qr{ ^ (?: status | history | action ) $ }x ) {
+ my $payload = $self->req->json;
+
+ if ( not $payload or ref($payload) ne 'HASH' ) {
$self->render(
json => {
- error => 'Invalid action',
+ success => \0,
+ deprecated => \0,
+ error => 'Malformed JSON',
},
);
return;
}
+
+ my $api_token = $payload->{token} // '';
+
if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) {
$self->render(
json => {
- error => 'Malformed token',
+ success => \0,
+ deprecated => \0,
+ error => 'Malformed token',
},
);
return;
@@ -126,29 +445,162 @@ sub get_v1 {
if ( $uid > 2147483647 ) {
$self->render(
json => {
- error => 'Malformed token',
+ success => \0,
+ deprecated => \0,
+ error => 'Malformed token',
},
);
return;
}
- my $token = $self->get_api_token($uid);
- if ( $api_token ne $token->{$api_action} ) {
+ my $token = $self->users->get_api_token( uid => $uid );
+ if ( not $token->{'import'} or $api_token ne $token->{'import'} ) {
$self->render(
json => {
- error => 'Invalid token',
+ success => \0,
+ deprecated => \0,
+ error => 'Invalid token',
},
);
return;
}
- if ( $api_action eq 'status' ) {
- $self->render( json => $self->get_user_status_json_v1($uid) );
+
+ if ( not exists $payload->{fromStation}
+ or not exists $payload->{toStation} )
+ {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => 'missing fromStation or toStation',
+ },
+ );
+ return;
+ }
+
+ my %opt;
+
+ eval {
+
+ if ( not $payload->{fromStation}{name}
+ or not $payload->{toStation}{name} )
+ {
+ die("Missing fromStation/toStation name\n");
+ }
+ if ( not $payload->{train}{type} or not $payload->{train}{no} ) {
+ die("Missing train data\n");
+ }
+ if ( not $payload->{fromStation}{scheduledTime}
+ or not $payload->{toStation}{scheduledTime} )
+ {
+ die("Missing fromStation/toStation scheduledTime\n");
+ }
+
+ %opt = (
+ uid => $uid,
+ train_type => sanitize( q{}, $payload->{train}{type} ),
+ train_no => sanitize( q{}, $payload->{train}{no} ),
+ train_line => sanitize( q{}, $payload->{train}{line} ),
+ cancelled => $payload->{cancelled} ? 1 : 0,
+ dep_station => sanitize( q{}, $payload->{fromStation}{name} ),
+ arr_station => sanitize( q{}, $payload->{toStation}{name} ),
+ sched_departure =>
+ sanitize( 0, $payload->{fromStation}{scheduledTime} ),
+ rt_departure => sanitize(
+ 0,
+ $payload->{fromStation}{realTime}
+ // $payload->{fromStation}{scheduledTime}
+ ),
+ sched_arrival =>
+ sanitize( 0, $payload->{toStation}{scheduledTime} ),
+ rt_arrival => sanitize(
+ 0,
+ $payload->{toStation}{realTime}
+ // $payload->{toStation}{scheduledTime}
+ ),
+ comment => sanitize( q{}, $payload->{comment} ),
+ lax => $payload->{lax} ? 1 : 0,
+ );
+
+ if ( $payload->{intermediateStops}
+ and ref( $payload->{intermediateStops} ) eq 'ARRAY' )
+ {
+ $opt{route}
+ = [ map { sanitize( q{}, $_ ) }
+ @{ $payload->{intermediateStops} } ];
+ }
+
+ for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival))
+ {
+ $opt{$key} = DateTime->from_epoch(
+ time_zone => 'Europe/Berlin',
+ epoch => $opt{$key}
+ );
+ }
+ };
+ if ($@) {
+ my ($first_line) = split( qr{\n}, $@ );
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => $first_line
+ }
+ );
+ return;
+ }
+
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+
+ $opt{db} = $db;
+ my ( $journey_id, $error ) = $self->journeys->add(%opt);
+ 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 );
+ }
+
+ if ($error) {
+ $self->render(
+ json => {
+ success => \0,
+ deprecated => \0,
+ error => $error
+ }
+ );
+ }
+ elsif ( $payload->{dryRun} ) {
+ $self->render(
+ json => {
+ success => \1,
+ deprecated => \0,
+ id => $journey_id,
+ result => $journey
+ }
+ );
}
else {
+ $self->journey_stats_cache->invalidate(
+ ts => $opt{rt_departure},
+ db => $db,
+ uid => $uid
+ );
+ $tx->commit;
$self->render(
json => {
- error => 'not implemented',
- },
+ success => \1,
+ deprecated => \0,
+ id => $journey_id,
+ result => $journey
+ }
);
}
}
@@ -156,11 +608,15 @@ sub get_v1 {
sub set_token {
my ($self) = @_;
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
- $self->render( 'account', invalid => 'csrf' );
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
return;
}
my $token = make_token();
- my $token_id = $self->app->token_type->{ $self->param('token') };
+ my $token_id = $self->users->get_token_id( $self->param('token') );
if ( not $token_id ) {
$self->redirect_to('account');
@@ -193,4 +649,21 @@ sub set_token {
$self->redirect_to('account');
}
+sub autocomplete {
+ my ($self) = @_;
+
+ $self->res->headers->cache_control('max-age=86400, immutable');
+
+ 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 .= "\n});});\n";
+
+ $self->render(
+ format => 'js',
+ data => $output
+ );
+}
+
1;