52c0da3f46
This is a breaking change insofar as that traewelling support is no longer provided automatically, but must be enabled by providing a traewelling.de application ID and secret in travelynx.conf. However, as traewelling.de password login is deprecated and wil soon be disabled, travelynx would break either way. So we might or might not see travelynx 2.0.0 in the next days. Automatic token refresh is still todo, but that was the case for password login as well. Closes #64
385 lines
9 KiB
Perl
385 lines
9 KiB
Perl
package Travelynx::Helper::Traewelling;
|
|
|
|
# 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;
|
|
use Mojo::Promise;
|
|
|
|
sub new {
|
|
my ( $class, %opt ) = @_;
|
|
|
|
my $version = $opt{version};
|
|
|
|
$opt{header} = {
|
|
'User-Agent' =>
|
|
"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx",
|
|
'Accept' => 'application/json',
|
|
};
|
|
$opt{strp1} = DateTime::Format::Strptime->new(
|
|
pattern => '%Y-%m-%dT%H:%M:%S.000000Z',
|
|
time_zone => 'UTC',
|
|
);
|
|
$opt{strp2} = DateTime::Format::Strptime->new(
|
|
pattern => '%Y-%m-%d %H:%M:%S',
|
|
time_zone => 'Europe/Berlin',
|
|
);
|
|
$opt{strp3} = DateTime::Format::Strptime->new(
|
|
pattern => '%Y-%m-%dT%H:%M:%S%z',
|
|
time_zone => 'Europe/Berlin',
|
|
);
|
|
|
|
return bless( \%opt, $class );
|
|
}
|
|
|
|
sub epoch_to_dt_or_undef {
|
|
my ($epoch) = @_;
|
|
|
|
if ( not $epoch ) {
|
|
return undef;
|
|
}
|
|
|
|
return DateTime->from_epoch(
|
|
epoch => $epoch,
|
|
time_zone => 'Europe/Berlin',
|
|
locale => 'de-DE',
|
|
);
|
|
}
|
|
|
|
sub parse_datetime {
|
|
my ( $self, $dt ) = @_;
|
|
|
|
return $self->{strp1}->parse_datetime($dt)
|
|
// $self->{strp2}->parse_datetime($dt)
|
|
// $self->{strp3}->parse_datetime($dt);
|
|
}
|
|
|
|
sub get_status_p {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $username = $opt{username};
|
|
my $token = $opt{token};
|
|
my $promise = Mojo::Promise->new;
|
|
|
|
my $header = {
|
|
'User-Agent' => $self->{header}{'User-Agent'},
|
|
'Accept' => 'application/json',
|
|
'Authorization' => "Bearer $token",
|
|
};
|
|
|
|
$self->{user_agent}->request_timeout(20)
|
|
->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
|
|
= "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->{data}[0] ) {
|
|
my $status_id = $status->{id};
|
|
my $message = $status->{body};
|
|
my $checkin_at
|
|
= $self->parse_datetime( $status->{createdAt} );
|
|
|
|
my $dep_dt = $self->parse_datetime(
|
|
$status->{train}{origin}{departurePlanned} );
|
|
my $arr_dt = $self->parse_datetime(
|
|
$status->{train}{destination}{arrivalPlanned} );
|
|
|
|
my $dep_eva
|
|
= $status->{train}{origin}{evaIdentifier};
|
|
my $arr_eva
|
|
= $status->{train}{destination}{evaIdentifier};
|
|
|
|
my $dep_ds100
|
|
= $status->{train}{origin}{rilIdentifier};
|
|
my $arr_ds100
|
|
= $status->{train}{destination}{rilIdentifier};
|
|
|
|
my $dep_name
|
|
= $status->{train}{origin}{name};
|
|
my $arr_name
|
|
= $status->{train}{destination}{name};
|
|
|
|
my $category = $status->{train}{category};
|
|
my $linename = $status->{train}{lineName};
|
|
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,
|
|
train_type => $train_type,
|
|
line => $linename,
|
|
line_no => $train_line,
|
|
category => $category,
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
else {
|
|
$promise->reject(
|
|
{ text => "v1/${username}/statuses: unknown error" } );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
)->catch(
|
|
sub {
|
|
my ($err) = @_;
|
|
$promise->reject( { text => "v1/${username}/statuses: $err" } );
|
|
return;
|
|
}
|
|
)->wait;
|
|
|
|
return $promise;
|
|
}
|
|
|
|
sub get_user_p {
|
|
my ( $self, $uid, $token ) = @_;
|
|
my $ua = $self->{user_agent}->request_timeout(20);
|
|
|
|
my $header = {
|
|
'User-Agent' => $self->{header}{'User-Agent'},
|
|
'Accept' => 'application/json',
|
|
'Authorization' => "Bearer $token",
|
|
};
|
|
my $promise = Mojo::Promise->new;
|
|
|
|
$ua->get_p( "https://traewelling.de/api/v1/auth/user" => $header )->then(
|
|
sub {
|
|
my ($tx) = @_;
|
|
if ( my $err = $tx->error ) {
|
|
my $err_msg = "v1/auth/user: HTTP $err->{code} $err->{message}";
|
|
$promise->reject($err_msg);
|
|
return;
|
|
}
|
|
else {
|
|
my $user_data = $tx->result->json->{data};
|
|
$self->{model}->set_user(
|
|
uid => $uid,
|
|
trwl_id => $user_data->{id},
|
|
screen_name => $user_data->{displayName},
|
|
user_name => $user_data->{username},
|
|
);
|
|
$promise->resolve;
|
|
return;
|
|
}
|
|
}
|
|
)->catch(
|
|
sub {
|
|
my ($err) = @_;
|
|
$promise->reject("v1/auth/user: $err");
|
|
return;
|
|
}
|
|
)->wait;
|
|
|
|
return $promise;
|
|
}
|
|
|
|
sub logout_p {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $uid = $opt{uid};
|
|
my $token = $opt{token};
|
|
|
|
my $ua = $self->{user_agent}->request_timeout(20);
|
|
|
|
my $header = {
|
|
'User-Agent' => $self->{header}{'User-Agent'},
|
|
'Accept' => 'application/json',
|
|
'Authorization' => "Bearer $token",
|
|
};
|
|
my $request = {};
|
|
|
|
$self->{model}->unlink( uid => $uid );
|
|
|
|
my $promise = Mojo::Promise->new;
|
|
|
|
$ua->post_p(
|
|
"https://traewelling.de/api/v1/auth/logout" => $header => json =>
|
|
$request )->then(
|
|
sub {
|
|
my ($tx) = @_;
|
|
if ( my $err = $tx->error ) {
|
|
my $err_msg
|
|
= "v1/auth/logout: HTTP $err->{code} $err->{message}";
|
|
$promise->reject($err_msg);
|
|
return;
|
|
}
|
|
else {
|
|
$promise->resolve;
|
|
return;
|
|
}
|
|
}
|
|
)->catch(
|
|
sub {
|
|
my ($err) = @_;
|
|
$promise->reject("v1/auth/logout: $err");
|
|
return;
|
|
}
|
|
)->wait;
|
|
|
|
return $promise;
|
|
}
|
|
|
|
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 = {
|
|
'User-Agent' => $self->{header}{'User-Agent'},
|
|
'Accept' => 'application/json',
|
|
'Authorization' => "Bearer $opt{token}",
|
|
};
|
|
|
|
my $departure_ts = epoch_to_dt_or_undef( $opt{dep_ts} );
|
|
my $arrival_ts = epoch_to_dt_or_undef( $opt{arr_ts} );
|
|
|
|
if ($departure_ts) {
|
|
$departure_ts = $departure_ts->rfc3339;
|
|
}
|
|
if ($arrival_ts) {
|
|
$arrival_ts = $arrival_ts->rfc3339;
|
|
}
|
|
|
|
my $request = {
|
|
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,
|
|
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/v1/trains/checkin" => $header => json =>
|
|
$request )->then(
|
|
sub {
|
|
my ($tx) = @_;
|
|
if ( my $err = $tx->error ) {
|
|
my $err_msg = "HTTP $err->{code} $err->{message}";
|
|
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},
|
|
message =>
|
|
"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 );
|
|
|
|
$self->{model}->log(
|
|
uid => $opt{uid},
|
|
message => "Eingecheckt in $opt{train_type} $opt{train_no}",
|
|
status_id => $tx->res->json->{statusId}
|
|
);
|
|
$self->{model}->set_latest_push_ts(
|
|
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("... $debug_prefix error: $err");
|
|
$self->{model}->log(
|
|
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;
|