summaryrefslogtreecommitdiff
path: root/lib/Travelynx/Helper/Traewelling.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx/Helper/Traewelling.pm')
-rw-r--r--lib/Travelynx/Helper/Traewelling.pm387
1 files changed, 387 insertions, 0 deletions
diff --git a/lib/Travelynx/Helper/Traewelling.pm b/lib/Travelynx/Helper/Traewelling.pm
new file mode 100644
index 0000000..d688004
--- /dev/null
+++ b/lib/Travelynx/Helper/Traewelling.pm
@@ -0,0 +1,387 @@
+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 $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,
+ 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;