From d4a647014194d635d1025f83e595138c71db9c1d Mon Sep 17 00:00:00 2001 From: Derf Null Date: Sat, 3 Jun 2023 10:42:14 +0200 Subject: Users: support notifications about pending follow requests --- lib/Travelynx/Command/database.pm | 29 ++++++++ lib/Travelynx/Model/Users.pm | 145 ++++++++++++++++++++++++++++++++++---- t/21-relations.t | 46 ++++++++++++ 3 files changed, 206 insertions(+), 14 deletions(-) diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index f906cb5..53168bf 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -1518,8 +1518,37 @@ my @migrations = ( } ); }, + + # v35 -> v36 + sub { + my ($db) = @_; + $db->query( + qq{ + alter table relations + add column ts timestamptz not null; + alter table users + add column accept_follows smallint default 1; + update schema_version set version = 36; + } + ); + }, + + # v36 -> v37 + sub { + my ($db) = @_; + $db->query( + qq{ + alter table users + add column notifications smallint default 0, + add column profile jsonb; + update schema_version set version = 37; + } + ); + }, ); +# TODO add 'hafas' column to in_transit (and maybe journeys? undo/redo needs something to work with...) + sub sync_stations { my ( $db, $iris_version ) = @_; diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 4444dc9..fbd53fd 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -103,7 +103,6 @@ sub verify_registration_token { my $db = $opt{db} // $self->{pg}->db; my $tx; - if ( not $opt{in_transaction} ) { $tx = $db->begin; } @@ -120,7 +119,7 @@ sub verify_registration_token { if ( $res->hash->{count} ) { $db->update( 'users', { status => 1 }, { id => $uid } ); $db->delete( 'pending_registrations', { user_id => $uid } ); - if ( not $opt{in_transaction} ) { + if ($tx) { $tx->commit; } return 1; @@ -179,8 +178,11 @@ sub get_privacy_by { $where{id} = $opt{uid}; } - my $res = $db->select( 'users', [ 'id', 'public_level' ], - { %where, status => 1 } ); + my $res = $db->select( + 'users', + [ 'id', 'public_level', 'accept_follows' ], + { %where, status => 1 } + ); if ( my $user = $res->hash ) { return { @@ -189,10 +191,12 @@ sub get_privacy_by { 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, - past_status => $user->{public_level} & 0x800 ? 1 : 0, + comments_visible => $user->{public_level} & 0x80 ? 1 : 0, + past_visible => ( $user->{public_level} & 0x300 ) >> 8, + past_all => $user->{public_level} & 0x400 ? 1 : 0, + past_status => $user->{public_level} & 0x800 ? 1 : 0, + accept_follows => $user->{accept_follows} == 2 ? 1 : 0, + accept_follow_requests => $user->{accept_follows} == 1 ? 1 : 0, }; } return; @@ -215,6 +219,27 @@ sub set_privacy { $db->update( 'users', { public_level => $public_level }, { id => $uid } ); } +sub set_social { + my ( $self, %opt ) = @_; + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + + my $accept_follows = 0; + + if ( $opt{accept_follows} ) { + $accept_follows = 2; + } + elsif ( $opt{accept_follow_requests} ) { + $accept_follows = 1; + } + + $db->update( + 'users', + { accept_follows => $accept_follows }, + { id => $uid } + ); +} + sub mark_for_password_reset { my ( $self, %opt ) = @_; my $db = $opt{db} // $self->{pg}->db; @@ -373,7 +398,8 @@ sub get { my $user = $db->select( 'users', - 'id, name, status, public_level, email, external_services, ' + 'id, name, status, public_level, email, ' + . 'external_services, accept_follows, notifications, ' . 'extract(epoch from registered_at) as registered_at_ts, ' . 'extract(epoch from last_seen) as last_seen_ts, ' . 'extract(epoch from deletion_requested) as deletion_requested_ts', @@ -384,6 +410,9 @@ sub get { id => $user->{id}, name => $user->{name}, status => $user->{status}, + notifications => $user->{notifications}, + accept_follows => $user->{accept_follows} == 2 ? 1 : 0, + accept_follow_requests => $user->{accept_follows} == 1 ? 1 : 0, is_public => $user->{public_level}, default_visibility => $user->{public_level} & 0x7f, default_visibility_str => @@ -512,7 +541,6 @@ sub delete { my $db = $opt{db} // $self->{pg}->db; my $uid = $opt{uid}; my $tx; - if ( not $opt{in_transaction} ) { $tx = $db->begin; } @@ -537,7 +565,7 @@ sub delete { die("Deleted $res{users} rows from users, expected 1. Rolling back.\n"); } - if ( not $opt{in_transaction} ) { + if ($tx) { $tx->commit; } @@ -722,8 +750,6 @@ sub update_webhook_status { ); } -# TODO irgendwo muss auch noch ne einstellung rein, um follows / follow requests global zu deaktivieren - sub get_relation { my ( $self, %opt ) = @_; @@ -749,6 +775,31 @@ sub get_relation { # { subject_id => [$uid, $target], object_id => [$target, $target] } )->hash; } +sub update_notifications { + my ( $self, %opt ) = @_; + + # must be called inside a transaction, so $opt{db} is mandatory. + my $db = $opt{db}; + my $uid = $opt{uid}; + + my $has_follow_requests = $opt{has_follow_requests} + // $self->has_follow_requests( + db => $db, + uid => $uid + ); + + my $notifications + = $db->select( 'users', ['notifications'], { id => $uid } ) + ->hash->{notifications}; + if ($has_follow_requests) { + $notifications |= 0x01; + } + else { + $notifications &= ~0x01; + } + $db->update( 'users', { notifications => $notifications }, { id => $uid } ); +} + sub request_follow { my ( $self, %opt ) = @_; @@ -756,14 +807,29 @@ sub request_follow { my $uid = $opt{uid}; my $target = $opt{target}; + my $tx; + if ( not $opt{in_transaction} ) { + $tx = $db->begin; + } + $db->insert( 'relations', { subject_id => $uid, predicate => $predicate_atoi{requests_follow}, object_id => $target, + ts => DateTime->now( time_zone => 'Europe/Berlin' ), } ); + $self->update_notifications( + db => $db, + uid => $target, + has_follow_requests => 1, + ); + + if ($tx) { + $tx->commit; + } } sub accept_follow_request { @@ -773,10 +839,16 @@ sub accept_follow_request { my $uid = $opt{uid}; my $applicant = $opt{applicant}; + my $tx; + if ( not $opt{in_transaction} ) { + $tx = $db->begin; + } + $db->update( 'relations', { predicate => $predicate_atoi{follows}, + ts => DateTime->now( time_zone => 'Europe/Berlin' ), }, { subject_id => $applicant, @@ -784,6 +856,14 @@ sub accept_follow_request { object_id => $uid } ); + $self->update_notifications( + db => $db, + uid => $uid + ); + + if ($tx) { + $tx->commit; + } } sub reject_follow_request { @@ -793,6 +873,11 @@ sub reject_follow_request { my $uid = $opt{uid}; my $applicant = $opt{applicant}; + my $tx; + if ( not $opt{in_transaction} ) { + $tx = $db->begin; + } + $db->delete( 'relations', { @@ -801,6 +886,14 @@ sub reject_follow_request { object_id => $uid } ); + $self->update_notifications( + db => $db, + uid => $uid + ); + + if ($tx) { + $tx->commit; + } } sub unfollow { @@ -837,18 +930,32 @@ sub block { my $uid = $opt{uid}; my $target = $opt{target}; + my $tx; + if ( not $opt{in_transaction} ) { + $tx = $db->begin; + } + $db->insert( 'relations', { subject_id => $target, predicate => $predicate_atoi{is_blocked_by}, - object_id => $uid + object_id => $uid, + ts => DateTime->now( time_zone => 'Europe/Berlin' ), }, { on_conflict => \ '(subject_id, object_id) do update set predicate = EXCLUDED.predicate' }, ); + $self->update_notifications( + db => $db, + uid => $uid + ); + + if ($tx) { + $tx->commit; + } } sub unblock { @@ -891,6 +998,16 @@ sub get_follow_requests { return $res->hashes->each; } +sub has_follow_requests { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + + return $db->select( 'follow_requests', 'count(*) as count', + { self_id => $uid } )->hash->{count}; +} + sub get_followees { my ( $self, %opt ) = @_; diff --git a/t/21-relations.t b/t/21-relations.t index 5af691a..54e4867 100644 --- a/t/21-relations.t +++ b/t/21-relations.t @@ -82,6 +82,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); $u->request_follow( uid => $uid1, @@ -110,6 +114,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 1 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 1 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 1 ); is_deeply( [ $u->get_follow_requests( uid => $uid2 ) ], [ { id => $uid1, name => 'test1' } ] @@ -142,6 +150,8 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); $u->request_follow( uid => $uid1, @@ -170,6 +180,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 1 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 1 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 1 ); is_deeply( [ $u->get_follow_requests( uid => $uid2 ) ], [ { id => $uid1, name => 'test1' } ] @@ -202,6 +216,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); is_deeply( [ $u->get_followers( uid => $uid2 ) ], [ { id => $uid1, name => 'test1' } ] @@ -238,6 +256,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); $u->request_follow( uid => $uid1, @@ -286,6 +308,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 1 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); is_deeply( [ $u->get_blocked_users( uid => $uid2 ) ], [ { id => $uid1, name => 'test1' } ] @@ -318,6 +344,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); $u->block( uid => $uid2, @@ -346,6 +376,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 1 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); is_deeply( [ $u->get_blocked_users( uid => $uid2 ) ], [ { id => $uid1, name => 'test1' } ] @@ -378,6 +412,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); $u->request_follow( uid => $uid1, @@ -410,6 +448,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); is_deeply( [ $u->get_followers( uid => $uid2 ) ], [ { id => $uid1, name => 'test1' } ] @@ -446,6 +488,10 @@ is( scalar $u->get_follow_requests( uid => $uid1 ), 0 ); is( scalar $u->get_follow_requests( uid => $uid2 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid1 ), 0 ); is( scalar $u->get_blocked_users( uid => $uid2 ), 0 ); +is( $u->has_follow_requests( uid => $uid1 ), 0 ); +is( $u->has_follow_requests( uid => $uid2 ), 0 ); +is( $u->get( uid => $uid1 )->{notifications}, 0 ); +is( $u->get( uid => $uid2 )->{notifications}, 0 ); $t->app->pg->db->query('drop schema travelynx_test_21 cascade'); done_testing(); -- cgit v1.2.3