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

View file

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

View file

@ -64,17 +64,69 @@
</p>
</div>
</div>
<!--
<h3>History</h3>
<h2>Travel</h2>
<div class="row">
<div class="col s12">
<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>
</div>
</div>-->
</div>
<h3>Import</h3>
<h2>Import</h2>
<div class="row">
<div class="col s12">
<p>
@ -86,7 +138,7 @@
<p>Payload (alle nicht als optional markierten Felder sind Pflicht):</p>
<p style="font-family: Monospace;">
{<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/>
"cancelled" : True/False, (Zugausfall?)<br/>
"train" : {<br/>