diff options
| author | Daniel Friesel <derf@finalrewind.org> | 2020-09-30 19:12:29 +0200 | 
|---|---|---|
| committer | Daniel Friesel <derf@finalrewind.org> | 2020-09-30 19:12:29 +0200 | 
| commit | 89e709d8d593939ab528b81d125fd37d303c4fa9 (patch) | |
| tree | 574a0fbe6b6b0849d878c228e1f6b84e2c29d878 /lib | |
| parent | 952740969ca9fa74c893dfe0961d3ae55ec9e85b (diff) | |
Allow linking a Träwelling account, auto-sync Träwelling→travelynx
travelynx→Träwelling is still work-in-progress
Squashed commit of the following:
commit 97faa6e2e6c8d20fba30f2d0f6e78187ceeb72e6
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Wed Sep 30 18:50:05 2020 +0200
    improve traewelling log and tx handling
commit 487d7dd728b9d45b731bdc7098cf3358ea2e206e
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Wed Sep 30 18:02:41 2020 +0200
    add missing traewelling template
commit 0148da2f48d9a52dcddc0ab81f83d8f8ac3062ab
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Wed Sep 30 18:02:35 2020 +0200
    improve traewelling pull sync
commit 4861a9750f9f2d7621043361d0af6b0a8869a0df
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Tue Sep 29 22:14:24 2020 +0200
    wip checkin from traewelling
commit f6aeb6f06998a2a7a80f63a7b1b688b1a26b66bd
Author: Daniel Friesel <derf@finalrewind.org>
Date:   Tue Sep 29 18:37:53 2020 +0200
    refactor traewelling integration. login and logout are less of a hack now.
    checkin and checkout are not supported at the moment.
Diffstat (limited to 'lib')
| -rwxr-xr-x | lib/Travelynx.pm | 251 | ||||
| -rw-r--r-- | lib/Travelynx/Command/database.pm | 26 | ||||
| -rw-r--r-- | lib/Travelynx/Command/work.pm | 45 | ||||
| -rwxr-xr-x | lib/Travelynx/Controller/Api.pm | 20 | ||||
| -rw-r--r-- | lib/Travelynx/Controller/Traewelling.pm | 104 | ||||
| -rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 30 | ||||
| -rw-r--r-- | lib/Travelynx/Helper/Traewelling.pm | 332 | ||||
| -rw-r--r-- | lib/Travelynx/Model/Traewelling.pm | 204 | 
8 files changed, 971 insertions, 41 deletions
| diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index 4f58f57..ab1f7ef 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -20,7 +20,9 @@ use Travelynx::Helper::DBDB;  use Travelynx::Helper::HAFAS;  use Travelynx::Helper::IRIS;  use Travelynx::Helper::Sendmail; +use Travelynx::Helper::Traewelling;  use Travelynx::Model::Journeys; +use Travelynx::Model::Traewelling;  use Travelynx::Model::Users;  use XML::LibXML; @@ -293,6 +295,26 @@ sub startup {  	);  	$self->helper( +		traewelling => sub { +			my ($self) = @_; +			state $trwl = Travelynx::Model::Traewelling->new( pg => $self->pg ); +		} +	); + +	$self->helper( +		traewelling_api => sub { +			my ($self) = @_; +			state $trwl_api = Travelynx::Helper::Traewelling->new( +				log        => $self->app->log, +				model      => $self->traewelling, +				root_url   => $self->url_for('/')->to_abs, +				user_agent => $self->ua, +				version    => $self->app->config->{version}, +			); +		} +	); + +	$self->helper(  		journeys => sub {  			my ($self) = @_;  			state $journeys = Travelynx::Model::Journeys->new( @@ -389,9 +411,12 @@ sub startup {  	$self->helper(  		'checkin' => sub { -			my ( $self, $station, $train_id, $uid ) = @_; +			my ( $self, %opt ) = @_; -			$uid //= $self->current_user->{id}; +			my $station  = $opt{station}; +			my $train_id = $opt{train_id}; +			my $uid      = $opt{uid} // $self->current_user->{id}; +			my $db       = $opt{db} // $self->pg->db;  			my $status = $self->iris->get_departures(  				station    => $station, @@ -409,7 +434,7 @@ sub startup {  				}  				else { -					my $user = $self->get_user_status($uid); +					my $user = $self->get_user_status( $uid, $db );  					if ( $user->{checked_in} or $user->{cancelled} ) {  						if (    $user->{train_id} eq $train_id @@ -420,12 +445,17 @@ sub startup {  						}  						# Otherwise, someone forgot to check out first -						$self->checkout( $station, 1, $uid ); +						$self->checkout( +							station => $station, +							force   => 1, +							uid     => $uid, +							db      => $db +						);  					}  					eval {  						my $json = JSON->new; -						$self->pg->db->insert( +						$db->insert(  							'in_transit',  							{  								user_id   => $uid, @@ -459,8 +489,12 @@ sub startup {  							"Checkin($uid): INSERT failed: $@");  						return ( undef, 'INSERT failed: ' . $@ );  					} -					$self->add_route_timestamps( $uid, $train, 1 ); -					$self->run_hook( $uid, 'checkin' ); +					if ( not $opt{in_transaction} ) { + +						# mustn't be called during a transaction +						$self->add_route_timestamps( $uid, $train, 1 ); +						$self->run_hook( $uid, 'checkin' ); +					}  					return ( $train, undef );  				}  			} @@ -547,16 +581,19 @@ sub startup {  	$self->helper(  		'checkout' => sub { -			my ( $self, $station, $force, $uid ) = @_; +			my ( $self, %opt ) = @_; -			my $db     = $self->pg->db; -			my $status = $self->iris->get_departures( +			my $station = $opt{station}; +			my $force   = $opt{force}; +			my $uid     = $opt{uid}; +			my $db      = $opt{db} // $self->pg->db; +			my $status  = $self->iris->get_departures(  				station    => $station,  				lookbehind => 120,  				lookahead  => 120  			);  			$uid //= $self->current_user->{id}; -			my $user     = $self->get_user_status($uid); +			my $user     = $self->get_user_status( $uid, $db );  			my $train_id = $user->{train_id};  			if ( not $user->{checked_in} and not $user->{cancelled} ) { @@ -671,7 +708,11 @@ sub startup {  					}  				}  				if ( not $force ) { -					$self->run_hook( $uid, 'update' ); + +					# mustn't be called during a transaction +					if ( not $opt{in_transaction} ) { +						$self->run_hook( $uid, 'update' ); +					}  					return ( 1, undef );  				}  			} @@ -680,7 +721,10 @@ sub startup {  			eval { -				my $tx = $db->begin; +				my $tx; +				if ( not $opt{in_transaction} ) { +					$tx = $db->begin; +				}  				if ( defined $train and not $train->arrival and not $force ) {  					my $train_no = $train->train_no; @@ -778,7 +822,9 @@ sub startup {  					);  				} -				$tx->commit; +				if ( not $opt{in_transaction} ) { +					$tx->commit; +				}  			};  			if ($@) { @@ -787,27 +833,33 @@ sub startup {  			}  			if ( $has_arrived or $force ) { -				$self->run_hook( $uid, 'checkout' ); +				if ( not $opt{in_transaction} ) { +					$self->run_hook( $uid, 'checkout' ); +				}  				return ( 0, undef );  			} -			$self->run_hook( $uid, 'update' ); -			$self->add_route_timestamps( $uid, $train, 0 ); +			if ( not $opt{in_transaction} ) { +				$self->run_hook( $uid, 'update' ); +				$self->add_route_timestamps( $uid, $train, 0 ); +			}  			return ( 1, undef );  		}  	);  	$self->helper(  		'update_in_transit_comment' => sub { -			my ( $self, $comment, $uid ) = @_; +			my ( $self, $comment, $uid, $db ) = @_;  			$uid //= $self->current_user->{id}; +			$db  //= $self->pg->db; -			my $status = $self->pg->db->select( 'in_transit', ['user_data'], -				{ user_id => $uid } )->expand->hash; +			my $status +			  = $db->select( 'in_transit', ['user_data'], { user_id => $uid } ) +			  ->expand->hash;  			if ( not $status ) {  				return;  			}  			$status->{user_data}{comment} = $comment; -			$self->pg->db->update( +			$db->update(  				'in_transit',  				{ user_data => JSON->new->encode( $status->{user_data} ) },  				{ user_id   => $uid } @@ -1872,11 +1924,11 @@ sub startup {  	$self->helper(  		'get_user_status' => sub { -			my ( $self, $uid ) = @_; +			my ( $self, $uid, $db ) = @_;  			$uid //= $self->current_user->{id}; +			$db  //= $self->pg->db; -			my $db    = $self->pg->db;  			my $now   = DateTime->now( time_zone => 'Europe/Berlin' );  			my $epoch = $now->epoch; @@ -2316,6 +2368,157 @@ sub startup {  	);  	$self->helper( +		'traewelling_to_travelynx' => sub { +			my ( $self, %opt ) = @_; +			my $traewelling = $opt{traewelling}; +			my $user_data   = $opt{user_data}; +			my $uid         = $user_data->{user_id}; + +			if ( not $traewelling->{checkin} +				or $self->now->epoch - $traewelling->{checkin}->epoch > 900 ) +			{ +				$self->log->debug("... not checked in"); +				return; +			} +			if (    $traewelling->{status_id} +				and $user_data->{data}{latest_pull_status_id} +				and $traewelling->{status_id} +				== $user_data->{data}{latest_pull_status_id} ) +			{ +				$self->log->debug("... already handled"); +				return; +			} +			$self->log->debug("... checked in"); +			my $user_status = $self->get_user_status($uid); +			if ( $user_status->{checked_in} ) { +				$self->log->debug( +					"... also checked in via travelynx. aborting."); +				return; +			} + +			if ( $traewelling->{category} +				!~ m{^ (?: nationalExpress | regional | suburban ) $ }x ) +			{ +				$self->log->debug("... status is not a train"); +				$self->traewelling->log( +					uid => $uid, +					message => +"$traewelling->{line} nach $traewelling->{arr_name} ist keine Zugfahrt", +					status_id => $traewelling->{status_id}, +				); +				$self->traewelling->set_latest_pull_status_id( +					uid       => $uid, +					status_id => $traewelling->{status_id} +				); +				return; +			} + +			my $dep = $self->iris->get_departures( +				station    => $traewelling->{dep_eva}, +				lookbehind => 60, +				lookahead  => 40 +			); +			if ( $dep->{errstr} ) { +				$self->traewelling->log( +					uid => $uid, +					message => +"Fehler bei $traewelling->{line} nach $traewelling->{arr_name}: $dep->{errstr}", +					status_id => $traewelling->{status_id}, +					is_error  => 1, +				); +				return; +			} +			my ( $train_ref, $train_id ); +			for my $train ( @{ $dep->{results} } ) { +				if ( $train->line ne $traewelling->{line} ) { +					next; +				} +				if ( not $train->sched_departure +					or $train->sched_departure->epoch +					!= $traewelling->{dep_dt}->epoch ) +				{ +					next; +				} +				if ( +					not List::Util::first { $_ eq $traewelling->{arr_name} } +					$train->route_post +				  ) +				{ +					next; +				} +				$train_id  = $train->train_id; +				$train_ref = $train; +				last; +			} +			if ($train_id) { +				$self->log->debug("... found train: $train_id"); + +				my $db = $self->pg->db; +				my $tx = $db->begin; + +				my ( undef, $err ) = $self->checkin( +					station        => $traewelling->{dep_eva}, +					train_id       => $train_id, +					uid            => $uid, +					in_transaction => 1, +					db             => $db +				); + +				if ( not $err ) { +					( undef, $err ) = $self->checkout( +						station        => $traewelling->{arr_eva}, +						train_id       => 0, +						uid            => $uid, +						in_transaction => 1, +						db             => $db +					); +					if ( not $err ) { +						$self->log->debug("... success!"); +						if ( $traewelling->{message} ) { +							$self->update_in_transit_comment( +								$traewelling->{message}, +								$uid, $db ); +						} +						$self->traewelling->log( +							uid => $uid, +							db  => $db, +							message => +"Eingecheckt in $traewelling->{line} nach $traewelling->{arr_name}", +							status_id => $traewelling->{status_id}, +						); +						$self->traewelling->set_latest_pull_status_id( +							uid       => $uid, +							status_id => $traewelling->{status_id}, +							db        => $db +						); + +						$tx->commit; +					} +				} +				if ($err) { +					$self->log->debug("... error: $err"); +					$self->traewelling->log( +						uid => $uid, +						message => +"Fehler bei $traewelling->{line} nach $traewelling->{arr_name}: $err", +						status_id => $traewelling->{status_id}, +						is_error  => 1 +					); +				} +			} +			else { +				$self->traewelling->log( +					uid => $uid, +					message => +"$traewelling->{line} nach $traewelling->{arr_name} nicht gefunden", +					status_id => $traewelling->{status_id}, +					is_error  => 1 +				); +			} +		} +	); + +	$self->helper(  		'journeys_to_map_data' => sub {  			my ( $self, %opt ) = @_; @@ -2647,6 +2850,7 @@ sub startup {  	$authed_r->get('/account')->to('account#account');  	$authed_r->get('/account/privacy')->to('account#privacy');  	$authed_r->get('/account/hooks')->to('account#webhook'); +	$authed_r->get('/account/traewelling')->to('traewelling#settings');  	$authed_r->get('/account/insight')->to('account#insight');  	$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');  	$authed_r->get('/cancelled')->to('traveling#cancelled'); @@ -2668,6 +2872,7 @@ sub startup {  	$authed_r->get('/confirm_mail/:token')->to('account#confirm_mail');  	$authed_r->post('/account/privacy')->to('account#privacy');  	$authed_r->post('/account/hooks')->to('account#webhook'); +	$authed_r->post('/account/traewelling')->to('traewelling#settings');  	$authed_r->post('/account/insight')->to('account#insight');  	$authed_r->post('/journey/add')->to('traveling#add_journey_form');  	$authed_r->post('/journey/comment')->to('traveling#comment_form'); diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index 59e41d9..e92dd4b 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -1012,6 +1012,32 @@ my @migrations = (  			}  		);  	}, + +	# v21 -> v22 +	sub { +		my ($db) = @_; +		$db->query( +			qq{ +				create table traewelling ( +					user_id integer not null references users (id) primary key, +					email varchar(256) not null, +					push_sync boolean not null, +					pull_sync boolean not null, +					errored boolean, +					token text, +					data jsonb, +					latest_run timestamptz +				); +				comment on table traewelling is 'Token and Status for Traewelling'; +				create view traewelling_str as select +					user_id, email, push_sync, pull_sync, errored, token, data, +					extract(epoch from latest_run) as latest_run_ts +					from traewelling +				; +				update schema_version set version = 22; +			} +		); +	},  );  sub setup_db { diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm index 9c870d8..593735f 100644 --- a/lib/Travelynx/Command/work.pm +++ b/lib/Travelynx/Command/work.pm @@ -108,7 +108,11 @@ sub run {                    # check out (adds a cancelled journey and resets journey state                    # to checkin -						$self->app->checkout( $arr, 1, $uid ); +						$self->app->checkout( +							station => $arr, +							force   => 1, +							uid     => $uid +						);  					}  				}  				else { @@ -201,7 +205,11 @@ sub run {  					{                    # check out (adds a cancelled journey and resets journey state                    # to destination selection) -						$self->app->checkout( $arr, 0, $uid ); +						$self->app->checkout( +							station => $arr, +							force   => 0, +							uid     => $uid +						);  					}  				}  				else { @@ -209,7 +217,11 @@ sub run {  				}  			}  			elsif ( $entry->{real_arr_ts} ) { -				my ( undef, $error ) = $self->app->checkout( $arr, 1, $uid ); +				my ( undef, $error ) = $self->app->checkout( +					station => $arr, +					force   => 1, +					uid     => $uid +				);  				if ($error) {  					die("${error}\n");  				} @@ -222,6 +234,31 @@ sub run {  		eval { }  	} +	for my $account_data ( $self->app->traewelling->get_pull_accounts ) { + +		# $account_data->{user_id} is the travelynx uid +		# $account_data->{user_name} is the Träwelling username +		$self->app->log->debug( +			"Pulling Traewelling status for UID $account_data->{user_id}"); +		$self->app->traewelling_api->get_status_p( +			username => $account_data->{data}{user_name}, +			token    => $account_data->{token} +		)->then( +			sub { +				my ($traewelling) = @_; +				$self->app->traewelling_to_travelynx( +					traewelling => $traewelling, +					user_data   => $account_data +				); +			} +		)->catch( +			sub { +				my ($err) = @_; +				$self->app->log->debug("Error $err"); +			} +		)->wait; +	} +  	# Computing yearly stats may take a while, but we've got all time in the  	# world here. This means users won't have to wait when loading their  	# own by-year journey log. @@ -232,6 +269,8 @@ sub run {  			year => $now->year  		);  	} + +	# TODO wait until all background jobs have terminated  }  1; diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm index c7e58a9..8d2d2a7 100755 --- a/lib/Travelynx/Controller/Api.pm +++ b/lib/Travelynx/Controller/Api.pm @@ -258,14 +258,21 @@ sub travel_v1 {  			$train_id = $train->train_id;  		} -		my ( $train, $error ) -		  = $self->checkin( $from_station, $train_id, $uid ); +		my ( $train, $error ) = $self->checkin( +			station  => $from_station, +			train_id => $train_id, +			uid      => $uid +		);  		if ( $payload->{comment} and not $error ) {  			$self->update_in_transit_comment(  				sanitize( q{}, $payload->{comment} ), $uid );  		}  		if ( $to_station and not $error ) { -			( $train, $error ) = $self->checkout( $to_station, 0, $uid ); +			( $train, $error ) = $self->checkout( +				station => $to_station, +				force   => 0, +				uid     => $uid +			);  		}  		if ($error) {  			$self->render( @@ -307,8 +314,11 @@ sub travel_v1 {  				sanitize( q{}, $payload->{comment} ), $uid );  		} -		my ( $train, $error ) -		  = $self->checkout( $to_station, $payload->{force} ? 1 : 0, $uid ); +		my ( $train, $error ) = $self->checkout( +			station => $to_station, +			force   => $payload->{force} ? 1 : 0, +			uid     => $uid +		);  		if ($error) {  			$self->render(  				json => { diff --git a/lib/Travelynx/Controller/Traewelling.pm b/lib/Travelynx/Controller/Traewelling.pm new file mode 100644 index 0000000..78c501f --- /dev/null +++ b/lib/Travelynx/Controller/Traewelling.pm @@ -0,0 +1,104 @@ +package Travelynx::Controller::Traewelling; +use Mojo::Base 'Mojolicious::Controller'; +use Mojo::Promise; + +sub settings { +	my ($self) = @_; + +	my $uid = $self->current_user->{id}; + +	if (    $self->param('action') +		and $self->validation->csrf_protect->has_error('csrf_token') ) +	{ +		$self->render( +			'traewelling', +			invalid => 'csrf', +		); +		return; +	} + +	if ( $self->param('action') and $self->param('action') eq 'login' ) { +		my $email    = $self->param('email'); +		my $password = $self->param('password'); +		$self->render_later; +		$self->traewelling_api->login_p( +			uid      => $uid, +			email    => $email, +			password => $password +		)->then( +			sub { +				my $traewelling = $self->traewelling->get($uid); +				$self->param( sync_source => 'none' ); +				$self->render( +					'traewelling', +					traewelling     => $traewelling, +					new_traewelling => 1, +				); +			} +		)->catch( +			sub { +				my ($err) = @_; +				$self->render( +					'traewelling', +					traewelling     => {}, +					new_traewelling => 1, +					login_error     => $err, +				); +			} +		)->wait; +		return; +	} +	elsif ( $self->param('action') and $self->param('action') eq 'logout' ) { +		$self->render_later; +		my $traewelling = $self->traewelling->get($uid); +		$self->traewelling_api->logout_p( +			uid   => $uid, +			token => $traewelling->{token} +		)->then( +			sub { +				$self->flash( success => 'traewelling' ); +				$self->redirect_to('account'); +			} +		)->catch( +			sub { +				my ($err) = @_; +				$self->render( +					'traewelling', +					traewelling     => {}, +					new_traewelling => 1, +					logout_error    => $err, +				); +			} +		)->wait; +		return; +	} +	elsif ( $self->param('action') and $self->param('action') eq 'config' ) { +		$self->traewelling->set_sync( +			uid       => $uid, +			push_sync => $self->param('sync_source') eq 'travelynx' ? 1 : 0, +			pull_sync => $self->param('sync_source') eq 'traewelling' ? 1 : 0 +		); +		$self->flash( success => 'traewelling' ); +		$self->redirect_to('account'); +		return; +	} + +	my $traewelling = $self->traewelling->get($uid); + +	if ( $traewelling->{push_sync} ) { +		$self->param( sync_source => 'travelynx' ); +	} +	elsif ( $traewelling->{pull_sync} ) { +		$self->param( sync_source => 'traewelling' ); +	} +	else { +		$self->param( sync_source => 'none' ); +	} + +	$self->render( +		'traewelling', +		traewelling => $traewelling, +	); +} + +1; diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index f5e3255..4df1558 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -424,8 +424,10 @@ sub log_action {  	if ( $params->{action} eq 'checkin' ) { -		my ( $train, $error ) -		  = $self->checkin( $params->{station}, $params->{train} ); +		my ( $train, $error ) = $self->checkin( +			station  => $params->{station}, +			train_id => $params->{train} +		);  		my $destination = $params->{dest};  		if ($error) { @@ -447,8 +449,10 @@ sub log_action {  		else {  			# Silently ignore errors -- if they are permanent, the user will see  			# them when selecting the destination manually. -			my ( $still_checked_in, undef ) -			  = $self->checkout( $destination, 0 ); +			my ( $still_checked_in, undef ) = $self->checkout( +				station => $destination, +				force   => 0 +			);  			my $station_link = '/s/' . $destination;  			$self->render(  				json => { @@ -459,8 +463,10 @@ sub log_action {  		}  	}  	elsif ( $params->{action} eq 'checkout' ) { -		my ( $still_checked_in, $error ) -		  = $self->checkout( $params->{station}, $params->{force} ); +		my ( $still_checked_in, $error ) = $self->checkout( +			station => $params->{station}, +			force   => $params->{force} +		);  		my $station_link = '/s/' . $params->{station};  		if ($error) { @@ -505,8 +511,10 @@ sub log_action {  		}  	}  	elsif ( $params->{action} eq 'cancelled_from' ) { -		my ( undef, $error ) -		  = $self->checkin( $params->{station}, $params->{train} ); +		my ( undef, $error ) = $self->checkin( +			station  => $params->{station}, +			train_id => $params->{train} +		);  		if ($error) {  			$self->render( @@ -526,8 +534,10 @@ sub log_action {  		}  	}  	elsif ( $params->{action} eq 'cancelled_to' ) { -		my ( undef, $error ) -		  = $self->checkout( $params->{station}, 1 ); +		my ( undef, $error ) = $self->checkout( +			station => $params->{station}, +			force   => 1 +		);  		if ($error) {  			$self->render( diff --git a/lib/Travelynx/Helper/Traewelling.pm b/lib/Travelynx/Helper/Traewelling.pm new file mode 100644 index 0000000..3c7bec2 --- /dev/null +++ b/lib/Travelynx/Helper/Traewelling.pm @@ -0,0 +1,332 @@ +package Travelynx::Helper::Traewelling; + +use strict; +use warnings; +use 5.020; + +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" +	  }; + +	return bless( \%opt, $class ); +} + +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'}, +		'Authorization' => "Bearer $token", +	}; + +	$self->{user_agent}->request_timeout(20) +	  ->get_p( "https://traewelling.de/api/v0/user/${username}" => $header ) +	  ->then( +		sub { +			my ($tx) = @_; +			if ( my $err = $tx->error ) { +				my $err_msg = "HTTP $err->{code} $err->{message}"; +				$promise->reject($err_msg); +				return; +			} +			else { +				if ( my $status = $tx->result->json->{statuses}{data}[0] ) { +					my $strp = DateTime::Format::Strptime->new( +						pattern   => '%Y-%m-%dT%H:%M:%S.000000Z', +						time_zone => 'UTC', +					); +					my $status_id = $status->{id}; +					my $message   = $status->{body}; +					my $checkin_at +					  = $strp->parse_datetime( $status->{created_at} ); + +					my $dep_dt = $strp->parse_datetime( +						$status->{train_checkin}{departure} ); +					my $arr_dt = $strp->parse_datetime( +						$status->{train_checkin}{arrival} ); + +					my $dep_eva +					  = $status->{train_checkin}{origin}{ibnr}; +					my $arr_eva +					  = $status->{train_checkin}{destination}{ibnr}; + +					my $dep_name +					  = $status->{train_checkin}{origin}{name}; +					my $arr_name +					  = $status->{train_checkin}{destination}{name}; + +					my $category +					  = $status->{train_checkin}{hafas_trip}{category}; +					my $trip_id +					  = $status->{train_checkin}{hafas_trip}{trip_id}; +					my $linename +					  = $status->{train_checkin}{hafas_trip}{linename}; +					my ( $train_type, $train_line ) = split( qr{ }, $linename ); +					$promise->resolve( +						{ +							status_id  => $status_id, +							message    => $message, +							checkin    => $checkin_at, +							dep_dt     => $dep_dt, +							dep_eva    => $dep_eva, +							dep_name   => $dep_name, +							arr_dt     => $arr_dt, +							arr_eva    => $arr_eva, +							arr_name   => $arr_name, +							trip_id    => $trip_id, +							train_type => $train_type, +							line       => $linename, +							line_no    => $train_line, +							category   => $category, +						} +					); +					return; +				} +				else { +					$promise->reject("unknown error"); +					return; +				} +			} +		} +	)->catch( +		sub { +			my ($err) = @_; +			$promise->reject($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'}, +		'Authorization' => "Bearer $token", +	}; +	my $promise = Mojo::Promise->new; + +	$ua->get_p( "https://traewelling.de/api/v0/getuser" => $header )->then( +		sub { +			my ($tx) = @_; +			if ( my $err = $tx->error ) { +				my $err_msg +				  = "HTTP $err->{code} $err->{message} bei Abfrage der Nutzerdaten"; +				$promise->reject($err_msg); +				return; +			} +			else { +				my $user_data = $tx->result->json; +				$self->{model}->set_user( +					uid         => $uid, +					trwl_id     => $user_data->{id}, +					screen_name => $user_data->{name}, +					user_name   => $user_data->{username}, +				); +				$promise->resolve; +				return; +			} +		} +	)->catch( +		sub { +			my ($err) = @_; +			$promise->reject("$err bei Abfrage der Nutzerdaten"); +			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 = { +		email    => $email, +		password => $password, +	}; + +	my $promise = Mojo::Promise->new; +	my $token; + +	$ua->post_p( +		"https://traewelling.de/api/v0/auth/login" => $self->{header} => +		  json                                     => $request )->then( +		sub { +			my ($tx) = @_; +			if ( my $err = $tx->error ) { +				my $err_msg = "HTTP $err->{code} $err->{message} bei Login"; +				$promise->reject($err_msg); +				return; +			} +			else { +				$token = $tx->result->json->{token}; +				$self->{model}->link( +					uid   => $uid, +					email => $email, +					token => $token +				); +				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 { +						$promise->reject($err); +						return; +					} +				); +			} +			else { +				$promise->reject($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'}, +		'Authorization' => "Bearer $token", +	}; +	my $request = {}; + +	$self->{model}->unlink( uid => $uid ); + +	my $promise = Mojo::Promise->new; + +	$ua->post_p( +		"https://traewelling.de/api/v0/auth/logout" => $header => json => +		  $request )->then( +		sub { +			my ($tx) = @_; +			if ( my $err = $tx->error ) { +				my $err_msg = "HTTP $err->{code} $err->{message}"; +				$promise->reject($err_msg); +				return; +			} +			else { +				$promise->resolve; +				return; +			} +		} +	)->catch( +		sub { +			my ($err) = @_; +			$promise->reject($err); +			return; +		} +	)->wait; + +	return $promise; +} + +sub checkin { +	my ( $self, $uid ) = @_; +	if ( my $token = $self->get_traewelling_push_token($uid) ) { +		my $user = $self->get_user_status; + +# TODO delete previous traewelling status if the train's destination has been changed +# TODO delete traewelling status when undoing a travelynx checkin +		if ( $user->{checked_in} and $user->{extra_data}{trip_id} ) { +			my $traewelling = $self->{model}->get($uid); +			if ( $traewelling->{data}{trip_id} eq $user->{extra_data}{trip_id} ) +			{ +				return; +			} +			my $header = { +				'User-Agent'    => 'travelynx/' . $self->{version}, +				'Authorization' => "Bearer $token", +			}; + +			my $request = { +				tripID      => $user->{extra_data}{trip_id}, +				start       => q{} . $user->{dep_eva}, +				destination => q{} . $user->{arr_eva}, +			}; +			my $trip_req = sprintf( +				"tripID=%s&lineName=%s%%20%s&start=%s", +				$user->{extra_data}{trip_id}, $user->{train_type}, +				$user->{train_line} // $user->{train_no}, $user->{dep_eva} +			); +			$self->{user_agent}->request_timeout(20) +			  ->get_p( +				"https://traewelling.de/api/v0/trains/trip?$trip_req" => +				  $header )->then( +				sub { +					return $self->{user_agent}->request_timeout(20) +					  ->post_p( +						"https://traewelling.de/api/v0/trains/checkin" => +						  $header => json => $request ); +				} +			)->then( +				sub { +					my ($tx) = @_; +					if ( my $err = $tx->error ) { +						my $err_msg = "HTTP $err->{code} $err->{message}"; +						$self->mark_trwl_checkin_error( $uid, $user, $err_msg ); +					} +					else { +  # TODO check for traewelling error ("error" key in response) +  # TODO store ID of resulting status (request /user/{name} and store status ID) +						$self->mark_trwl_checkin_success( $uid, $user ); + +                      # mark success: checked into (trip_id, start, destination) +					} +				} +			)->catch( +				sub { +					my ($err) = @_; +					$self->mark_trwl_checkin_error( $uid, $user, $err ); +				} +			)->wait; +		} +	} +} + +1; diff --git a/lib/Travelynx/Model/Traewelling.pm b/lib/Travelynx/Model/Traewelling.pm new file mode 100644 index 0000000..7f08b0d --- /dev/null +++ b/lib/Travelynx/Model/Traewelling.pm @@ -0,0 +1,204 @@ +package Travelynx::Model::Traewelling; + +use strict; +use warnings; +use 5.020; + +use DateTime; + +sub epoch_to_dt { +	my ($epoch) = @_; + +	# Bugs (and user errors) may lead to undefined timestamps. Set them to +	# 1970-01-01 to avoid crashing and show obviously wrong data instead. +	$epoch //= 0; + +	return DateTime->from_epoch( +		epoch     => $epoch, +		time_zone => 'Europe/Berlin', +		locale    => 'de-DE', +	); + +} + +sub new { +	my ( $class, %opt ) = @_; + +	return bless( \%opt, $class ); +} + +sub now { +	return DateTime->now( time_zone => 'Europe/Berlin' ); +} + +sub link { +	my ( $self, %opt ) = @_; + +	my $log = [ [ $self->now->epoch, "Erfolgreich angemeldet" ] ]; + +	my $data = { +		user_id   => $opt{uid}, +		email     => $opt{email}, +		push_sync => 0, +		pull_sync => 0, +		token     => $opt{token}, +		data      => JSON->new->encode( { log => $log } ), +	}; + +	$self->{pg}->db->insert( +		'traewelling', +		$data, +		{ +			on_conflict => \ +'(user_id) do update set email = EXCLUDED.email, token = EXCLUDED.token, push_sync = false, pull_sync = false, data = null, errored = false, latest_run = null' +		} +	); + +	return $data; +} + +sub set_user { +	my ( $self, %opt ) = @_; + +	my $res_h +	  = $self->{pg} +	  ->db->select( 'traewelling', 'data', { user_id => $opt{uid} } ) +	  ->expand->hash; + +	$res_h->{data}{user_id}     = $opt{trwl_id}; +	$res_h->{data}{screen_name} = $opt{screen_name}; +	$res_h->{data}{user_name}   = $opt{user_name}; + +	$self->{pg}->db->update( +		'traewelling', +		{ data    => JSON->new->encode( $res_h->{data} ) }, +		{ user_id => $opt{uid} } +	); +} + +sub unlink { +	my ( $self, %opt ) = @_; + +	my $uid = $opt{uid}; + +	$self->{pg}->db->delete( 'traewelling', { user_id => $uid } ); +} + +sub get { +	my ( $self, $uid ) = @_; +	$uid //= $self->current_user->{id}; + +	my $res_h +	  = $self->{pg}->db->select( 'traewelling_str', '*', { user_id => $uid } ) +	  ->expand->hash; + +	$res_h->{latest_run} = epoch_to_dt( $res_h->{latest_run_ts} ); +	for my $log_entry ( @{ $res_h->{data}{log} // [] } ) { +		$log_entry->[0] = epoch_to_dt( $log_entry->[0] ); +	} + +	return $res_h; +} + +sub log { +	my ( $self, %opt ) = @_; +	my $uid      = $opt{uid}; +	my $message  = $opt{message}; +	my $is_error = $opt{is_error}; +	my $db       = $opt{db} // $self->{pg}->db; +	my $res_h +	  = $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash; +	splice( @{ $res_h->{data}{log} // [] }, 9 ); +	unshift( +		@{ $res_h->{data}{log} }, +		[ $self->now->epoch, $message, $opt{status_id} ] +	); + +	if ($is_error) { +		$res_h->{data}{error} = $message; +	} +	$db->update( +		'traewelling', +		{ +			errored    => $is_error ? 1 : 0, +			latest_run => $self->now, +			data       => JSON->new->encode( $res_h->{data} ) +		}, +		{ user_id => $uid } +	); +} + +sub set_latest_pull_status_id { +	my ( $self, %opt ) = @_; +	my $uid       = $opt{uid}; +	my $status_id = $opt{status_id}; +	my $db        = $opt{db} // $self->{pg}->db; + +	my $res_h +	  = $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash; + +	$res_h->{data}{latest_pull_status_id} = $status_id; + +	$db->update( +		'traewelling', +		{ data    => JSON->new->encode( $res_h->{data} ) }, +		{ user_id => $uid } +	); +} + +sub set_latest_push_status_id { +	my ( $self, %opt ) = @_; +	my $uid       = $opt{uid}; +	my $status_id = $opt{status_id}; +	my $db        = $opt{db} // $self->{pg}->db; + +	my $res_h +	  = $db->select( 'traewelling', 'data', { user_id => $uid } )->expand->hash; + +	$res_h->{data}{latest_push_status_id} = $status_id; + +	$db->update( +		'traewelling', +		{ data    => JSON->new->encode( $res_h->{data} ) }, +		{ user_id => $uid } +	); +} + +sub set_sync { +	my ( $self, %opt ) = @_; + +	my $uid       = $opt{uid}; +	my $push_sync = $opt{push_sync}; +	my $pull_sync = $opt{pull_sync}; + +	$self->{pg}->db->update( +		'traewelling', +		{ +			push_sync => $push_sync, +			pull_sync => $pull_sync +		}, +		{ user_id => $uid } +	); +} + +sub get_push_accounts { +	my ($self) = @_; +	my $res = $self->{pg}->db->select( +		'traewelling', +		[ 'user_id', 'token', 'data' ], +		{ push_sync => 1 } +	); +	return $res->expand->hashes->each; +} + +sub get_pull_accounts { +	my ($self) = @_; +	my $res = $self->{pg}->db->select( +		'traewelling', +		[ 'user_id', 'token', 'data' ], +		{ pull_sync => 1 } +	); +	return $res->expand->hashes->each; +} + +1; | 
