diff options
| author | Daniel Friesel <derf@finalrewind.org> | 2023-02-27 22:14:54 +0100 | 
|---|---|---|
| committer | Daniel Friesel <derf@finalrewind.org> | 2023-02-27 22:14:54 +0100 | 
| commit | 6d261197e3138c80357ec485749e2089ef6ff96a (patch) | |
| tree | 3089879a8863354e6b1cd669caf37bcb8558883a /lib | |
| parent | fb3878665b38f1e4c2439238c19cba0c9767f721 (diff) | |
set visibility per journey (wip)
some odds and ends left to polish, but ready for testing
Diffstat (limited to 'lib')
| -rwxr-xr-x | lib/Travelynx.pm | 26 | ||||
| -rw-r--r-- | lib/Travelynx/Command/database.pm | 114 | ||||
| -rw-r--r-- | lib/Travelynx/Controller/Account.pm | 78 | ||||
| -rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 668 | ||||
| -rw-r--r-- | lib/Travelynx/Model/InTransit.pm | 51 | ||||
| -rwxr-xr-x | lib/Travelynx/Model/Journeys.pm | 66 | ||||
| -rw-r--r-- | lib/Travelynx/Model/Users.pm | 53 | 
7 files changed, 766 insertions, 290 deletions
| diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index 98b22d8..6d903e6 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -421,6 +421,25 @@ sub startup {  	);  	$self->helper( +		'visibility_icon' => sub { +			my ( $self, $visibility ) = @_; +			if ( $visibility eq 'public' ) { +				return 'language'; +			} +			if ( $visibility eq 'travelynx' ) { +				return 'lock_open'; +			} +			if ( $visibility eq 'unlisted' ) { +				return 'lock_outline'; +			} +			if ( $visibility eq 'private' ) { +				return 'lock'; +			} +			return 'help_outline'; +		} +	); + +	$self->helper(  		'checkin' => sub {  			my ( $self, %opt ) = @_; @@ -1348,7 +1367,8 @@ sub startup {  				uid             => $uid,  				db              => $db,  				with_data       => 1, -				with_timestamps => 1 +				with_timestamps => 1, +				with_visibility => 1,  			);  			if ($in_transit) { @@ -1425,6 +1445,8 @@ sub startup {  					messages      => $in_transit->{messages},  					extra_data    => $in_transit->{data},  					comment       => $in_transit->{user_data}{comment}, +					visibility    => $in_transit->{visibility}, +					visibility_str => $in_transit->{visibility_str},  				};  				my $traewelling = $self->traewelling->get( @@ -2205,6 +2227,7 @@ sub startup {  	$authed_r->get('/history/:year/:month')->to('traveling#monthly_history');  	$authed_r->get('/journey/add')->to('traveling#add_journey_form');  	$authed_r->get('/journey/comment')->to('traveling#comment_form'); +	$authed_r->get('/journey/visibility')->to('traveling#visibility_form');  	$authed_r->get('/journey/:id')->to('traveling#journey_details');  	$authed_r->get('/s/*station')->to('traveling#station');  	$authed_r->get('/confirm_mail/:token')->to('account#confirm_mail'); @@ -2215,6 +2238,7 @@ sub startup {  	$authed_r->post('/account/services')->to('account#services');  	$authed_r->post('/journey/add')->to('traveling#add_journey_form');  	$authed_r->post('/journey/comment')->to('traveling#comment_form'); +	$authed_r->post('/journey/visibility')->to('traveling#visibility_form');  	$authed_r->post('/journey/edit')->to('traveling#edit_journey');  	$authed_r->post('/journey/passenger_rights/*filename')  	  ->to('passengerrights#generate'); diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index 5ef80f0..82f1f47 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -1312,6 +1312,120 @@ my @migrations = (  			}  		);  	}, + +	# v32 -> v33 +	# add optional per-status visibility that overrides global visibility +	sub { +		my ($db) = @_; +		$db->query( +			qq{ +				alter table journeys add column visibility smallint; +				alter table in_transit add column visibility smallint; +				drop view journeys_str; +				drop view in_transit_str; +				create view journeys_str as select +					journeys.id as journey_id, user_id, +					train_type, train_line, train_no, train_id, +					extract(epoch from checkin_time) as checkin_ts, +					extract(epoch from sched_departure) as sched_dep_ts, +					extract(epoch from real_departure) as real_dep_ts, +					checkin_station_id as dep_eva, +					dep_station.ds100 as dep_ds100, +					dep_station.name as dep_name, +					dep_station.lat as dep_lat, +					dep_station.lon as dep_lon, +					extract(epoch from checkout_time) as checkout_ts, +					extract(epoch from sched_arrival) as sched_arr_ts, +					extract(epoch from real_arrival) as real_arr_ts, +					checkout_station_id as arr_eva, +					arr_station.ds100 as arr_ds100, +					arr_station.name as arr_name, +					arr_station.lat as arr_lat, +					arr_station.lon as arr_lon, +					polylines.polyline as polyline, +					visibility, +					cancelled, edited, route, messages, user_data, +					dep_platform, arr_platform +					from journeys +					left join polylines on polylines.id = polyline_id +					left join stations as dep_station on checkin_station_id = dep_station.eva +					left join stations as arr_station on checkout_station_id = arr_station.eva +					; +				create view in_transit_str as select +					user_id, +					train_type, train_line, train_no, train_id, +					extract(epoch from checkin_time) as checkin_ts, +					extract(epoch from sched_departure) as sched_dep_ts, +					extract(epoch from real_departure) as real_dep_ts, +					checkin_station_id as dep_eva, +					dep_station.ds100 as dep_ds100, +					dep_station.name as dep_name, +					dep_station.lat as dep_lat, +					dep_station.lon as dep_lon, +					extract(epoch from checkout_time) as checkout_ts, +					extract(epoch from sched_arrival) as sched_arr_ts, +					extract(epoch from real_arrival) as real_arr_ts, +					checkout_station_id as arr_eva, +					arr_station.ds100 as arr_ds100, +					arr_station.name as arr_name, +					arr_station.lat as arr_lat, +					arr_station.lon as arr_lon, +					polylines.polyline as polyline, +					visibility, +					cancelled, route, messages, user_data, +					dep_platform, arr_platform, data +					from in_transit +					left join polylines on polylines.id = polyline_id +					left join stations as dep_station on checkin_station_id = dep_station.eva +					left join stations as arr_station on checkout_station_id = arr_station.eva +					; +			} +		); +		my $res = $db->select( 'users', [ 'id', 'public_level' ] ); +		while ( my $row = $res->hash ) { +			my $old_level = $row->{public_level}; +			my $new_level = 0; +			if ( $old_level & 0x01 ) { + +				# status: account required +				$new_level = 80; +			} +			if ( $old_level & 0x02 ) { + +				# status: public +				$new_level = 100; +			} +			if ( $old_level & 0x04 ) { + +				# comment public +				$new_level |= 0x80; +			} +			if ( $old_level & 0x10 ) { + +				# past: account required +				$new_level |= 0x100; +			} +			if ( $old_level & 0x20 ) { + +				# past: public +				$new_level |= 0x200; +			} +			if ( $old_level & 0x40 ) { + +				# past: infinite (default is 4 weeks) +				$new_level |= 0x400; +			} +			my $r = $db->update( +				'users', +				{ public_level => $new_level }, +				{ id           => $row->{id} } +			)->rows; +			if ( $r != 1 ) { +				die("oh no"); +			} +		} +		$db->update( 'schema_version', { version => 33 } ); +	},  );  sub sync_stations { diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index f0e9346..e8bfcaf 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -9,6 +9,22 @@ use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);  use JSON;  use UUID::Tiny qw(:std); +my %visibility_itoa = ( +	100 => 'public', +	80  => 'travelynx', +	60  => 'followers', +	30  => 'unlisted', +	10  => 'private', +); + +my %visibility_atoi = ( +	public    => 100, +	travelynx => 80, +	followers => 60, +	unlisted  => 30, +	private   => 10, +); +  # Internal Helpers  sub hash_password { @@ -438,50 +454,30 @@ sub privacy {  	my $public_level = $user->{is_public};  	if ( $self->param('action') and $self->param('action') eq 'save' ) { -		if ( $self->param('status_level') eq 'intern' ) { -			$public_level |= 0x01; -			$public_level &= ~0x02; -		} -		elsif ( $self->param('status_level') eq 'extern' ) { -			$public_level |= 0x02; -			$public_level &= ~0x01; -		} -		else { -			$public_level &= ~0x03; +		my %opt; +		my $default_visibility +		  = $visibility_atoi{ $self->param('status_level') }; +		if ( defined $default_visibility ) { +			$opt{default_visibility} = $default_visibility;  		} -		# public comment with non-public status does not make sense -		if (    $self->param('public_comment') -			and $self->param('status_level') ne 'private' ) -		{ -			$public_level |= 0x04; -		} -		else { -			$public_level &= ~0x04; -		} +		$opt{comments_visible} = $self->param('public_comment') ? 1 : 0; + +		$opt{past_all} = $self->param('history_age') eq 'infinite' ? 1 : 0;  		if ( $self->param('history_level') eq 'intern' ) { -			$public_level |= 0x10; -			$public_level &= ~0x20; +			$opt{past_visible} = 1;  		}  		elsif ( $self->param('history_level') eq 'extern' ) { -			$public_level |= 0x20; -			$public_level &= ~0x10; -		} -		else { -			$public_level &= ~0x30; -		} - -		if ( $self->param('history_age') eq 'infinite' ) { -			$public_level |= 0x40; +			$opt{past_visible} = 2;  		}  		else { -			$public_level &= ~0x40; +			$opt{past_visible} = 0;  		}  		$self->users->set_privacy( -			uid   => $user->{id}, -			level => $public_level +			uid => $user->{id}, +			%opt  		);  		$self->flash( success => 'privacy' ); @@ -489,18 +485,14 @@ sub privacy {  	}  	else {  		$self->param( -			  status_level => $public_level & 0x01 ? 'intern' -			: $public_level & 0x02 ? 'extern' -			:                        'private' -		); -		$self->param( public_comment => $public_level & 0x04 ? 1 : 0 ); +			status_level => $visibility_itoa{ $user->{default_visibility} } ); +		$self->param( public_comment => $user->{comments_visible} );  		$self->param( -			  history_level => $public_level & 0x10 ? 'intern' -			: $public_level & 0x20 ? 'extern' -			:                        'private' +			  history_level => $user->{past_visible} & 0x01 ? 'intern' +			: $user->{past_visible} & 0x02 ? 'extern' +			:                                'private'  		); -		$self->param( -			history_age => $public_level & 0x40 ? 'infinite' : 'month' ); +		$self->param( history_age => $user->{past_all} ? 'infinite' : 'month' );  		$self->render( 'privacy', name => $user->{name} );  	}  } diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 3f39996..15b131a 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -370,6 +370,14 @@ sub get_connecting_trains_p {  	return $promise;  } +sub compute_effective_visibility { +	my ( $self, $default_visibility, $journey_visibility ) = @_; +	if ( $journey_visibility eq 'default' ) { +		return $default_visibility; +	} +	return $journey_visibility; +} +  # Controllers  sub homepage { @@ -378,6 +386,10 @@ sub homepage {  		my $status = $self->get_user_status;  		my @recent_targets;  		if ( $status->{checked_in} ) { +			my $journey_visibility +			  = $self->compute_effective_visibility( +				$self->current_user->{default_visibility_str}, +				$status->{visibility_str} );  			if ( defined $status->{arrival_countdown}  				and $status->{arrival_countdown} < ( 40 * 60 ) )  			{ @@ -389,11 +401,10 @@ sub homepage {  							'landingpage',  							version => $self->app->config->{version}  							  // 'UNKNOWN', -							user_status       => $status, -							connections       => $connecting_trains, -							transit_fyi       => $transit_fyi, -							with_autocomplete => 1, -							with_geolocation  => 1 +							user_status        => $status, +							journey_visibility => $journey_visibility, +							connections        => $connecting_trains, +							transit_fyi        => $transit_fyi,  						);  						$self->users->mark_seen(  							uid => $self->current_user->{id} ); @@ -404,9 +415,8 @@ sub homepage {  							'landingpage',  							version => $self->app->config->{version}  							  // 'UNKNOWN', -							user_status       => $status, -							with_autocomplete => 1, -							with_geolocation  => 1 +							user_status        => $status, +							journey_visibility => $journey_visibility,  						);  						$self->users->mark_seen(  							uid => $self->current_user->{id} ); @@ -414,6 +424,16 @@ sub homepage {  				)->wait;  				return;  			} +			else { +				$self->render( +					'landingpage', +					version     => $self->app->config->{version} // 'UNKNOWN', +					user_status => $status, +					journey_visibility => $journey_visibility, +				); +				$self->users->mark_seen( uid => $self->current_user->{id} ); +				return; +			}  		}  		else {  			@recent_targets = uniq_by { $_->{eva} } @@ -439,6 +459,26 @@ sub homepage {  	}  } +sub status_token_ok { +	my ( $self, $status, $ts2_ext ) = @_; +	my $token = $self->param('token') // q{}; + +	my ( $eva, $ts, $ts2 ) = split( qr{-}, $token ); +	if ( not $ts ) { +		return; +	} + +	$ts2 //= $ts2_ext; + +	if (    $eva == $status->{dep_eva} +		and $ts == $status->{timestamp}->epoch +		and $ts2 == $status->{sched_departure}->epoch ) +	{ +		return 1; +	} +	return; +} +  sub user_status {  	my ($self) = @_; @@ -446,47 +486,33 @@ sub user_status {  	my $ts   = $self->stash('ts') // 0;  	my $user = $self->users->get_privacy_by_name( name => $name ); -	if ( not $user or not $user->{public_level} & 0x03 ) { +	if ( not $user ) {  		$self->render('not_found');  		return;  	} -	if ( $user->{public_level} & 0x01 and not $self->is_user_authenticated ) { -		$self->render( 'login', redirect_to => $self->req->url ); -		return; -	} -  	my $status = $self->get_user_status( $user->{id} ); -	my $journey;  	if (  		$ts  		and ( not $status->{checked_in}  			or $status->{sched_departure}->epoch != $ts ) -		and ( -			$user->{public_level} & 0x20 -			or (    $user->{public_level} & 0x10 -				and $self->is_user_authenticated ) -		)  	  )  	{  		for my $candidate (  			$self->journeys->get(  				uid   => $user->{id}, -				limit => 10, +				limit => 20,  			)  		  )  		{  			if ( $candidate->{sched_dep_ts} eq $ts ) { -				$journey = $self->journeys->get_single( -					uid           => $user->{id}, -					journey_id    => $candidate->{id}, -					verbose       => 1, -					with_datetime => 1, -					with_polyline => 1, -				); +				$self->redirect_to("/p/${name}/j/$candidate->{id}"); +				return;  			}  		} +		$self->render('not_found'); +		return;  	}  	my %tw_data = ( @@ -502,24 +528,30 @@ sub user_status {  		site_name => 'travelynx',  	); -	if ($journey) { -		$og_data{title} = $tw_data{title} = sprintf( 'Fahrt von %s nach %s', -			$journey->{from_name}, $journey->{to_name} ); -		$og_data{description} = $tw_data{description} -		  = $journey->{rt_arrival}->strftime('Ankunft am %d.%m.%Y um %H:%M'); -		$og_data{url} .= "/${ts}"; -	} -	elsif ( -		$ts -		and ( not $status->{checked_in} -			or $status->{sched_departure}->epoch != $ts ) -	  ) -	{ -		$og_data{title}       = $tw_data{title} = "Bahnfahrt beendet"; -		$og_data{description} = $tw_data{description} -		  = "${name} hat das Ziel erreicht"; +	my $visibility; +	if ( $status->{checked_in} ) { +		$visibility +		  = $self->compute_effective_visibility( +			$user->{default_visibility_str}, +			$status->{visibility_str} ); +		if ( +			not( +				$visibility eq 'public' +				or (    $visibility eq 'unlisted' +					and $self->status_token_ok( $status, $ts ) ) +				or ( +					$visibility eq 'travelynx' +					and (  $self->is_user_authenticated +						or $self->status_token_ok( $status, $ts ) ) +				) +			) +		  ) +		{ +			$status->{checked_in} = 0; +		}  	} -	elsif ( $status->{checked_in} ) { + +	if ( $status->{checked_in} ) {  		$og_data{url} .= '/' . $status->{sched_departure}->epoch;  		$og_data{title}       = $tw_data{title}       = "${name} ist unterwegs";  		$og_data{description} = $tw_data{description} = sprintf( @@ -537,39 +569,18 @@ sub user_status {  	else {  		$og_data{title} = $tw_data{title}  		  = "${name} ist gerade nicht eingecheckt"; -		$og_data{description} = $tw_data{description} -		  = "Letztes Fahrtziel: $status->{arr_name}"; +		$og_data{description} = $tw_data{description} = q{};  	} -	if ($journey) { -		if ( not $user->{public_level} & 0x04 ) { -			delete $journey->{user_data}{comment}; -		} -		my $map_data = $self->journeys_to_map_data( -			journeys       => [$journey], -			include_manual => 1, -		); -		$self->render( -			'journey', -			error     => undef, -			with_map  => 1, -			readonly  => 1, -			journey   => $journey, -			twitter   => \%tw_data, -			opengraph => \%og_data, -			%{$map_data}, -		); -	} -	else { -		$self->render( -			'user_status', -			name         => $name, -			public_level => $user->{public_level}, -			journey      => $status, -			twitter      => \%tw_data, -			opengraph    => \%og_data, -		); -	} +	$self->render( +		'user_status', +		name               => $name, +		public_level       => $user->{public_level}, +		journey            => $status, +		journey_visibility => $visibility, +		twitter            => \%tw_data, +		opengraph          => \%og_data, +	);  }  sub public_profile { @@ -578,48 +589,77 @@ sub public_profile {  	my $name = $self->stash('name');  	my $user = $self->users->get_privacy_by_name( name => $name ); +	if ( not $user ) { +		$self->render('not_found'); +	} + +	my $status = $self->get_user_status( $user->{id} ); +	my $visibility; +	if ( $status->{checked_in} ) { +		$visibility +		  = $self->compute_effective_visibility( +			$user->{default_visibility_str}, +			$status->{visibility_str} ); +		if ( +			not( +				$visibility eq 'public' +				or (    $visibility eq 'unlisted' +					and $self->status_token_ok($status) ) +				or ( +					$visibility eq 'travelynx' +					and (  $self->is_user_authenticated +						or $self->status_token_ok($status) ) +				) +			) +		  ) +		{ +			$status->{checked_in} = 0; +		} +	} + +	my %opt = ( +		uid           => $user->{id}, +		limit         => 10, +		with_datetime => 1 +	); + +	if ( not $user->{past_all} ) { +		my $now = DateTime->now( time_zone => 'Europe/Berlin' ); +		$opt{before} = DateTime->now( time_zone => 'Europe/Berlin' ); +		$opt{after}  = $now->clone->subtract( weeks => 4 ); +	} +  	if ( -		$user -		and ( -			$user->{public_level} & 0x22 -			or (    $user->{public_level} & 0x11 -				and $self->is_user_authenticated ) -		) +		$user->{default_visibility_str} eq 'public' +		or (    $user->{default_visibility_str} eq 'travelynx' +			and $self->is_user_authenticated )  	  )  	{ -		my $status = $self->get_user_status( $user->{id} ); -		my @journeys; -		if ( $user->{public_level} & 0x40 ) { -			@journeys = $self->journeys->get( -				uid           => $user->{id}, -				limit         => 10, -				with_datetime => 1 -			); -		} -		else { -			my $now       = DateTime->now( time_zone => 'Europe/Berlin' ); -			my $month_ago = $now->clone->subtract( weeks => 4 ); -			@journeys = $self->journeys->get( -				uid           => $user->{id}, -				limit         => 10, -				with_datetime => 1, -				after         => $month_ago, -				before        => $now -			); -		} -		$self->render( -			'profile', -			name         => $name, -			uid          => $user->{id}, -			public_level => $user->{public_level}, -			journey      => $status, -			journeys     => [@journeys], -			version      => $self->app->config->{version} // 'UNKNOWN', -		); +		$opt{with_default_visibility} = 1;  	}  	else { -		$self->render('not_found'); +		$opt{with_default_visibility} = 0;  	} + +	if ( $self->is_user_authenticated ) { +		$opt{min_visibility} = 'travelynx'; +	} +	else { +		$opt{min_visibility} = 'public'; +	} + +	my @journeys = $self->journeys->get(%opt); + +	$self->render( +		'profile', +		name               => $name, +		uid                => $user->{id}, +		public_level       => $user->{public_level}, +		journey            => $status, +		journey_visibility => $visibility, +		journeys           => [@journeys], +		version            => $self->app->config->{version} // 'UNKNOWN', +	);  }  sub public_journey_details { @@ -630,7 +670,26 @@ sub public_journey_details {  	$self->param( journey_id => $journey_id ); -	if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) { +	if ( not( $user and $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) { +		$self->render( +			'journey', +			status  => 404, +			error   => 'notfound', +			journey => {} +		); +		return; +	} + +	my $journey = $self->journeys->get_single( +		uid             => $user->{id}, +		journey_id      => $journey_id, +		verbose         => 1, +		with_datetime   => 1, +		with_polyline   => 1, +		with_visibility => 1, +	); + +	if ( not $journey ) {  		$self->render(  			'journey',  			status  => 404, @@ -640,98 +699,93 @@ sub public_journey_details {  		return;  	} +	my $visibility +	  = $self->compute_effective_visibility( $user->{default_visibility_str}, +		$journey->{visibility_str} ); +  	if ( -		$user -		and ( -			$user->{public_level} & 0x20 -			or (    $user->{public_level} & 0x10 -				and $self->is_user_authenticated ) +		not( +			$visibility eq 'public' +			or (    $visibility eq 'unlisted' +				and $self->status_token_ok($journey) ) +			or ( +				$visibility eq 'travelynx' +				and (  $self->is_user_authenticated +					or $self->status_token_ok($journey) ) +			)  		)  	  )  	{ -		my $journey = $self->journeys->get_single( -			uid           => $user->{id}, -			journey_id    => $journey_id, -			verbose       => 1, -			with_datetime => 1, -			with_polyline => 1, +		$self->render( +			'journey', +			status  => 404, +			error   => 'notfound', +			journey => {}  		); +		return; +	} -		if ( not( $user->{public_level} & 0x40 ) ) { -			my $month_ago = DateTime->now( time_zone => 'Europe/Berlin' ) -			  ->subtract( weeks => 4 )->epoch; -			if ( $journey and $journey->{rt_dep_ts} < $month_ago ) { -				$journey = undef; -			} -		} - -		if ($journey) { -			my $title = sprintf( 'Fahrt von %s nach %s am %s', -				$journey->{from_name}, $journey->{to_name}, -				$journey->{rt_arrival}->strftime('%d.%m.%Y') ); -			my $delay = 'pünktlich '; -			if ( $journey->{rt_arrival} != $journey->{sched_arrival} ) { -				$delay = sprintf( -					'mit %+d ', -					( -						    $journey->{rt_arrival}->epoch -						  - $journey->{sched_arrival}->epoch -					) / 60 -				); -			} -			my $description = sprintf( 'Ankunft mit %s %s %s', -				$journey->{type}, $journey->{no}, -				$journey->{rt_arrival}->strftime('um %H:%M') ); -			if ( $journey->{km_route} > 0.1 ) { -				$description = sprintf( '%.0f km mit %s %s – Ankunft %sum %s', -					$journey->{km_route}, $journey->{type}, $journey->{no}, -					$delay, $journey->{rt_arrival}->strftime('%H:%M') ); -			} -			my %tw_data = ( -				card  => 'summary', -				site  => '@derfnull', -				image => $self->url_for('/static/icons/icon-512x512.png') -				  ->to_abs->scheme('https'), -				title       => $title, -				description => $description, -			); -			my %og_data = ( -				type        => 'article', -				image       => $tw_data{image}, -				url         => $self->url_for->to_abs, -				site_name   => 'travelynx', -				title       => $title, -				description => $description, -			); +	# TODO re-add age check unless status_token_ok (helper function?) -			my $map_data = $self->journeys_to_map_data( -				journeys       => [$journey], -				include_manual => 1, -			); -			if ( $journey->{user_data}{comment} -				and not $user->{public_level} & 0x04 ) -			{ -				delete $journey->{user_data}{comment}; -			} -			$self->render( -				'journey', -				error     => undef, -				journey   => $journey, -				with_map  => 1, -				username  => $name, -				readonly  => 1, -				twitter   => \%tw_data, -				opengraph => \%og_data, -				%{$map_data}, -			); -		} -		else { -			$self->render('not_found'); -		} +	my $title = sprintf( 'Fahrt von %s nach %s am %s', +		$journey->{from_name}, $journey->{to_name}, +		$journey->{rt_arrival}->strftime('%d.%m.%Y') ); +	my $delay = 'pünktlich '; +	if ( $journey->{rt_arrival} != $journey->{sched_arrival} ) { +		$delay = sprintf( +			'mit %+d ', +			( +				    $journey->{rt_arrival}->epoch +				  - $journey->{sched_arrival}->epoch +			) / 60 +		);  	} -	else { -		$self->render('not_found'); +	my $description = sprintf( 'Ankunft mit %s %s %s', +		$journey->{type}, $journey->{no}, +		$journey->{rt_arrival}->strftime('um %H:%M') ); +	if ( $journey->{km_route} > 0.1 ) { +		$description = sprintf( '%.0f km mit %s %s – Ankunft %sum %s', +			$journey->{km_route}, $journey->{type}, $journey->{no}, +			$delay, $journey->{rt_arrival}->strftime('%H:%M') );  	} +	my %tw_data = ( +		card  => 'summary', +		site  => '@derfnull', +		image => $self->url_for('/static/icons/icon-512x512.png') +		  ->to_abs->scheme('https'), +		title       => $title, +		description => $description, +	); +	my %og_data = ( +		type        => 'article', +		image       => $tw_data{image}, +		url         => $self->url_for->to_abs, +		site_name   => 'travelynx', +		title       => $title, +		description => $description, +	); + +	my $map_data = $self->journeys_to_map_data( +		journeys       => [$journey], +		include_manual => 1, +	); +	if ( $journey->{user_data}{comment} +		and not $user->{public_level} & 0x04 ) +	{ +		delete $journey->{user_data}{comment}; +	} +	$self->render( +		'journey', +		error              => undef, +		journey            => $journey, +		with_map           => 1, +		username           => $name, +		readonly           => 1, +		twitter            => \%tw_data, +		opengraph          => \%og_data, +		journey_visibility => $visibility, +		%{$map_data}, +	);  }  sub public_status_card { @@ -743,26 +797,42 @@ sub public_status_card {  	delete $self->stash->{layout}; -	if ( -		$user -		and ( -			$user->{public_level} & 0x02 -			or (    $user->{public_level} & 0x01 -				and $self->is_user_authenticated ) -		) -	  ) -	{ -		my $status = $self->get_user_status( $user->{id} ); -		$self->render( -			'_public_status_card', -			name         => $name, -			public_level => $user->{public_level}, -			journey      => $status -		); -	} -	else { +	if ( not $user ) {  		$self->render('not_found'); +		return; +	} + +	my $status = $self->get_user_status( $user->{id} ); +	my $visibility; +	if ( $status->{checked_in} ) { +		$visibility +		  = $self->compute_effective_visibility( +			$user->{default_visibility_str}, +			$status->{visibility_str} ); +		if ( +			not( +				$visibility eq 'public' +				or (    $visibility eq 'unlisted' +					and $self->status_token_ok($status) ) +				or ( +					$visibility eq 'travelynx' +					and (  $self->is_user_authenticated +						or $self->status_token_ok($status) ) +				) +			) +		  ) +		{ +			$status->{checked_in} = 0; +		}  	} + +	$self->render( +		'_public_status_card', +		name               => $name, +		public_level       => $user->{public_level}, +		journey            => $status, +		journey_visibility => $visibility, +	);  }  sub status_card { @@ -772,6 +842,10 @@ sub status_card {  	delete $self->stash->{layout};  	if ( $status->{checked_in} ) { +		my $journey_visibility +		  = $self->compute_effective_visibility( +			$self->current_user->{default_visibility_str}, +			$status->{visibility_str} );  		if ( defined $status->{arrival_countdown}  			and $status->{arrival_countdown} < ( 40 * 60 ) )  		{ @@ -781,19 +855,28 @@ sub status_card {  					my ( $connecting_trains, $transit_fyi ) = @_;  					$self->render(  						'_checked_in', -						journey     => $status, -						connections => $connecting_trains, -						transit_fyi => $transit_fyi +						journey            => $status, +						journey_visibility => $journey_visibility, +						connections        => $connecting_trains, +						transit_fyi        => $transit_fyi  					);  				}  			)->catch(  				sub { -					$self->render( '_checked_in', journey => $status ); +					$self->render( +						'_checked_in', +						journey            => $status, +						journey_visibility => $journey_visibility, +					);  				}  			)->wait;  			return;  		} -		$self->render( '_checked_in', journey => $status ); +		$self->render( +			'_checked_in', +			journey            => $status, +			journey_visibility => $journey_visibility, +		);  	}  	elsif ( $status->{cancellation} ) {  		$self->render_later; @@ -1693,11 +1776,12 @@ sub journey_details {  	}  	my $journey = $self->journeys->get_single( -		uid           => $uid, -		journey_id    => $journey_id, -		verbose       => 1, -		with_datetime => 1, -		with_polyline => 1, +		uid             => $uid, +		journey_id      => $journey_id, +		verbose         => 1, +		with_datetime   => 1, +		with_polyline   => 1, +		with_visibility => 1,  	);  	if ($journey) { @@ -1705,15 +1789,18 @@ sub journey_details {  			journeys       => [$journey],  			include_manual => 1,  		); +		my $with_share;  		my $share_text; -		my $with_share = $user->{is_public} & 0x40 ? 1 : 0; -		if ( not $with_share and $user->{is_public} & 0x20 ) { -			my $month_ago = DateTime->now( time_zone => 'Europe/Berlin' ) -			  ->subtract( weeks => 4 )->epoch; -			$with_share = $journey->{rt_dep_ts} > $month_ago ? 1 : 0; -		} -		if ($with_share) { +		my $visibility +		  = $self->compute_effective_visibility( +			$user->{default_visibility_str}, +			$journey->{visibility_str} ); + +		if (   $visibility eq 'public' +			or $visibility eq 'travelynx' +			or $visibility eq 'unlisted' ) +		{  			my $delay = 'pünktlich ';  			if ( $journey->{rt_arrival} != $journey->{sched_arrival} ) {  				$delay = sprintf( @@ -1724,6 +1811,7 @@ sub journey_details {  					) / 60  				);  			} +			$with_share = 1;  			$share_text  			  = $journey->{km_route}  			  ? sprintf( '%.0f km', $journey->{km_route} ) @@ -1733,13 +1821,15 @@ sub journey_details {  				$delay,           $journey->{rt_arrival}->strftime('%H:%M') );  		} +		# TODO add token if visibility != public  		$self->render(  			'journey', -			error      => undef, -			journey    => $journey, -			with_map   => 1, -			with_share => $with_share, -			share_text => $share_text, +			error              => undef, +			journey            => $journey, +			journey_visibility => $visibility, +			with_map           => 1, +			with_share         => $with_share, +			share_text         => $share_text,  			%{$map_data},  		);  	} @@ -1754,6 +1844,112 @@ sub journey_details {  } +sub visibility_form { +	my ($self)     = @_; +	my $dep_ts     = $self->param('dep_ts'); +	my $journey_id = $self->param('id'); +	my $action     = $self->param('action') // 'none'; +	my $user       = $self->current_user; +	my $user_level = $user->{default_visibility_str}; +	my $uid        = $user->{id}; +	my $status     = $self->get_user_status; +	my $visibility = $status->{visibility}; +	my $journey; + +	if ($journey_id) { +		$journey = $self->journeys->get_single( +			uid             => $uid, +			journey_id      => $journey_id, +			with_datetime   => 1, +			with_visibility => 1, +		); +		$visibility = $journey->{visibility}; +	} + +	if ( $action eq 'save' ) { +		if ( $self->validation->csrf_protect->has_error('csrf_token') ) { +			$self->render( +				'edit_visibility', +				error      => 'csrf', +				user_level => $user_level, +				journey    => {} +			); +		} +		elsif ( $dep_ts and $dep_ts != $status->{sched_departure}->epoch ) { + +			# TODO find and update appropriate past journey (if it exists) +			$self->render( +				'edit_visibility', +				error      => 'old', +				user_level => $user_level, +				journey    => {} +			); +		} +		else { +			$self->app->log->debug("set visibility"); +			if ($dep_ts) { +				$self->in_transit->update_visibility( +					uid        => $uid, +					visibility => $self->param('status_level'), +				); +				$self->redirect_to('/'); +			} +			elsif ($journey_id) { +				$self->journeys->update_visibility( +					uid        => $uid, +					id         => $journey_id, +					visibility => $self->param('status_level'), +				); +				$self->redirect_to( '/journey/' . $journey_id ); +			} +		} +		return; +	} + +	# todo use visibility_str +	if ( not defined $visibility ) { +		$self->param( status_level => 'default' ); +	} +	elsif ( $visibility == 100 ) { +		$self->param( status_level => 'public' ); +	} +	elsif ( $visibility == 80 ) { +		$self->param( status_level => 'travelynx' ); +	} +	elsif ( $visibility == 30 ) { +		$self->param( status_level => 'unlisted' ); +	} +	elsif ( $visibility == 10 ) { +		$self->param( status_level => 'private' ); +	} + +	if ($journey_id) { +		$self->render( +			'edit_visibility', +			error      => undef, +			user_level => $user_level, +			journey    => $journey +		); +	} +	elsif ( $status->{checked_in} ) { +		$self->param( dep_ts => $status->{sched_departure}->epoch ); +		$self->render( +			'edit_visibility', +			error      => undef, +			user_level => $user_level, +			journey    => $status +		); +	} +	else { +		$self->render( +			'edit_visibility', +			error      => 'notfound', +			user_level => $user_level, +			journey    => {} +		); +	} +} +  sub comment_form {  	my ($self) = @_;  	my $dep_ts = $self->param('dep_ts'); diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm index d3b6d6b..78fd297 100644 --- a/lib/Travelynx/Model/InTransit.pm +++ b/lib/Travelynx/Model/InTransit.pm @@ -11,6 +11,22 @@ use 5.020;  use DateTime;  use JSON; +my %visibility_itoa = ( +	100 => 'public', +	80  => 'travelynx', +	60  => 'followers', +	30  => 'unlisted', +	10  => 'private', +); + +my %visibility_atoi = ( +	public    => 100, +	travelynx => 80, +	followers => 60, +	unlisted  => 30, +	private   => 10, +); +  sub new {  	my ( $class, %opt ) = @_; @@ -117,11 +133,23 @@ sub get {  	}  	my $res = $db->select( $table, '*', { user_id => $uid } ); +	my $ret;  	if ( $opt{with_data} ) { -		return $res->expand->hash; +		$ret = $res->expand->hash; +	} +	else { +		$ret = $res->hash;  	} -	return $res->hash; + +	if ( $opt{with_visibility} and $ret ) { +		$ret->{visibility_str} +		  = $ret->{visibility} +		  ? $visibility_itoa{ $ret->{visibility} } +		  : 'default'; +	} + +	return $ret;  }  sub get_all_active { @@ -449,4 +477,23 @@ sub update_user_data {  	);  } +sub update_visibility { +	my ( $self, %opt ) = @_; + +	my $uid = $opt{uid}; +	my $db  = $opt{db} // $self->{pg}->db; + +	my $visibility; + +	if ( $opt{visibility} and $visibility_atoi{ $opt{visibility} } ) { +		$visibility = $visibility_atoi{ $opt{visibility} }; +	} + +	$db->update( +		'in_transit', +		{ visibility => $visibility }, +		{ user_id    => $uid } +	); +} +  1; diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index 08d9ff0..a9c6200 100755 --- a/lib/Travelynx/Model/Journeys.pm +++ b/lib/Travelynx/Model/Journeys.pm @@ -15,6 +15,22 @@ use utf8;  use DateTime;  use JSON; +my %visibility_itoa = ( +	100 => 'public', +	80  => 'travelynx', +	60  => 'followers', +	30  => 'unlisted', +	10  => 'private', +); + +my %visibility_atoi = ( +	public    => 100, +	travelynx => 80, +	followers => 60, +	unlisted  => 30, +	private   => 10, +); +  my @month_name    = (  	qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember) @@ -509,7 +525,7 @@ sub get {  	my @select  	  = ( -		qw(journey_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data) +		qw(journey_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility)  	  );  	my %where = (  		user_id   => $uid, @@ -548,6 +564,24 @@ sub get {  		push( @select, 'polyline' );  	} +	if ( $opt{min_visibility} ) { +		if ( $visibility_atoi{ $opt{min_visibility} } ) { +			$opt{min_visibility} = $visibility_atoi{ $opt{min_visibility} }; +		} +		if ( $opt{with_default_visibility} ) { +			$where{visibility} = [ +				-or => { '=', undef }, +				{ '>=', $opt{min_visibility} } +			]; +		} +		else { +			$where{visibility} = [ +				-and => { '!=', undef }, +				{ '>=', $opt{min_visibility} } +			]; +		} +	} +  	my @travels;  	my $res = $db->select( 'journeys_str', \@select, \%where, \%order ); @@ -577,8 +611,16 @@ sub get {  			route        => $entry->{route},  			edited       => $entry->{edited},  			user_data    => $entry->{user_data}, +			visibility   => $entry->{visibility},  		}; +		if ( $opt{with_visibility} ) { +			$ref->{visibility_str} +			  = $ref->{visibility} +			  ? $visibility_itoa{ $ref->{visibility} } +			  : 'default'; +		} +  		if ( $opt{with_polyline} ) {  			$ref->{polyline} = $entry->{polyline};  		} @@ -1703,4 +1745,26 @@ sub get_connection_targets {  	return @destinations;  } +sub update_visibility { +	my ( $self, %opt ) = @_; + +	my $uid = $opt{uid}; +	my $db  = $opt{db} // $self->{pg}->db; + +	my $visibility; + +	if ( $opt{visibility} and $visibility_atoi{ $opt{visibility} } ) { +		$visibility = $visibility_atoi{ $opt{visibility} }; +	} + +	$db->update( +		'journeys', +		{ visibility => $visibility }, +		{ +			user_id => $uid, +			id      => $opt{id} +		} +	); +} +  1; diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 8f5ce1f..63cc261 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -11,6 +11,22 @@ use 5.020;  use DateTime;  use JSON; +my %visibility_itoa = ( +	100 => 'public', +	80  => 'travelynx', +	60  => 'followers', +	30  => 'unlisted', +	10  => 'private', +); + +my %visibility_atoi = ( +	public    => 100, +	travelynx => 80, +	followers => 60, +	unlisted  => 30, +	private   => 10, +); +  my @sb_templates = (  	undef,  	[ 'DBF',         'https://dbf.finalrewind.org/{name}?rt=1#{tt}{tn}' ], @@ -153,7 +169,16 @@ sub get_privacy_by_name {  	);  	if ( my $user = $res->hash ) { -		return $user; +		return { +			id                 => $user->{id}, +			public_level       => $user->{public_level},          # todo remove? +			default_visibility => $user->{public_level} & 0x7f, +			default_visibility_str => +			  $visibility_itoa{ $user->{public_level} & 0x7f }, +			comments_visible => $user->{public_level} & 0x80 ? 1 : 0, +			past_visible     => ( $user->{public_level} & 0x300 ) >> 8, +			past_all         => $user->{public_level} & 0x400 ? 1 : 0, +		};  	}  	return;  } @@ -164,6 +189,14 @@ sub set_privacy {  	my $uid          = $opt{uid};  	my $public_level = $opt{level}; +	if ( not defined $public_level and defined $opt{default_visibility} ) { +		$public_level +		  = ( $opt{default_visibility} & 0x7f ) +		  | ( $opt{comments_visible} ? 0x80 : 0x00 ) +		  | ( ( ( $opt{past_visible} // 0 ) << 8 ) & 0x300 ) +		  | ( $opt{past_all} ? 0x400 : 0 ); +	} +  	$db->update( 'users', { public_level => $public_level }, { id => $uid } );  } @@ -333,12 +366,18 @@ sub get {  	)->hash;  	if ($user) {  		return { -			id        => $user->{id}, -			name      => $user->{name}, -			status    => $user->{status}, -			is_public => $user->{public_level}, -			email     => $user->{email}, -			sb_name   => $user->{external_services} +			id                     => $user->{id}, +			name                   => $user->{name}, +			status                 => $user->{status}, +			is_public              => $user->{public_level}, +			default_visibility     => $user->{public_level} & 0x7f, +			default_visibility_str => +			  $visibility_itoa{ $user->{public_level} & 0x7f }, +			comments_visible => $user->{public_level} & 0x80 ? 1 : 0, +			past_visible     => ( $user->{public_level} & 0x300 ) >> 8, +			past_all         => $user->{public_level} & 0x400 ? 1 : 0, +			email            => $user->{email}, +			sb_name          => $user->{external_services}  			? $sb_templates[ $user->{external_services} & 0x07 ][0]  			: undef,  			sb_template => $user->{external_services} | 
