From 9332f60a4370cc5ec9a000ef505e613f84b17d2f Mon Sep 17 00:00:00 2001 From: Derf Null Date: Wed, 31 May 2023 22:16:27 +0200 Subject: prepare for follow relations and follow-only checkins --- lib/Travelynx/Command/database.pm | 48 +++++ lib/Travelynx/Model/Users.pm | 194 ++++++++++++++++++++ t/21-relations.t | 375 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 617 insertions(+) create mode 100644 t/21-relations.t diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index 39404c4..f906cb5 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -1470,6 +1470,54 @@ my @migrations = ( } ); }, + + # v34 -> v35 + sub { + my ($db) = @_; + + # 1 : follows + # 2 : follow requested + # 3 : is blocked by + $db->query( + qq{ + create table relations ( + subject_id integer not null references users (id), + predicate smallint not null, + object_id integer not null references users (id), + primary key (subject_id, object_id) + ); + create view followers as select + relations.object_id as self_id, + users.id as id, + users.name as name + from relations + join users on relations.subject_id = users.id + where predicate = 1; + create view followees as select + relations.subject_id as self_id, + users.id as id, + users.name as name + from relations + join users on relations.object_id = users.id + where predicate = 1; + create view follow_requests as select + relations.object_id as self_id, + users.id as id, + users.name as name + from relations + join users on relations.subject_id = users.id + where predicate = 2; + create view blocked_users as select + relations.object_id as self_id, + users.id as id, + users.name as name + from relations + join users on relations.subject_id = users.id + where predicate = 3; + update schema_version set version = 35; + } + ); + }, ); sub sync_stations { diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 67296ca..9b0a115 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -27,6 +27,18 @@ my %visibility_atoi = ( private => 10, ); +my %predicate_itoa = ( + 1 => 'follows', + 2 => 'requests_follow', + 3 => 'is_blocked_by', +); + +my %predicate_atoi = ( + follows => 1, + requests_follow => 2, + is_blocked_by => 3, +); + my @sb_templates = ( undef, [ 'DBF', 'https://dbf.finalrewind.org/{name}?rt=1#{tt}{tn}' ], @@ -710,4 +722,186 @@ 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 ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + my $target = $opt{target}; + + my $res_h = $db->select( + 'relations', + ['predicate'], + { + subject_id => $uid, + object_id => $target + } + )->hash; + + if ($res_h) { + return $predicate_itoa{ $res_h->{predicate} }; + } + return; + + #my $res_h = $db->select( 'relations', ['subject_id', 'predicate'], + # { subject_id => [$uid, $target], object_id => [$target, $target] } )->hash; +} + +sub request_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{requests_follow}, + object_id => $target, + } + ); +} + +sub accept_follow_request { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + my $applicant = $opt{applicant}; + + $db->update( + 'relations', + { + predicate => $predicate_atoi{follows}, + }, + { + subject_id => $applicant, + predicate => $predicate_atoi{requests_follow}, + object_id => $uid + } + ); +} + +sub reject_follow_request { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + my $applicant = $opt{applicant}; + + $db->delete( + 'relations', + { + subject_id => $applicant, + predicate => $predicate_atoi{requests_follow}, + object_id => $uid + } + ); +} + +sub remove_follower { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + my $follower = $opt{follower}; + + $db->delete( + 'relations', + { + subject_id => $follower, + predicate => $predicate_atoi{follows}, + object_id => $uid + } + ); +} + +sub block { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + my $target = $opt{target}; + + $db->insert( + 'relations', + { + subject_id => $target, + predicate => $predicate_atoi{is_blocked_by}, + object_id => $uid + }, + { + on_conflict => \ +'(subject_id, object_id) do update set predicate = EXCLUDED.predicate' + }, + ); +} + +sub unblock { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + my $target = $opt{target}; + + $db->delete( + 'relations', + { + subject_id => $target, + predicate => $predicate_atoi{is_blocked_by}, + object_id => $uid + }, + ); +} + +sub get_followers { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + + my $res = $db->select( 'followers', [ 'id', 'name' ], { self_id => $uid } ); + + return $res->hashes->each; +} + +sub get_follow_requests { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + + my $res + = $db->select( 'follow_requests', [ 'id', 'name' ], { self_id => $uid } ); + + return $res->hashes->each; +} + +sub get_followees { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + + my $res = $db->select( 'followees', [ 'id', 'name' ], { self_id => $uid } ); + + return $res->hashes->each; +} + +sub get_blocked_users { + my ( $self, %opt ) = @_; + + my $db = $opt{db} // $self->{pg}->db; + my $uid = $opt{uid}; + + my $res + = $db->select( 'blocked_users', [ 'id', 'name' ], { self_id => $uid } ); + + return $res->hashes->each; +} + 1; diff --git a/t/21-relations.t b/t/21-relations.t new file mode 100644 index 0000000..d34e3ea --- /dev/null +++ b/t/21-relations.t @@ -0,0 +1,375 @@ +#!/usr/bin/env perl + +# Copyright (C) 2023 Birthe Friesel +# +# SPDX-License-Identifier: MIT + +use Mojo::Base -strict; + +# Tests journey entry and statistics + +use Test::More; +use Test::Mojo; + +# Include application +use FindBin; +require "$FindBin::Bin/../index.pl"; + +my $t = Test::Mojo->new('Travelynx'); + +if ( not $t->app->config->{db} ) { + plan( skip_all => 'No database configured' ); +} + +$t->app->pg->db->query('drop schema if exists travelynx_test_21 cascade'); +$t->app->pg->db->query('create schema travelynx_test_21'); +$t->app->pg->db->query('set search_path to travelynx_test_21'); +$t->app->pg->on( + connection => sub { + my ( $pg, $dbh ) = @_; + $dbh->do('set search_path to travelynx_test_21'); + } +); + +$t->app->config->{mail}->{disabled} = 1; + +$t->app->start( 'database', 'migrate' ); + +my $u = $t->app->users; + +my $uid1 = $u->add( + name => 'test1', + email => 'test1@example.org', + token => 'abcd', + password => q{}, +); + +my $uid2 = $u->add( + name => 'test2', + email => 'test2@example.org', + token => 'efgh', + password => q{}, +); + +$u->verify_registration_token( + uid => $uid1, + token => 'abcd' +); +$u->verify_registration_token( + uid => $uid2, + token => 'efgh' +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + undef +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); +is( scalar $u->get_followers( uid => $uid1 ), 0 ); +is( scalar $u->get_followers( uid => $uid2 ), 0 ); +is( scalar $u->get_followees( uid => $uid1 ), 0 ); +is( scalar $u->get_followees( uid => $uid2 ), 0 ); +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 ); + +$u->request_follow( + uid => $uid1, + target => $uid2 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + 'requests_follow' +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); +is( scalar $u->get_followers( uid => $uid1 ), 0 ); +is( scalar $u->get_followers( uid => $uid2 ), 0 ); +is( scalar $u->get_followees( uid => $uid1 ), 0 ); +is( scalar $u->get_followees( uid => $uid2 ), 0 ); +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_deeply( + [ $u->get_follow_requests( uid => $uid2 ) ], + [ { id => $uid1, name => 'test1' } ] +); + +$u->reject_follow_request( + uid => $uid2, + applicant => $uid1 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + undef +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); +is( scalar $u->get_followers( uid => $uid1 ), 0 ); +is( scalar $u->get_followers( uid => $uid2 ), 0 ); +is( scalar $u->get_followees( uid => $uid1 ), 0 ); +is( scalar $u->get_followees( uid => $uid2 ), 0 ); +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 ); + +$u->request_follow( + uid => $uid1, + target => $uid2 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + 'requests_follow' +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); +is( scalar $u->get_followers( uid => $uid1 ), 0 ); +is( scalar $u->get_followers( uid => $uid2 ), 0 ); +is( scalar $u->get_followees( uid => $uid1 ), 0 ); +is( scalar $u->get_followees( uid => $uid2 ), 0 ); +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_deeply( + [ $u->get_follow_requests( uid => $uid2 ) ], + [ { id => $uid1, name => 'test1' } ] +); + +$u->accept_follow_request( + uid => $uid2, + applicant => $uid1 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + 'follows' +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); +is( scalar $u->get_followers( uid => $uid1 ), 0 ); +is( scalar $u->get_followers( uid => $uid2 ), 1 ); +is( scalar $u->get_followees( uid => $uid1 ), 1 ); +is( scalar $u->get_followees( uid => $uid2 ), 0 ); +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_deeply( + [ $u->get_followers( uid => $uid2 ) ], + [ { id => $uid1, name => 'test1' } ] +); +is_deeply( + [ $u->get_followees( uid => $uid1 ) ], + [ { id => $uid2, name => 'test2' } ] +); + +$u->remove_follower( + uid => $uid2, + follower => $uid1 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + undef +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); +is( scalar $u->get_followers( uid => $uid1 ), 0 ); +is( scalar $u->get_followers( uid => $uid2 ), 0 ); +is( scalar $u->get_followees( uid => $uid1 ), 0 ); +is( scalar $u->get_followees( uid => $uid2 ), 0 ); +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 ); + +$u->request_follow( + uid => $uid1, + target => $uid2 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + 'requests_follow' +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); + +$u->block( + uid => $uid2, + target => $uid1 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + 'is_blocked_by' +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); +is( scalar $u->get_followers( uid => $uid1 ), 0 ); +is( scalar $u->get_followers( uid => $uid2 ), 0 ); +is( scalar $u->get_followees( uid => $uid1 ), 0 ); +is( scalar $u->get_followees( uid => $uid2 ), 0 ); +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_deeply( + [ $u->get_blocked_users( uid => $uid2 ) ], + [ { id => $uid1, name => 'test1' } ] +); + +$u->unblock( + uid => $uid2, + target => $uid1 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + undef +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); +is( scalar $u->get_followers( uid => $uid1 ), 0 ); +is( scalar $u->get_followers( uid => $uid2 ), 0 ); +is( scalar $u->get_followees( uid => $uid1 ), 0 ); +is( scalar $u->get_followees( uid => $uid2 ), 0 ); +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 ); + +$u->block( + uid => $uid2, + target => $uid1 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + 'is_blocked_by' +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); +is( scalar $u->get_followers( uid => $uid1 ), 0 ); +is( scalar $u->get_followers( uid => $uid2 ), 0 ); +is( scalar $u->get_followees( uid => $uid1 ), 0 ); +is( scalar $u->get_followees( uid => $uid2 ), 0 ); +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_deeply( + [ $u->get_blocked_users( uid => $uid2 ) ], + [ { id => $uid1, name => 'test1' } ] +); + +$u->unblock( + uid => $uid2, + target => $uid1 +); + +is( + $u->get_relation( + uid => $uid1, + target => $uid2 + ), + undef +); +is( + $u->get_relation( + uid => $uid2, + target => $uid1 + ), + undef +); + +$t->app->pg->db->query('drop schema travelynx_test_21 cascade'); +done_testing(); -- cgit v1.2.3