Commit 89e709d8 authored by Birte Kristina Friesel's avatar Birte Kristina Friesel
Browse files

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.
parent 95274096
Loading
Loading
Loading
Loading
+228 −23
Original line number Diff line number Diff line
@@ -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;

@@ -292,6 +294,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) = @_;
@@ -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: ' . $@ );
					}
					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 $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 ) {

					# 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 {
					);
				}

				if ( not $opt{in_transaction} ) {
					$tx->commit;
				}
			};

			if ($@) {
@@ -787,27 +833,33 @@ sub startup {
			}

			if ( $has_arrived or $force ) {
				if ( not $opt{in_transaction} ) {
					$self->run_hook( $uid, 'checkout' );
				}
				return ( 0, undef );
			}
			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;

@@ -2315,6 +2367,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');
+26 −0
Original line number Diff line number Diff line
@@ -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 {
+42 −3
Original line number Diff line number Diff line
@@ -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;
+15 −5
Original line number Diff line number Diff line
@@ -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 => {
+104 −0
Original line number Diff line number Diff line
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;
Loading