Add travel (checkin/checkout/undo) API

This commit is contained in:
Daniel Friesel 2019-12-14 21:53:23 +01:00
parent 5fe4174feb
commit 46fc64de48
4 changed files with 246 additions and 29 deletions

View file

@ -11,7 +11,7 @@ use DateTime::Format::Strptime;
use Encode qw(decode encode); use Encode qw(decode encode);
use Geo::Distance; use Geo::Distance;
use JSON; use JSON;
use List::Util qw(first); use List::Util;
use List::MoreUtils qw(after_incl before_incl); use List::MoreUtils qw(after_incl before_incl);
use Travel::Status::DE::DBWagenreihung; use Travel::Status::DE::DBWagenreihung;
use Travel::Status::DE::IRIS; use Travel::Status::DE::IRIS;
@ -158,14 +158,14 @@ sub startup {
return { return {
status => 1, status => 1,
history => 2, history => 2,
action => 3, travel => 3,
import => 4, import => 4,
}; };
} }
); );
$self->attr( $self->attr(
token_types => sub { token_types => sub {
return [qw(status history action import)]; return [qw(status history travel import)];
} }
); );
@ -425,21 +425,23 @@ sub startup {
$self->helper( $self->helper(
'checkin' => sub { 'checkin' => sub {
my ( $self, $station, $train_id ) = @_; my ( $self, $station, $train_id, $uid ) = @_;
$uid //= $self->current_user->{id};
my $status = $self->get_departures( $station, 140, 40, 0 ); my $status = $self->get_departures( $station, 140, 40, 0 );
if ( $status->{errstr} ) { if ( $status->{errstr} ) {
return ( undef, $status->{errstr} ); return ( undef, $status->{errstr} );
} }
else { else {
my ($train) my ($train) = List::Util::first { $_->train_id eq $train_id }
= first { $_->train_id eq $train_id } @{ $status->{results} }; @{ $status->{results} };
if ( not defined $train ) { if ( not defined $train ) {
return ( undef, "Train ${train_id} not found" ); return ( undef, "Train ${train_id} not found" );
} }
else { else {
my $user = $self->get_user_status; my $user = $self->get_user_status($uid);
if ( $user->{checked_in} or $user->{cancelled} ) { if ( $user->{checked_in} or $user->{cancelled} ) {
if ( $user->{train_id} eq $train_id if ( $user->{train_id} eq $train_id
@ -450,7 +452,7 @@ sub startup {
} }
# Otherwise, someone forgot to check out first # Otherwise, someone forgot to check out first
$self->checkout( $station, 1 ); $self->checkout( $station, 1, $uid );
} }
eval { eval {
@ -458,7 +460,7 @@ sub startup {
$self->pg->db->insert( $self->pg->db->insert(
'in_transit', 'in_transit',
{ {
user_id => $self->current_user->{id}, user_id => $uid,
cancelled => $train->departure_is_cancelled cancelled => $train->departure_is_cancelled
? 1 ? 1
: 0, : 0,
@ -488,14 +490,12 @@ sub startup {
); );
}; };
if ($@) { if ($@) {
my $uid = $self->current_user->{id};
$self->app->log->error( $self->app->log->error(
"Checkin($uid): INSERT failed: $@"); "Checkin($uid): INSERT failed: $@");
return ( undef, 'INSERT failed: ' . $@ ); return ( undef, 'INSERT failed: ' . $@ );
} }
$self->add_route_timestamps( $self->current_user->{id}, $self->add_route_timestamps( $uid, $train, 1 );
$train, 1 ); $self->run_hook( $uid, 'checkin' );
$self->run_hook( $self->current_user->{id}, 'checkin' );
return ( $train, undef ); return ( $train, undef );
} }
} }
@ -504,8 +504,8 @@ sub startup {
$self->helper( $self->helper(
'undo' => sub { 'undo' => sub {
my ( $self, $journey_id ) = @_; my ( $self, $journey_id, $uid ) = @_;
my $uid = $self->current_user->{id}; $uid //= $self->current_user->{id};
if ( $journey_id eq 'in_transit' ) { if ( $journey_id eq 'in_transit' ) {
eval { eval {
@ -627,8 +627,8 @@ sub startup {
my $journey my $journey
= $db->select( 'in_transit', '*', { user_id => $uid } ) = $db->select( 'in_transit', '*', { user_id => $uid } )
->expand->hash; ->expand->hash;
my ($train) my ($train) = List::Util::first { $_->train_id eq $train_id }
= first { $_->train_id eq $train_id } @{ $status->{results} }; @{ $status->{results} };
# When a checkout is triggered by a checkin, there is an edge case # When a checkout is triggered by a checkin, there is an edge case
# with related stations. # with related stations.
@ -641,8 +641,8 @@ sub startup {
# well. # well.
if ( not $train ) { if ( not $train ) {
$status = $self->get_departures( $station, 120, 180, 1 ); $status = $self->get_departures( $station, 120, 180, 1 );
($train) ($train) = List::Util::first { $_->train_id eq $train_id }
= first { $_->train_id eq $train_id } @{ $status->{results} }; @{ $status->{results} };
} }
# Store the intended checkout station regardless of this operation's # Store the intended checkout station regardless of this operation's
@ -681,8 +681,11 @@ sub startup {
# Arrival time via IRIS is unknown, so the train probably has not # Arrival time via IRIS is unknown, so the train probably has not
# arrived yet. Fall back to HAFAS. # arrived yet. Fall back to HAFAS.
if ( my $station_data if (
= first { $_->[0] eq $station } @{ $journey->{route} } ) my $station_data
= List::Util::first { $_->[0] eq $station }
@{ $journey->{route} }
)
{ {
$station_data = $station_data->[1]; $station_data = $station_data->[1];
if ( $station_data->{sched_arr} ) { if ( $station_data->{sched_arr} ) {
@ -784,7 +787,7 @@ sub startup {
return ( 0, undef ); return ( 0, undef );
} }
$self->run_hook( $uid, 'update' ); $self->run_hook( $uid, 'update' );
$self->add_route_timestamps( $self->current_user->{id}, $train, 0 ); $self->add_route_timestamps( $uid, $train, 0 );
return ( 1, undef ); return ( 1, undef );
} }
); );
@ -3234,6 +3237,7 @@ sub startup {
$r->get('/ajax/status/:name')->to('traveling#public_status_card'); $r->get('/ajax/status/:name')->to('traveling#public_status_card');
$r->get('/ajax/status/:name/:ts')->to('traveling#public_status_card'); $r->get('/ajax/status/:name/:ts')->to('traveling#public_status_card');
$r->post('/api/v1/import')->to('api#import_v1'); $r->post('/api/v1/import')->to('api#import_v1');
$r->post('/api/v1/travel')->to('api#travel_v1');
$r->post('/action')->to('traveling#log_action'); $r->post('/action')->to('traveling#log_action');
$r->post('/geolocation')->to('traveling#geolocation'); $r->post('/geolocation')->to('traveling#geolocation');
$r->post('/list_departures')->to('traveling#redirect_to_station'); $r->post('/list_departures')->to('traveling#redirect_to_station');

View file

@ -2,6 +2,7 @@ package Travelynx::Controller::Api;
use Mojo::Base 'Mojolicious::Controller'; use Mojo::Base 'Mojolicious::Controller';
use DateTime; use DateTime;
use List::Util;
use Travel::Status::DE::IRIS::Stations; use Travel::Status::DE::IRIS::Stations;
use UUID::Tiny qw(:std); use UUID::Tiny qw(:std);
@ -165,6 +166,166 @@ sub get_v1 {
} }
} }
sub travel_v1 {
my ($self) = @_;
my $payload = $self->req->json;
my $api_token = $payload->{token} // '';
if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) {
$self->render(
json => {
success => \0,
error => 'Malformed JSON or malformed token',
},
);
return;
}
my $uid = $+{id};
$api_token = $+{token};
if ( $uid > 2147483647 ) {
$self->render(
json => {
success => \0,
error => 'Malformed token',
},
);
return;
}
my $token = $self->get_api_token($uid);
if ( $api_token ne $token->{'travel'} ) {
$self->render(
json => {
success => \0,
error => 'Invalid token',
},
);
return;
}
if ( not exists $payload->{action}
or $payload->{action} !~ m{^(checkin|checkout|undo)$} )
{
$self->render(
json => {
success => \0,
error => 'Missing or invalid action',
},
);
return;
}
if ( $payload->{action} eq 'checkin' ) {
my $from_station = sanitize( q{}, $payload->{fromStation} );
my $to_station = sanitize( q{}, $payload->{toStation} );
my $train_id;
if ( exists $payload->{train}{id} ) {
$train_id = sanitize( 0, $payload->{train}{id} );
}
else {
my $train_type = sanitize( q{}, $payload->{train}{type} );
my $train_no = sanitize( q{}, $payload->{train}{no} );
my $status = $self->get_departures( $from_station, 140, 40, 0 );
if ( $status->{errstr} ) {
$self->render(
json => {
success => \0,
error => 'Fehler am Abfahrtsbahnhof: '
. $status->{errstr},
status => $self->get_user_status_json_v1($uid)
}
);
return;
}
my ($train) = List::Util::first {
$_->type eq $train_type and $_->train_no eq $train_no
}
@{ $status->{results} };
if ( not defined $train ) {
$self->render(
json => {
success => \0,
error => 'Fehler am Abfahrtsbahnhof: '
. $status->{errstr},
status => $self->get_user_status_json_v1($uid)
}
);
return;
}
$train_id = $train->train_id;
}
my ( $train, $error )
= $self->checkin( $from_station, $train_id, $uid );
if ( $to_station and not $error ) {
( $train, $error ) = $self->checkout( $to_station, 0, $uid );
}
if ($error) {
$self->render(
json => {
success => \0,
error => $error,
status => $self->get_user_status_json_v1($uid)
}
);
}
else {
$self->render(
json => {
success => \1,
status => $self->get_user_status_json_v1($uid)
}
);
}
}
elsif ( $payload->{action} eq 'checkout' ) {
my $to_station = sanitize( q{}, $payload->{toStation} );
my ( $train, $error )
= $self->checkout( $to_station, $payload->{force} ? 1 : 0, $uid );
if ($error) {
$self->render(
json => {
success => \0,
error => $error,
status => $self->get_user_status_json_v1($uid)
}
);
}
else {
$self->render(
json => {
success => \1,
status => $self->get_user_status_json_v1($uid)
}
);
}
}
elsif ( $payload->{action} eq 'undo' ) {
my $error = $self->undo( 'in_transit', $uid );
if ($error) {
$self->render(
json => {
success => \0,
error => $error,
status => $self->get_user_status_json_v1($uid)
}
);
}
else {
$self->render(
json => {
success => \1,
status => $self->get_user_status_json_v1($uid)
}
);
}
}
}
sub import_v1 { sub import_v1 {
my ($self) = @_; my ($self) = @_;

View file

@ -183,7 +183,7 @@
<td> <td>
%= form_for 'set_token' => begin %= form_for 'set_token' => begin
%= csrf_field %= csrf_field
%= hidden_field 'token' => 'action' %= hidden_field 'token' => 'travel'
<button class="btn waves-effect waves-light" type="submit" name="action" value="generate"> <button class="btn waves-effect waves-light" type="submit" name="action" value="generate">
Generieren Generieren
</button> </button>

View file

@ -64,17 +64,69 @@
</p> </p>
</div> </div>
</div> </div>
<!--
<h3>History</h3> <h2>Travel</h2>
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
<p> <p>
Coming soon. Checkin per API. Sobald eine Zielstation bekannt ist, erfolgt der
Checkout wie beim Webinterface automatisch zehn Minuten nach Ankunft.
</p>
<p style="font-family: Monospace;">
curl -X POST -H "Content-Type: application/json" -d '{"token":"<%= $uid %>-<%= $token->{travel} // 'TOKEN' %>"}' <%= $api_root %>/travel
</p>
<p>Payload zum Einchecken, optional mit Zielwahl:</p>
<p style="font-family: Monospace;">
{<br/>
"token" : "<%= $uid %>-<%= $token->{import} // 'TOKEN' %>",<br/>
"action" : "checkin",<br/>
"train" : {<br/>
"type" : "ICE",<br/>
"no" : "1234",<br/>
}<br/>
"fromStation" : "Essen Hbf", (DS100 oder EVA-Nummer sind ebenfalls möglich)<br/>
"toStation" : "Berlin Hbf" (optional, DS100 oder EVA-Nummer sind ebenfalls möglich)<br/>
}
</p>
<p>Payload zur Wahl eines neuen Ziels, wenn bereits eingecheckt:</p>
<p style="font-family: Monospace;">
{<br/>
"token" : "<%= $uid %>-<%= $token->{import} // 'TOKEN' %>",<br/>
"action" : "checkout",<br/>
"force" : True/False, (wenn True: Checkout jetzt durchführen und auftretende Fehler ignorieren. Kann zu Logeinträgen ohne Ankunftsdaten führen.)<br/>
"toStation" : "Berlin Hbf" (DS100 oder EVA-Nummer sind ebenfalls möglich)<br/>
}
</p>
<p>Payload zum Rückgängigmachen eines Checkins (nur während der Fahrt möglich):</p>
<p style="font-family: Monospace;">
{<br/>
"token" : "<%= $uid %>-<%= $token->{import} // 'TOKEN' %>",<br/>
"action" : "undo"<br/>
}
</p>
<p>
Antwort bei Erfolg:
</p>
<p style="font-family: Monospace;">
{<br/>
"success" : True,<br/>
"status" : { aktueller Nutzerstatus gemäß Status-API }<br/>
}
</p>
<p>
Antwort bei Fehler:
</p>
<p style="font-family: Monospace;">
{<br/>
"success" : False,<br/>
"error" : "Begründung",<br/>
"status" : { aktueller Nutzerstatus gemäß Status-API }<br/>
}
</p> </p>
</div> </div>
</div>--> </div>
<h3>Import</h3> <h2>Import</h2>
<div class="row"> <div class="row">
<div class="col s12"> <div class="col s12">
<p> <p>
@ -86,7 +138,7 @@
<p>Payload (alle nicht als optional markierten Felder sind Pflicht):</p> <p>Payload (alle nicht als optional markierten Felder sind Pflicht):</p>
<p style="font-family: Monospace;"> <p style="font-family: Monospace;">
{<br/> {<br/>
"token" : "<%= $token->{import} // 'TOKEN' %>",<br/> "token" : "<%= $uid %>-<%= $token->{import} // 'TOKEN' %>",<br/>
"dryRun" : True/False, (optional: wenn True, wird die Eingabe validiert, aber keine Zugfahrt angelegt)<br/> "dryRun" : True/False, (optional: wenn True, wird die Eingabe validiert, aber keine Zugfahrt angelegt)<br/>
"cancelled" : True/False, (Zugausfall?)<br/> "cancelled" : True/False, (Zugausfall?)<br/>
"train" : {<br/> "train" : {<br/>