2020-09-30 17:12:29 +00:00
package Travelynx::Helper::Traewelling ;
2021-02-02 17:20:49 +00:00
2023-07-03 15:59:25 +00:00
# Copyright (C) 2020-2023 Birte Kristina Friesel
2023-07-13 18:17:51 +00:00
# Copyright (C) 2023 networkException <git@nwex.de>
2020-11-27 21:12:56 +00:00
#
2021-01-29 17:32:13 +00:00
# SPDX-License-Identifier: AGPL-3.0-or-later
2020-09-30 17:12:29 +00:00
use strict ;
use warnings ;
use 5.020 ;
2022-07-03 10:13:58 +00:00
use utf8 ;
2020-09-30 17:12:29 +00:00
2020-10-04 08:38:09 +00:00
use DateTime ;
use DateTime::Format::Strptime ;
2020-09-30 17:12:29 +00:00
use Mojo::Promise ;
sub new {
my ( $ class , % opt ) = @ _ ;
my $ version = $ opt { version } ;
2020-10-04 10:59:08 +00:00
$ opt { header } = {
'User-Agent' = >
"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx" ,
'Accept' = > 'application/json' ,
} ;
2020-10-17 09:03:47 +00:00
$ 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' ,
) ;
2020-09-30 17:12:29 +00:00
return bless ( \ % opt , $ class ) ;
}
2021-07-01 17:44:09 +00:00
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' ,
) ;
}
2020-10-17 09:03:47 +00:00
sub parse_datetime {
my ( $ self , $ dt ) = @ _ ;
return $ self - > { strp1 } - > parse_datetime ( $ dt )
// $ self - > { strp2 } - > parse_datetime ( $ dt )
// $ self - > { strp3 } - > parse_datetime ( $ dt ) ;
}
2020-09-30 17:12:29 +00:00
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' } ,
2020-10-04 10:59:08 +00:00
'Accept' = > 'application/json' ,
2020-09-30 17:12:29 +00:00
'Authorization' = > "Bearer $token" ,
} ;
$ self - > { user_agent } - > request_timeout ( 20 )
2022-01-08 09:54:54 +00:00
- > get_p (
2022-07-09 21:10:56 +00:00
"https://traewelling.de/api/v1/user/${username}/statuses?limit=1" = >
$ header ) - > then (
2020-09-30 17:12:29 +00:00
sub {
my ( $ tx ) = @ _ ;
if ( my $ err = $ tx - > error ) {
2022-08-17 19:51:24 +00:00
my $ err_msg
= "v1/user/${username}/statuses: HTTP $err->{code} $err->{message}" ;
2023-01-07 12:31:14 +00:00
$ promise - > reject ( { http = > $ err - > { code } , text = > $ err_msg } ) ;
2020-09-30 17:12:29 +00:00
return ;
}
else {
2022-01-08 09:54:54 +00:00
if ( my $ status = $ tx - > result - > json - > { data } [ 0 ] ) {
2020-09-30 17:12:29 +00:00
my $ status_id = $ status - > { id } ;
my $ message = $ status - > { body } ;
my $ checkin_at
2022-01-08 09:54:54 +00:00
= $ self - > parse_datetime ( $ status - > { createdAt } ) ;
2020-09-30 17:12:29 +00:00
2020-10-17 09:03:47 +00:00
my $ dep_dt = $ self - > parse_datetime (
2022-01-08 09:54:54 +00:00
$ status - > { train } { origin } { departurePlanned } ) ;
2020-10-17 09:03:47 +00:00
my $ arr_dt = $ self - > parse_datetime (
2022-01-08 09:54:54 +00:00
$ status - > { train } { destination } { arrivalPlanned } ) ;
2020-09-30 17:12:29 +00:00
my $ dep_eva
2022-05-28 09:35:44 +00:00
= $ status - > { train } { origin } { evaIdentifier } ;
2020-09-30 17:12:29 +00:00
my $ arr_eva
2022-05-28 09:35:44 +00:00
= $ status - > { train } { destination } { evaIdentifier } ;
2020-09-30 17:12:29 +00:00
2022-08-17 19:51:24 +00:00
my $ dep_ds100
= $ status - > { train } { origin } { rilIdentifier } ;
my $ arr_ds100
= $ status - > { train } { destination } { rilIdentifier } ;
2020-09-30 17:12:29 +00:00
my $ dep_name
2022-01-08 09:54:54 +00:00
= $ status - > { train } { origin } { name } ;
2020-09-30 17:12:29 +00:00
my $ arr_name
2022-01-08 09:54:54 +00:00
= $ status - > { train } { destination } { name } ;
my $ category = $ status - > { train } { category } ;
my $ linename = $ status - > { train } { lineName } ;
2020-09-30 17:12:29 +00:00
my ( $ train_type , $ train_line ) = split ( qr{ } , $ linename ) ;
$ promise - > resolve (
{
2023-01-07 12:31:14 +00:00
http = > $ tx - > res - > code ,
2020-09-30 17:12:29 +00:00
status_id = > $ status_id ,
message = > $ message ,
checkin = > $ checkin_at ,
dep_dt = > $ dep_dt ,
dep_eva = > $ dep_eva ,
2022-08-17 19:51:24 +00:00
dep_ds100 = > $ dep_ds100 ,
2020-09-30 17:12:29 +00:00
dep_name = > $ dep_name ,
arr_dt = > $ arr_dt ,
arr_eva = > $ arr_eva ,
2022-08-17 19:51:24 +00:00
arr_ds100 = > $ arr_ds100 ,
2020-09-30 17:12:29 +00:00
arr_name = > $ arr_name ,
train_type = > $ train_type ,
line = > $ linename ,
line_no = > $ train_line ,
category = > $ category ,
}
) ;
return ;
}
else {
2023-01-07 12:31:14 +00:00
$ promise - > reject (
{ text = > "v1/${username}/statuses: unknown error" } ) ;
2020-09-30 17:12:29 +00:00
return ;
}
}
}
) - > catch (
sub {
my ( $ err ) = @ _ ;
2023-01-07 12:31:14 +00:00
$ promise - > reject ( { text = > "v1/${username}/statuses: $err" } ) ;
2020-09-30 17:12:29 +00:00
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' } ,
2020-10-04 10:59:08 +00:00
'Accept' = > 'application/json' ,
2020-09-30 17:12:29 +00:00
'Authorization' = > "Bearer $token" ,
} ;
my $ promise = Mojo::Promise - > new ;
2022-10-31 19:58:27 +00:00
$ ua - > get_p ( "https://traewelling.de/api/v1/auth/user" = > $ header ) - > then (
2020-09-30 17:12:29 +00:00
sub {
my ( $ tx ) = @ _ ;
if ( my $ err = $ tx - > error ) {
2022-10-31 19:58:27 +00:00
my $ err_msg = "v1/auth/user: HTTP $err->{code} $err->{message}" ;
2020-09-30 17:12:29 +00:00
$ promise - > reject ( $ err_msg ) ;
return ;
}
else {
2022-10-31 19:58:27 +00:00
my $ user_data = $ tx - > result - > json - > { data } ;
2020-09-30 17:12:29 +00:00
$ self - > { model } - > set_user (
uid = > $ uid ,
trwl_id = > $ user_data - > { id } ,
2022-10-31 19:58:27 +00:00
screen_name = > $ user_data - > { displayName } ,
2020-09-30 17:12:29 +00:00
user_name = > $ user_data - > { username } ,
) ;
$ promise - > resolve ;
return ;
}
}
) - > catch (
sub {
my ( $ err ) = @ _ ;
2022-10-31 19:58:27 +00:00
$ promise - > reject ( "v1/auth/user: $err" ) ;
2020-09-30 17:12:29 +00:00
return ;
}
) - > wait ;
return $ promise ;
}
sub login_p {
my ( $ self , % opt ) = @ _ ;
my $ uid = $ opt { uid } ;
my $ email = $ opt { email } ;
my $ password = $ opt { password } ;
my $ ua = $ self - > { user_agent } - > request_timeout ( 20 ) ;
my $ request = {
2022-10-31 19:58:27 +00:00
login = > $ email ,
2020-09-30 17:12:29 +00:00
password = > $ password ,
} ;
my $ promise = Mojo::Promise - > new ;
my $ token ;
$ ua - > post_p (
2022-10-31 19:58:27 +00:00
"https://traewelling.de/api/v1/auth/login" = > $ self - > { header } ,
2020-10-04 10:59:08 +00:00
json = > $ request
) - > then (
2020-09-30 17:12:29 +00:00
sub {
my ( $ tx ) = @ _ ;
if ( my $ err = $ tx - > error ) {
2022-08-17 19:51:24 +00:00
my $ err_msg
2022-10-31 19:58:27 +00:00
= "v1/auth/login: HTTP $err->{code} $err->{message}" ;
2020-09-30 17:12:29 +00:00
$ promise - > reject ( $ err_msg ) ;
return ;
}
else {
2022-10-31 19:58:27 +00:00
my $ res = $ tx - > result - > json - > { data } ;
2020-10-17 09:03:47 +00:00
$ token = $ res - > { token } ;
my $ expiry_dt = $ self - > parse_datetime ( $ res - > { expires_at } ) ;
2020-10-04 08:38:09 +00:00
2020-10-17 09:03:47 +00:00
# Fall back to one year expiry
$ expiry_dt // = DateTime - > now ( time_zone = > 'Europe/Berlin' )
2020-10-04 08:38:09 +00:00
- > add ( years = > 1 ) ;
2020-09-30 17:12:29 +00:00
$ self - > { model } - > link (
2020-10-04 08:38:09 +00:00
uid = > $ uid ,
email = > $ email ,
token = > $ token ,
expires = > $ expiry_dt
2020-09-30 17:12:29 +00:00
) ;
return $ self - > get_user_p ( $ uid , $ token ) ;
}
}
) - > then (
sub {
$ promise - > resolve ;
return ;
}
) - > catch (
sub {
my ( $ err ) = @ _ ;
if ( $ token ) {
# We have a token, but couldn't complete the login. For now, we
# solve this by logging out and invalidating the token.
$ self - > logout_p (
uid = > $ uid ,
token = > $ token
) - > finally (
sub {
2022-10-31 19:58:27 +00:00
$ promise - > reject ( "v1/auth/login: $err" ) ;
2020-09-30 17:12:29 +00:00
return ;
}
) ;
}
else {
2022-10-31 19:58:27 +00:00
$ promise - > reject ( "v1/auth/login: $err" ) ;
2020-09-30 17:12:29 +00:00
}
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' } ,
2020-10-04 10:59:08 +00:00
'Accept' = > 'application/json' ,
2020-09-30 17:12:29 +00:00
'Authorization' = > "Bearer $token" ,
} ;
my $ request = { } ;
$ self - > { model } - > unlink ( uid = > $ uid ) ;
my $ promise = Mojo::Promise - > new ;
$ ua - > post_p (
2022-10-31 19:58:27 +00:00
"https://traewelling.de/api/v1/auth/logout" = > $ header = > json = >
2020-09-30 17:12:29 +00:00
$ request ) - > then (
sub {
my ( $ tx ) = @ _ ;
if ( my $ err = $ tx - > error ) {
2022-08-17 19:51:24 +00:00
my $ err_msg
2022-10-31 19:58:27 +00:00
= "v1/auth/logout: HTTP $err->{code} $err->{message}" ;
2020-09-30 17:12:29 +00:00
$ promise - > reject ( $ err_msg ) ;
return ;
}
else {
$ promise - > resolve ;
return ;
}
}
) - > catch (
sub {
my ( $ err ) = @ _ ;
2022-10-31 19:58:27 +00:00
$ promise - > reject ( "v1/auth/logout: $err" ) ;
2020-09-30 17:12:29 +00:00
return ;
}
) - > wait ;
return $ promise ;
}
2023-07-13 18:17:51 +00:00
sub convert_travelynx_to_traewelling_visibility {
my ( $ travelynx_visibility ) = @ _ ;
my % visibilities = (
100 = > 0 , # public => StatusVisibility::PUBLIC
80 = > 4 , # travelynx => StatusVisibility::AUTHENTICATED (only visible for logged in users)
60 = > 2 , # followers => StatusVisibility::FOLLOWERS
30 = > 3 , # unlisted => StatusVisibility::PRIVATE (there is no träwelling equivalent to unlisted, their StatusVisibility::UNLISTED shows the journey on the profile)
10 = > 3 , # private => StatusVisibility::PRIVATE
) ;
return $ visibilities { $ travelynx_visibility } ;
}
2023-01-07 12:31:14 +00:00
sub checkin_p {
2020-10-01 17:36:35 +00:00
my ( $ self , % opt ) = @ _ ;
my $ header = {
'User-Agent' = > $ self - > { header } { 'User-Agent' } ,
2020-10-04 10:59:08 +00:00
'Accept' = > 'application/json' ,
2020-10-01 17:36:35 +00:00
'Authorization' = > "Bearer $opt{token}" ,
} ;
2021-07-01 17:44:09 +00:00
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 ;
}
2020-10-01 17:36:35 +00:00
my $ request = {
2022-11-10 16:50:31 +00:00
tripId = > $ opt { trip_id } ,
2021-02-02 17:20:49 +00:00
lineName = > $ opt { train_type } . ' '
. ( $ opt { train_line } // $ opt { train_no } ) ,
2023-01-07 11:29:56 +00:00
ibnr = > \ 1 ,
2020-10-01 17:36:35 +00:00
start = > q{ } . $ opt { dep_eva } ,
destination = > q{ } . $ opt { arr_eva } ,
2021-07-01 17:44:09 +00:00
departure = > $ departure_ts ,
arrival = > $ arrival_ts ,
2022-01-08 09:54:54 +00:00
toot = > $ opt { data } { toot } ? \ 1 : \ 0 ,
2020-10-04 10:27:20 +00:00
tweet = > $ opt { data } { tweet } ? \ 1 : \ 0 ,
2023-07-13 18:17:51 +00:00
visibility = > convert_travelynx_to_traewelling_visibility ( $ opt { visibility } )
2020-10-01 17:36:35 +00:00
} ;
2020-10-03 12:33:56 +00:00
if ( $ opt { user_data } { comment } ) {
$ request - > { body } = $ opt { user_data } { comment } ;
}
2022-08-17 19:51:24 +00:00
my $ debug_prefix
2022-11-10 16:50:31 +00:00
= "v1/trains/checkin('$request->{lineName}' $request->{tripId} $request->{start} -> $request->{destination})" ;
2022-08-17 19:51:24 +00:00
2023-01-07 12:31:14 +00:00
my $ promise = Mojo::Promise - > new ;
2020-10-01 17:36:35 +00:00
$ self - > { user_agent } - > request_timeout ( 20 )
2022-01-08 09:54:54 +00:00
- > post_p (
2022-11-10 16:50:31 +00:00
"https://traewelling.de/api/v1/trains/checkin" = > $ header = > json = >
2022-01-08 09:54:54 +00:00
$ request ) - > then (
2020-10-01 17:36:35 +00:00
sub {
my ( $ tx ) = @ _ ;
if ( my $ err = $ tx - > error ) {
my $ err_msg = "HTTP $err->{code} $err->{message}" ;
2022-07-03 10:14:38 +00:00
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' ,
2023-01-07 11:32:57 +00:00
$ j - > { message } { lineName } ,
$ j - > { message } { status_id }
2022-07-03 10:14:38 +00:00
) ;
}
else {
$ err_msg . = ' ' . $ tx - > res - > body ;
}
}
2022-12-23 20:30:28 +00:00
$ self - > { log }
- > debug ( "Traewelling $debug_prefix error: $err_msg" ) ;
2020-10-01 17:36:35 +00:00
$ self - > { model } - > log (
2022-01-08 09:54:54 +00:00
uid = > $ opt { uid } ,
2020-10-01 17:36:35 +00:00
message = >
2022-08-17 19:51:24 +00:00
"Konnte $opt{train_type} $opt{train_no} nicht übertragen: $debug_prefix returned $err_msg" ,
2020-10-01 17:36:35 +00:00
is_error = > 1
) ;
2023-01-07 12:31:14 +00:00
$ promise - > reject ( { http = > $ err - > { code } } ) ;
2020-09-30 17:12:29 +00:00
return ;
}
2020-10-04 10:27:20 +00:00
$ self - > { log } - > debug ( "... success! " . $ tx - > res - > body ) ;
2020-10-04 08:35:04 +00:00
2020-10-01 17:36:35 +00:00
$ 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 }
2020-09-30 17:12:29 +00:00
) ;
2023-01-07 12:31:14 +00:00
$ promise - > resolve ( { http = > $ tx - > res - > code } ) ;
2020-09-30 17:12:29 +00:00
2020-10-01 17:36:35 +00:00
# TODO store status_id in in_transit object so that it can be shown
# on the user status page
2023-01-07 12:31:14 +00:00
return ;
2020-09-30 17:12:29 +00:00
}
2020-10-01 17:36:35 +00:00
) - > catch (
sub {
my ( $ err ) = @ _ ;
2022-08-17 19:51:24 +00:00
$ self - > { log } - > debug ( "... $debug_prefix error: $err" ) ;
2020-10-01 17:36:35 +00:00
$ self - > { model } - > log (
2022-02-16 19:56:28 +00:00
uid = > $ opt { uid } ,
message = >
2022-08-17 19:51:24 +00:00
"Konnte $opt{train_type} $opt{train_no} nicht übertragen: $debug_prefix returned $err" ,
2020-10-01 17:36:35 +00:00
is_error = > 1
) ;
2023-01-07 12:31:14 +00:00
$ promise - > reject ( { connection = > $ err } ) ;
return ;
2020-10-01 17:36:35 +00:00
}
) - > wait ;
2023-01-07 12:31:14 +00:00
return $ promise ;
2020-09-30 17:12:29 +00:00
}
1 ;