summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorDerf Null <derf@finalrewind.org>2023-06-04 18:21:36 +0200
committerDerf Null <derf@finalrewind.org>2023-06-04 18:21:36 +0200
commit00eb6af1bd21df42fc41195ceed0fad73bbb5f27 (patch)
treeaea171b00a79681e403292b0ad06b01e5d9a21d1 /lib
parent07fe4ecd1f73e7b111d4cd6f6bb8fd390b5f3151 (diff)
expose follows / social interaction in frontend
Diffstat (limited to 'lib')
-rwxr-xr-xlib/Travelynx.pm7
-rw-r--r--lib/Travelynx/Command/database.pm21
-rw-r--r--lib/Travelynx/Controller/Account.pm254
-rwxr-xr-xlib/Travelynx/Controller/Profile.pm100
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm1
-rw-r--r--lib/Travelynx/Model/Users.pm106
6 files changed, 463 insertions, 26 deletions
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 }
+ );
+
+ 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;
+}
- return $res->hashes->each;
+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;