diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index adb6132..6dd77fd 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -399,6 +399,9 @@ sub startup { if ( $visibility eq 'travelynx' ) { return 'lock_open'; } + if ( $visibility eq 'followers' ) { + return 'group'; + } if ( $visibility eq 'unlisted' ) { return 'lock_outline'; } @@ -2213,6 +2216,8 @@ sub startup { $authed_r->get('/account')->to('account#account'); $authed_r->get('/account/privacy')->to('account#privacy'); + $authed_r->get('/account/social')->to('account#social'); + $authed_r->get('/account/social/:kind')->to('account#social_list'); $authed_r->get('/account/profile')->to('account#profile'); $authed_r->get('/account/hooks')->to('account#webhook'); $authed_r->get('/account/traewelling')->to('traewelling#settings'); @@ -2240,6 +2245,7 @@ sub startup { $authed_r->get('/s/*station')->to('traveling#station'); $authed_r->get('/confirm_mail/:token')->to('account#confirm_mail'); $authed_r->post('/account/privacy')->to('account#privacy'); + $authed_r->post('/account/social')->to('account#social'); $authed_r->post('/account/profile')->to('account#profile'); $authed_r->post('/account/hooks')->to('account#webhook'); $authed_r->post('/account/traewelling')->to('traewelling#settings'); @@ -2254,6 +2260,7 @@ sub startup { $authed_r->post('/account/password')->to('account#change_password'); $authed_r->post('/account/mail')->to('account#change_mail'); $authed_r->post('/account/name')->to('account#change_name'); + $authed_r->post('/social-action')->to('account#social_action'); $authed_r->post('/delete')->to('account#delete'); $authed_r->post('/logout')->to('account#do_logout'); $authed_r->post('/set_token')->to('api#set_token'); diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index 53168bf..3878502 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -1545,6 +1545,27 @@ my @migrations = ( } ); }, + + # v37 -> v38 + sub { + my ($db) = @_; + $db->query( + qq{ + drop view followers; + create view followers as select + relations.object_id as self_id, + users.id as id, + users.name as name, + users.accept_follows as accept_follows, + r2.predicate as inverse_predicate + from relations + join users on relations.subject_id = users.id + left join relations as r2 on relations.subject_id = r2.object_id + where relations.predicate = 1; + update schema_version set version = 38; + } + ); + }, ); # TODO add 'hafas' column to in_transit (and maybe journeys? undo/redo needs something to work with...) diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index af97c96..fe5d5cc 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -408,11 +408,8 @@ sub delete { my ($self) = @_; my $uid = $self->current_user->{id}; if ( $self->validation->csrf_protect->has_error('csrf_token') ) { - $self->render( - 'account', - api_token => $self->users->get_api_token( uid => $uid ), - invalid => 'csrf', - ); + $self->flash( invalid => 'csrf' ); + $self->redirect_to('account'); return; } @@ -424,11 +421,8 @@ sub delete { ) ) { - $self->render( - 'account', - api_token => $self->users->get_api_token( uid => $uid ), - invalid => 'deletion password' - ); + $self->flash( invalid => 'deletion password' ); + $self->redirect_to('account'); return; } $self->users->flag_deletion( uid => $uid ); @@ -501,6 +495,228 @@ sub privacy { } } +sub social { + my ($self) = @_; + + my $user = $self->current_user; + + if ( $self->param('action') and $self->param('action') eq 'save' ) { + if ( $self->validation->csrf_protect->has_error('csrf_token') ) { + $self->render( + 'social', + invalid => 'csrf', + ); + return; + } + + my %opt; + my $accept_follow = $self->param('accept_follow'); + + if ( $accept_follow eq 'yes' ) { + $opt{accept_follows} = 1; + } + elsif ( $accept_follow eq 'request' ) { + $opt{accept_follow_requests} = 1; + } + + $self->users->set_social( + uid => $user->{id}, + %opt + ); + + $self->flash( success => 'social' ); + $self->redirect_to('account'); + } + else { + if ( $user->{accept_follows} ) { + $self->param( accept_follow => 'yes' ); + } + elsif ( $user->{accept_follow_requests} ) { + $self->param( accept_follow => 'request' ); + } + else { + $self->param( accept_follow => 'no' ); + } + $self->render( 'social', name => $user->{name} ); + } +} + +sub social_list { + my ($self) = @_; + + my $kind = $self->stash('kind'); + my $user = $self->current_user; + + if ( $kind eq 'follow-requests' ) { + my @follow_reqs + = $self->users->get_follow_requests( uid => $user->{id} ); + $self->render( + 'social_list', + type => 'follow-requests', + entries => [@follow_reqs], + notifications => $user->{notifications}, + ); + } + elsif ( $kind eq 'followers' ) { + my @followers = $self->users->get_followers( uid => $user->{id} ); + $self->render( + 'social_list', + type => 'followers', + entries => [@followers], + notifications => $user->{notifications}, + ); + } + elsif ( $kind eq 'follows' ) { + my @following = $self->users->get_followees( uid => $user->{id} ); + $self->render( + 'social_list', + type => 'follows', + entries => [@following], + notifications => $user->{notifications}, + ); + } + elsif ( $kind eq 'blocks' ) { + my @blocked = $self->users->get_blocked_users( uid => $user->{id} ); + $self->render( + 'social_list', + type => 'blocks', + entries => [@blocked], + notifications => $user->{notifications}, + ); + } + else { + $self->render( 'not_found', status => 404 ); + } +} + +sub social_action { + my ($self) = @_; + + my $user = $self->current_user; + my $action = $self->param('action'); + my $target_ids = $self->param('target'); + my $redirect_to = $self->param('redirect_to'); + + for my $key ( + qw(follow request_follow follow_or_request unfollow remove_follower cancel_follow_request accept_follow_request reject_follow_request block unblock) + ) + { + if ( $self->param($key) ) { + $action = $key; + $target_ids = $self->param($key); + } + } + + if ( $self->validation->csrf_protect->has_error('csrf_token') ) { + $self->redirect_to('/'); + return; + } + + if ( $action and $action eq 'clear_notifications' ) { + $self->users->update_notifications( + db => $self->pg->db, + uid => $user->{id}, + has_follow_requests => 0 + ); + $self->flash( success => 'clear_notifications' ); + $self->redirect_to('account'); + return; + } + + if ( not( $action and $target_ids and $redirect_to ) ) { + $self->redirect_to('/'); + return; + } + + for my $target_id ( split( qr{,}, $target_ids ) ) { + my $target = $self->users->get_privacy_by( uid => $target_id ); + + if ( not $target ) { + next; + } + + if ( $action eq 'follow' and $target->{accept_follows} ) { + $self->users->follow( + uid => $user->{id}, + target => $target->{id} + ); + } + elsif ( $action eq 'request_follow' + and $target->{accept_follow_requests} ) + { + $self->users->request_follow( + uid => $user->{id}, + target => $target->{id} + ); + } + elsif ( $action eq 'follow_or_request' ) { + if ( $target->{accept_follows} ) { + $self->users->follow( + uid => $user->{id}, + target => $target->{id} + ); + } + elsif ( $target->{accept_follow_requests} ) { + $self->users->request_follow( + uid => $user->{id}, + target => $target->{id} + ); + } + } + elsif ( $action eq 'unfollow' ) { + $self->users->unfollow( + uid => $user->{id}, + target => $target->{id} + ); + } + elsif ( $action eq 'remove_follower' ) { + $self->users->remove_follower( + uid => $user->{id}, + follower => $target->{id} + ); + } + elsif ( $action eq 'cancel_follow_request' ) { + $self->users->cancel_follow_request( + uid => $user->{id}, + target => $target->{id} + ); + } + elsif ( $action eq 'accept_follow_request' ) { + $self->users->accept_follow_request( + uid => $user->{id}, + applicant => $target->{id} + ); + } + elsif ( $action eq 'reject_follow_request' ) { + $self->users->reject_follow_request( + uid => $user->{id}, + applicant => $target->{id} + ); + } + elsif ( $action eq 'block' ) { + $self->users->block( + uid => $user->{id}, + target => $target->{id} + ); + } + elsif ( $action eq 'unblock' ) { + $self->users->unblock( + uid => $user->{id}, + target => $target->{id} + ); + } + + if ( $redirect_to eq 'profile' ) { + + # profile links do not perform bulk actions + $self->redirect_to( '/p/' . $target->{name} ); + return; + } + } + + $self->redirect_to($redirect_to); +} + sub profile { my ($self) = @_; my $user = $self->current_user; @@ -1012,11 +1228,21 @@ sub confirm_mail { } sub account { - my ($self) = @_; - my $uid = $self->current_user->{id}; + my ($self) = @_; + my $uid = $self->current_user->{id}; + my $follow_requests = $self->users->has_follow_requests( uid => $uid ); + my $followers = $self->users->has_followers( uid => $uid ); + my $following = $self->users->has_followees( uid => $uid ); + my $blocked = $self->users->has_blocked_users( uid => $uid ); - $self->render( 'account', - api_token => $self->users->get_api_token( uid => $uid ) ); + $self->render( + 'account', + api_token => $self->users->get_api_token( uid => $uid ), + num_follow_requests => $follow_requests, + num_followers => $followers, + num_following => $following, + num_blocked => $blocked, + ); $self->users->mark_seen( uid => $uid ); } diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm index 86b8922..b11dd71 100755 --- a/lib/Travelynx/Controller/Profile.pm +++ b/lib/Travelynx/Controller/Profile.pm @@ -186,6 +186,23 @@ sub profile { metadata => $profile->{metadata}, public_level => $user->{public_level}, is_self => $is_self, + following => ( $relation and $relation eq 'follows' ) ? 1 : 0, + follow_requested => ( $relation and $relation eq 'requests_follow' ) + ? 1 + : 0, + can_follow => ( $my_user and $user->{accept_follows} and not $relation ) + ? 1 + : 0, + can_request_follow => + ( $my_user and $user->{accept_follow_requests} and not $relation ) + ? 1 + : 0, + follows_me => ( $inverse_relation and $inverse_relation eq 'follows' ) + ? 1 + : 0, + follow_reqs_me => + ( $inverse_relation and $inverse_relation eq 'requests_follow' ) ? 1 + : 0, journey => $status, journey_visibility => $visibility, journeys => [@journeys], @@ -201,6 +218,24 @@ sub journey_details { $self->param( journey_id => $journey_id ); + my $my_user; + my $relation; + my $inverse_relation; + my $is_self; + if ( $self->is_user_authenticated ) { + $my_user = $self->current_user; + if ( $my_user->{id} == $user->{id} ) { + $is_self = 1; + $my_user = undef; + } + else { + $relation = $self->users->get_relation( + subject => $my_user->{id}, + object => $user->{id} + ); + } + } + if ( not( $user and $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) { $self->render( 'journey', @@ -249,7 +284,12 @@ sub journey_details { and $self->journey_token_ok($journey) ) or ( $visibility eq 'travelynx' - and ( ( $self->is_user_authenticated and not $is_past ) + and ( ( $my_user and not $is_past ) + or $self->journey_token_ok($journey) ) + ) + or ( + $visibility eq 'followers' + and ( ( $relation and $relation eq 'follows' ) or $self->journey_token_ok($journey) ) ) ) @@ -337,6 +377,24 @@ sub user_status { return; } + my $my_user; + my $relation; + my $inverse_relation; + my $is_self; + if ( $self->is_user_authenticated ) { + $my_user = $self->current_user; + if ( $my_user->{id} == $user->{id} ) { + $is_self = 1; + $my_user = undef; + } + else { + $relation = $self->users->get_relation( + subject => $my_user->{id}, + object => $user->{id} + ); + } + } + my $status = $self->get_user_status( $user->{id} ); if ( @@ -364,7 +422,12 @@ sub user_status { and $self->journey_token_ok( $journey, $ts ) ) or ( $visibility eq 'travelynx' - and ( $self->is_user_authenticated + and ( $my_user + or $self->journey_token_ok( $journey, $ts ) ) + ) + or ( + $visibility eq 'followers' + and ( ( $relation and $relation eq 'follows' ) or $self->journey_token_ok( $journey, $ts ) ) ) ) @@ -408,7 +471,12 @@ sub user_status { and $self->status_token_ok( $status, $ts ) ) or ( $visibility eq 'travelynx' - and ( $self->is_user_authenticated + and ( $my_user + or $self->status_token_ok( $status, $ts ) ) + ) + or ( + $visibility eq 'followers' + and ( ( $relation and $relation eq 'follows' ) or $self->status_token_ok( $status, $ts ) ) ) ) @@ -486,6 +554,24 @@ sub status_card { return; } + my $my_user; + my $relation; + my $inverse_relation; + my $is_self; + if ( $self->is_user_authenticated ) { + $my_user = $self->current_user; + if ( $my_user->{id} == $user->{id} ) { + $is_self = 1; + $my_user = undef; + } + else { + $relation = $self->users->get_relation( + subject => $my_user->{id}, + object => $user->{id} + ); + } + } + my $status = $self->get_user_status( $user->{id} ); my $visibility; if ( $status->{checked_in} or $status->{arr_name} ) { @@ -500,7 +586,12 @@ sub status_card { and $self->status_token_ok($status) ) or ( $visibility eq 'travelynx' - and ( $self->is_user_authenticated + and ( $my_user + or $self->status_token_ok($status) ) + ) + or ( + $visibility eq 'followers' + and ( ( $relation and $relation eq 'follows' ) or $self->status_token_ok($status) ) ) ) @@ -523,6 +614,7 @@ sub status_card { public_level => $user->{public_level}, journey => $status, journey_visibility => $visibility, + from_profile => $self->param('profile') ? 1 : 0, ); } diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 27005e8..5483e00 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -1459,6 +1459,7 @@ sub journey_details { if ( $visibility eq 'public' or $visibility eq 'travelynx' + or $visibility eq 'followers' or $visibility eq 'unlisted' ) { my $delay = 'pünktlich '; diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 7326e34..ade9711 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -180,13 +180,14 @@ sub get_privacy_by { my $res = $db->select( 'users', - [ 'id', 'public_level', 'accept_follows' ], + [ 'id', 'name', 'public_level', 'accept_follows' ], { %where, status => 1 } ); if ( my $user = $res->hash ) { return { id => $user->{id}, + name => $user->{name}, public_level => $user->{public_level}, # todo remove? default_visibility => $user->{public_level} & 0x7f, default_visibility_str => @@ -777,16 +778,16 @@ sub get_profile { sub get_relation { my ( $self, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; - my $uid = $opt{uid}; - my $target = $opt{target}; + my $db = $opt{db} // $self->{pg}->db; + my $subject = $opt{subject}; + my $object = $opt{object}; my $res_h = $db->select( 'relations', ['predicate'], { - subject_id => $uid, - object_id => $target + subject_id => $subject, + object_id => $object, } )->hash; @@ -824,6 +825,24 @@ sub update_notifications { $db->update( 'users', { notifications => $notifications }, { id => $uid } ); } +sub follow { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + my $target = $opt{target}; + + $db->insert( + 'relations', + { + subject_id => $uid, + predicate => $predicate_atoi{follows}, + object_id => $target, + ts => DateTime->now( time_zone => 'Europe/Berlin' ), + } + ); +} + sub request_follow { my ( $self, %opt ) = @_; @@ -920,6 +939,16 @@ sub reject_follow_request { } } +sub cancel_follow_request { + my ( $self, %opt ) = @_; + + $self->reject_follow_request( + db => $opt{db}, + uid => $opt{target}, + applicant => $opt{uid}, + ); +} + sub unfollow { my ( $self, %opt ) = @_; @@ -1005,9 +1034,50 @@ sub get_followers { my $db = $opt{db} // $self->{pg}->db; my $uid = $opt{uid}; - my $res = $db->select( 'followers', [ 'id', 'name' ], { self_id => $uid } ); + my $res = $db->select( + 'followers', + [ 'id', 'name', 'accept_follows', 'inverse_predicate' ], + { self_id => $uid } + ); - return $res->hashes->each; + my @ret; + while ( my $row = $res->hash ) { + push( + @ret, + { + id => $row->{id}, + name => $row->{name}, + following_back => ( + $row->{inverse_predicate} + and $row->{inverse_predicate} == $predicate_atoi{follows} + ) ? 1 : 0, + followback_requested => ( + $row->{inverse_predicate} + and $row->{inverse_predicate} + == $predicate_atoi{requests_follow} + ) ? 1 : 0, + can_follow_back => ( + not $row->{inverse_predicate} + and $row->{accept_follows} == 2 + ) ? 1 : 0, + can_request_follow_back => ( + not $row->{inverse_predicate} + and $row->{accept_follows} == 1 + ) ? 1 : 0, + } + ); + } + return @ret; +} + +sub has_followers { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + + return $db->select( 'followers', 'count(*) as count', { self_id => $uid } ) + ->hash->{count}; } sub get_follow_requests { @@ -1043,6 +1113,16 @@ sub get_followees { return $res->hashes->each; } +sub has_followees { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + + return $db->select( 'followees', 'count(*) as count', { self_id => $uid } ) + ->hash->{count}; +} + sub get_blocked_users { my ( $self, %opt ) = @_; @@ -1055,4 +1135,14 @@ sub get_blocked_users { return $res->hashes->each; } +sub has_blocked_users { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + + return $db->select( 'blocked_users', 'count(*) as count', + { self_id => $uid } )->hash->{count}; +} + 1; diff --git a/templates/_checked_in.html.ep b/templates/_checked_in.html.ep index 2d39842..1002654 100644 --- a/templates/_checked_in.html.ep +++ b/templates/_checked_in.html.ep @@ -3,8 +3,13 @@
<%= $journey->{comment} %>
diff --git a/templates/_format_train.html.ep b/templates/_format_train.html.ep new file mode 100644 index 0000000..5b61682 --- /dev/null +++ b/templates/_format_train.html.ep @@ -0,0 +1,9 @@ +% if ($journey->{extra_data}{wagonorder_pride}) { + 🏳️🌈 +% } +% if ($journey->{train_line}) { + <%= $journey->{train_type} %> <%= $journey->{train_line} %> <%= $journey->{train_no} %> +% } +% else { + <%= $journey->{train_type} %> <%= $journey->{train_no} %> +% } diff --git a/templates/_public_status_card.html.ep b/templates/_public_status_card.html.ep index 94ebf44..8e3eec7 100644 --- a/templates/_public_status_card.html.ep +++ b/templates/_public_status_card.html.ep @@ -1,28 +1,28 @@ -„<%= $journey->{comment} %>“
% }-
% if ($journey->{arr_name}) { Zuletzt gesehen diff --git a/templates/account.html.ep b/templates/account.html.ep index ef6b847..bb03c7b 100644 --- a/templates/account.html.ep +++ b/templates/account.html.ep @@ -1,4 +1,4 @@ -% if (my $invalid = stash('invalid')) { +% if (my $invalid = flash('invalid')) { %= include '_invalid_input', invalid => $invalid % } @@ -19,6 +19,9 @@ % elsif ($success eq 'privacy') { Einstellungen zu öffentlichen Account-Daten geändert % } + % elsif ($success eq 'social') { + Einstellungen zur Interaktionen mit anderen Accounts geändert + % } % elsif ($success eq 'traewelling') { Träwelling-Verknüpfung aktualisiert % } @@ -31,6 +34,9 @@ % elsif ($success eq 'webhook') { Web Hook aktualisiert % } + % elsif ($success eq 'clear_notifications') { + Benachrichtigungen gelesen + % }
Anfragen | ++ % if ($num_follow_requests == 0) { + keine offen + % } + % elsif ($num_follow_requests == 1) { + ein Account + % } + % else { + <%= $num_follow_requests %> Accounts + % } + | +
---|---|
Dir folg<%= $num_followers == 1 ? 't' : 'en' %> | ++ % if ($num_followers == 0) { + keine Accounts + % } + % elsif ($num_followers == 1) { + ein Account + % } + % else { + <%= $num_followers %> Accounts + % } + | +
Du folgst | ++ % if ($num_following == 0) { + keinen Accounts + % } + % elsif ($num_following == 1) { + einem Account + % } + % else { + <%= $num_following %> Accounts + % } + | +
Blockiert | ++ % if ($num_blocked == 0) { + keine Accounts + % } + % elsif ($num_blocked == 1) { + ein Account + % } + % else { + <%= $num_blocked %> Accounts + % } + | +
<%= $entry->{key} %> | +<%== $entry->{value}{html} %> | +
---|
+ Accounts die dir folgen können alle Checkins sehen, die nicht als privat oder nur mit Link zugänglich vermerkt sind. + Später werden sie zusätzlich die Möglichkeit haben, deinen aktuellen Checkin (sofern sichtbar) als Teil einer Übersicht über die Checkins aller gefolgten Accounts direkt anzusehen (analog zur Timeline im Fediverse). +
++ Du hast jederzeit die Möglichkeit, Accounts aus deiner Followerliste zu entfernen, Folge-Anfragen abzulehnen oder Accounts zu blockieren, so dass sie dir weder folgen noch neue Folge-Anfragen stellen können. +
++ Blockierte Accounts können dir nicht folgen und keine Folge-Anfragen stellen. + Sie haben weiterhin Zugriff auf deine als öffentlich oder travelynx-intern markierten Checkins. +
+<%= $entry->{name} %> | + % if ($type eq 'follow-requests') { ++ + | ++ + | ++ + | + % } + % elsif ($type eq 'followers') { ++ + | ++ + | ++ % if ($entry->{following_back}) { + group + % } + % elsif ($entry->{followback_requested}) { + access_time + % } + % elsif ($entry->{can_follow_back} or $entry->{can_request_follow_back}) { + + % } + | + % } + % elsif ($type eq 'follows') { ++ + | + % } + % elsif ($type eq 'blocks') { ++ + | + % } +