1161 lines
23 KiB
Perl
1161 lines
23 KiB
Perl
package Travelynx::Model::Users;
|
|
|
|
# Copyright (C) 2020-2023 Birte Kristina Friesel
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
use strict;
|
|
use warnings;
|
|
use 5.020;
|
|
|
|
use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
|
|
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 %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}' ],
|
|
[ 'bahn.expert', 'https://bahn.expert/{name}#{id}' ],
|
|
[ 'DBF HAFAS', 'https://dbf.finalrewind.org/{name}?rt=1&hafas=1#{tt}{tn}' ],
|
|
[ 'bahn.expert/regional', 'https://bahn.expert/regional/{name}#{id}' ],
|
|
);
|
|
|
|
my %token_id = (
|
|
status => 1,
|
|
history => 2,
|
|
travel => 3,
|
|
import => 4,
|
|
);
|
|
my @token_types = (qw(status history travel import));
|
|
|
|
sub new {
|
|
my ( $class, %opt ) = @_;
|
|
|
|
return bless( \%opt, $class );
|
|
}
|
|
|
|
sub hash_password {
|
|
my ( $self, $password ) = @_;
|
|
my @salt_bytes = map { int( rand(255) ) + 1 } ( 1 .. 16 );
|
|
my $salt = en_base64( pack( 'C[16]', @salt_bytes ) );
|
|
|
|
return bcrypt( substr( $password, 0, 10000 ), '$2a$12$' . $salt );
|
|
}
|
|
|
|
sub get_token_id {
|
|
my ( $self, $type ) = @_;
|
|
|
|
return $token_id{$type};
|
|
}
|
|
|
|
sub mark_seen {
|
|
my ( $self, %opt ) = @_;
|
|
my $uid = $opt{uid};
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
|
|
$db->update(
|
|
'users',
|
|
{
|
|
last_seen => DateTime->now( time_zone => 'Europe/Berlin' ),
|
|
deletion_notified => undef
|
|
},
|
|
{ id => $uid }
|
|
);
|
|
}
|
|
|
|
sub mark_deletion_notified {
|
|
my ( $self, %opt ) = @_;
|
|
my $uid = $opt{uid};
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
|
|
$db->update(
|
|
'users',
|
|
{
|
|
deletion_notified => DateTime->now( time_zone => 'Europe/Berlin' ),
|
|
},
|
|
{ id => $uid }
|
|
);
|
|
}
|
|
|
|
sub verify_registration_token {
|
|
my ( $self, %opt ) = @_;
|
|
my $uid = $opt{uid};
|
|
my $token = $opt{token};
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
|
|
my $tx;
|
|
if ( not $opt{in_transaction} ) {
|
|
$tx = $db->begin;
|
|
}
|
|
|
|
my $res = $db->select(
|
|
'pending_registrations',
|
|
'count(*) as count',
|
|
{
|
|
user_id => $uid,
|
|
token => $token
|
|
}
|
|
);
|
|
|
|
if ( $res->hash->{count} ) {
|
|
$db->update( 'users', { status => 1 }, { id => $uid } );
|
|
$db->delete( 'pending_registrations', { user_id => $uid } );
|
|
if ($tx) {
|
|
$tx->commit;
|
|
}
|
|
return 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub get_api_token {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
|
|
my $token = {};
|
|
my $res = $db->select( 'tokens', [ 'type', 'token' ], { user_id => $uid } );
|
|
|
|
for my $entry ( $res->hashes->each ) {
|
|
$token->{ $token_types[ $entry->{type} - 1 ] }
|
|
= $entry->{token};
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
|
|
sub get_uid_by_name_and_mail {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $name = $opt{name};
|
|
my $email = $opt{email};
|
|
|
|
my $res = $db->select(
|
|
'users',
|
|
['id'],
|
|
{
|
|
name => $name,
|
|
email => $email,
|
|
status => 1
|
|
}
|
|
);
|
|
|
|
if ( my $user = $res->hash ) {
|
|
return $user->{id};
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub get_privacy_by {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
|
|
my %where;
|
|
|
|
if ( $opt{name} ) {
|
|
$where{name} = $opt{name};
|
|
}
|
|
else {
|
|
$where{id} = $opt{uid};
|
|
}
|
|
|
|
my $res = $db->select(
|
|
'users',
|
|
[ 'id', 'name', 'public_level', 'accept_follows' ],
|
|
{ %where, status => 1 }
|
|
);
|
|
|
|
if ( my $user = $res->hash ) {
|
|
return {
|
|
id => $user->{id},
|
|
name => $user->{name},
|
|
default_visibility => $user->{public_level} & 0x7f,
|
|
default_visibility_str =>
|
|
$visibility_itoa{ $user->{public_level} & 0x7f },
|
|
comments_visible => $user->{public_level} & 0x80 ? 1 : 0,
|
|
past_visibility => ( $user->{public_level} & 0x7f00 ) >> 8,
|
|
past_visibility_str =>
|
|
$visibility_itoa{ ( $user->{public_level} & 0x7f00 ) >> 8 },
|
|
past_status => $user->{public_level} & 0x08000 ? 1 : 0,
|
|
past_all => $user->{public_level} & 0x10000 ? 1 : 0,
|
|
accept_follows => $user->{accept_follows} == 2 ? 1 : 0,
|
|
accept_follow_requests => $user->{accept_follows} == 1 ? 1 : 0,
|
|
};
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub set_privacy {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
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 : 0 )
|
|
| ( ( $opt{past_visibility} & 0x7f ) << 8 )
|
|
| ( $opt{past_status} ? 0x08000 : 0 )
|
|
| ( $opt{past_all} ? 0x10000 : 0 );
|
|
}
|
|
|
|
$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;
|
|
my $uid = $opt{uid};
|
|
my $token = $opt{token};
|
|
|
|
my $res = $db->select(
|
|
'pending_passwords',
|
|
'count(*) as count',
|
|
{ user_id => $uid }
|
|
);
|
|
if ( $res->hash->{count} ) {
|
|
return 'in progress';
|
|
}
|
|
|
|
$db->insert(
|
|
'pending_passwords',
|
|
{
|
|
user_id => $uid,
|
|
token => $token,
|
|
requested_at => DateTime->now( time_zone => 'Europe/Berlin' )
|
|
}
|
|
);
|
|
|
|
return undef;
|
|
}
|
|
|
|
sub verify_password_token {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $token = $opt{token};
|
|
|
|
my $res = $db->select(
|
|
'pending_passwords',
|
|
'count(*) as count',
|
|
{
|
|
user_id => $uid,
|
|
token => $token
|
|
}
|
|
);
|
|
|
|
if ( $res->hash->{count} ) {
|
|
return 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub mark_for_mail_change {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $email = $opt{email};
|
|
my $token = $opt{token};
|
|
|
|
$db->insert(
|
|
'pending_mails',
|
|
{
|
|
user_id => $uid,
|
|
email => $email,
|
|
token => $token,
|
|
requested_at => DateTime->now( time_zone => 'Europe/Berlin' )
|
|
},
|
|
{
|
|
on_conflict => \
|
|
'(user_id) do update set email = EXCLUDED.email, token = EXCLUDED.token, requested_at = EXCLUDED.requested_at'
|
|
},
|
|
);
|
|
}
|
|
|
|
sub change_mail_with_token {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $token = $opt{token};
|
|
|
|
my $tx = $db->begin;
|
|
|
|
my $res_h = $db->select(
|
|
'pending_mails',
|
|
['email'],
|
|
{
|
|
user_id => $uid,
|
|
token => $token
|
|
}
|
|
)->hash;
|
|
|
|
if ($res_h) {
|
|
$db->update( 'users', { email => $res_h->{email} }, { id => $uid } );
|
|
$db->delete( 'pending_mails', { user_id => $uid } );
|
|
$tx->commit;
|
|
return 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub is_name_invalid {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $name = $opt{name};
|
|
|
|
if ( not length($name) ) {
|
|
return 'user_empty';
|
|
}
|
|
|
|
if ( $name !~ m{ ^ [0-9a-zA-Z_-]+ $ }x ) {
|
|
return 'user_format';
|
|
}
|
|
|
|
if (
|
|
$self->user_name_exists(
|
|
db => $db,
|
|
name => $name
|
|
)
|
|
)
|
|
{
|
|
return 'user_collision';
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub change_name {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
|
|
eval { $db->update( 'users', { name => $opt{name} }, { id => $uid } ); };
|
|
|
|
if ($@) {
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub remove_password_token {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $token = $opt{token};
|
|
|
|
$db->delete(
|
|
'pending_passwords',
|
|
{
|
|
user_id => $uid,
|
|
token => $token
|
|
}
|
|
);
|
|
}
|
|
|
|
sub get {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
|
|
my $user = $db->select(
|
|
'users',
|
|
'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',
|
|
{ id => $uid }
|
|
)->hash;
|
|
if ($user) {
|
|
return {
|
|
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,
|
|
default_visibility => $user->{public_level} & 0x7f,
|
|
default_visibility_str =>
|
|
$visibility_itoa{ $user->{public_level} & 0x7f },
|
|
comments_visible => $user->{public_level} & 0x80 ? 1 : 0,
|
|
past_visibility => ( $user->{public_level} & 0x7f00 ) >> 8,
|
|
past_visibility_str =>
|
|
$visibility_itoa{ ( $user->{public_level} & 0x7f00 ) >> 8 },
|
|
past_status => $user->{public_level} & 0x08000 ? 1 : 0,
|
|
past_all => $user->{public_level} & 0x10000 ? 1 : 0,
|
|
email => $user->{email},
|
|
sb_name => $user->{external_services}
|
|
? $sb_templates[ $user->{external_services} & 0x07 ][0]
|
|
: undef,
|
|
sb_template => $user->{external_services}
|
|
? $sb_templates[ $user->{external_services} & 0x07 ][1]
|
|
: undef,
|
|
registered_at => DateTime->from_epoch(
|
|
epoch => $user->{registered_at_ts},
|
|
time_zone => 'Europe/Berlin'
|
|
),
|
|
last_seen => DateTime->from_epoch(
|
|
epoch => $user->{last_seen_ts},
|
|
time_zone => 'Europe/Berlin'
|
|
),
|
|
deletion_requested => $user->{deletion_requested_ts}
|
|
? DateTime->from_epoch(
|
|
epoch => $user->{deletion_requested_ts},
|
|
time_zone => 'Europe/Berlin'
|
|
)
|
|
: undef,
|
|
};
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
sub get_login_data {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $name = $opt{name};
|
|
|
|
my $res_h = $db->select(
|
|
'users',
|
|
'id, name, status, password as password_hash',
|
|
{ name => $name }
|
|
)->hash;
|
|
|
|
return $res_h;
|
|
}
|
|
|
|
sub add {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $user_name = $opt{name};
|
|
my $email = $opt{email};
|
|
my $token = $opt{token};
|
|
my $password = $self->hash_password( $opt{password} );
|
|
|
|
# This helper must be called during a transaction, as user creation
|
|
# may fail even after the database entry has been generated, e.g. if
|
|
# the registration mail cannot be sent. We therefore use $db (the
|
|
# database handle performing the transaction) instead of $self->pg->db
|
|
# (which may be a new handle not belonging to the transaction).
|
|
|
|
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
|
|
|
|
my $res = $db->insert(
|
|
'users',
|
|
{
|
|
name => $user_name,
|
|
status => 0,
|
|
public_level => $visibility_atoi{unlisted}
|
|
| ( $visibility_atoi{unlisted} << 8 ),
|
|
email => $email,
|
|
password => $password,
|
|
registered_at => $now,
|
|
last_seen => $now,
|
|
},
|
|
{ returning => 'id' }
|
|
);
|
|
my $uid = $res->hash->{id};
|
|
|
|
$db->insert(
|
|
'pending_registrations',
|
|
{
|
|
user_id => $uid,
|
|
token => $token
|
|
}
|
|
);
|
|
|
|
return $uid;
|
|
}
|
|
|
|
sub flag_deletion {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
|
|
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
|
|
|
|
$db->update(
|
|
'users',
|
|
{ deletion_requested => $now },
|
|
{
|
|
id => $uid,
|
|
}
|
|
);
|
|
}
|
|
|
|
sub unflag_deletion {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
|
|
$db->update(
|
|
'users',
|
|
{
|
|
deletion_requested => undef,
|
|
},
|
|
{
|
|
id => $uid,
|
|
}
|
|
);
|
|
}
|
|
|
|
sub delete {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $tx;
|
|
if ( not $opt{in_transaction} ) {
|
|
$tx = $db->begin;
|
|
}
|
|
|
|
my %res;
|
|
|
|
$res{tokens} = $db->delete( 'tokens', { user_id => $uid } );
|
|
$res{stats} = $db->delete( 'journey_stats', { user_id => $uid } );
|
|
$res{journeys} = $db->delete( 'journeys', { user_id => $uid } );
|
|
$res{transit} = $db->delete( 'in_transit', { user_id => $uid } );
|
|
$res{hooks} = $db->delete( 'webhooks', { user_id => $uid } );
|
|
$res{trwl} = $db->delete( 'traewelling', { user_id => $uid } );
|
|
$res{lt} = $db->delete( 'localtransit', { user_id => $uid } );
|
|
$res{password} = $db->delete( 'pending_passwords', { user_id => $uid } );
|
|
$res{users} = $db->delete( 'users', { id => $uid } );
|
|
|
|
for my $key ( keys %res ) {
|
|
$res{$key} = $res{$key}->rows;
|
|
}
|
|
|
|
if ( $res{users} != 1 ) {
|
|
die("Deleted $res{users} rows from users, expected 1. Rolling back.\n");
|
|
}
|
|
|
|
if ($tx) {
|
|
$tx->commit;
|
|
}
|
|
|
|
return \%res;
|
|
}
|
|
|
|
sub set_password {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $password = $self->hash_password( $opt{password} );
|
|
|
|
$db->update( 'users', { password => $password }, { id => $uid } );
|
|
}
|
|
|
|
sub user_name_exists {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $user_name = $opt{name};
|
|
|
|
my $count
|
|
= $db->select( 'users', 'count(*) as count', { name => $user_name } )
|
|
->hash->{count};
|
|
|
|
if ($count) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub mail_is_blacklisted {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $mail = $opt{email};
|
|
|
|
my $count = $db->select(
|
|
'users',
|
|
'count(*) as count',
|
|
{
|
|
email => $mail,
|
|
status => 0,
|
|
}
|
|
)->hash->{count};
|
|
|
|
if ($count) {
|
|
return 1;
|
|
}
|
|
|
|
$count = $db->select(
|
|
'mail_blacklist',
|
|
'count(*) as count',
|
|
{
|
|
email => $mail,
|
|
num_tries => { '>', 1 },
|
|
}
|
|
)->hash->{count};
|
|
|
|
if ($count) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
sub use_history {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $value = $opt{set};
|
|
|
|
if ( $opt{destinations} ) {
|
|
$db->insert(
|
|
'localtransit',
|
|
{
|
|
user_id => $uid,
|
|
data =>
|
|
JSON->new->encode( { destinations => $opt{destinations} } )
|
|
},
|
|
{ on_conflict => \'(user_id) do update set data = EXCLUDED.data' }
|
|
);
|
|
}
|
|
|
|
if ($value) {
|
|
$db->update( 'users', { use_history => $value }, { id => $uid } );
|
|
}
|
|
else {
|
|
if ( $opt{with_local_transit} ) {
|
|
my $res = $db->select(
|
|
'user_transit',
|
|
[ 'use_history', 'data' ],
|
|
{ id => $uid }
|
|
)->expand->hash;
|
|
return ( $res->{use_history}, $res->{data}{destinations} // [] );
|
|
}
|
|
else {
|
|
return $db->select( 'users', ['use_history'], { id => $uid } )
|
|
->hash->{use_history};
|
|
}
|
|
}
|
|
}
|
|
|
|
sub use_external_services {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $value = $opt{set};
|
|
|
|
if ( defined $value ) {
|
|
if ( $value < 0 or $value > 4 ) {
|
|
$value = 0;
|
|
}
|
|
$db->update( 'users', { external_services => $value }, { id => $uid } );
|
|
}
|
|
else {
|
|
return $db->select( 'users', ['external_services'], { id => $uid } )
|
|
->hash->{external_services};
|
|
}
|
|
}
|
|
|
|
sub get_webhook {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
|
|
my $res_h = $db->select( 'webhooks_str', '*', { user_id => $uid } )->hash;
|
|
|
|
$res_h->{latest_run} = DateTime->from_epoch(
|
|
epoch => $res_h->{latest_run_ts} // 0,
|
|
time_zone => 'Europe/Berlin',
|
|
locale => 'de-DE',
|
|
);
|
|
|
|
return $res_h;
|
|
}
|
|
|
|
sub set_webhook {
|
|
my ( $self, %opt ) = @_;
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
|
|
if ( $opt{token} ) {
|
|
$opt{token} =~ tr{\r\n}{}d;
|
|
}
|
|
|
|
my $res = $db->insert(
|
|
'webhooks',
|
|
{
|
|
user_id => $opt{uid},
|
|
enabled => $opt{enabled},
|
|
url => $opt{url},
|
|
token => $opt{token}
|
|
},
|
|
{
|
|
on_conflict => \
|
|
'(user_id) do update set enabled = EXCLUDED.enabled, url = EXCLUDED.url, token = EXCLUDED.token, errored = null, latest_run = null, output = null'
|
|
}
|
|
);
|
|
}
|
|
|
|
sub update_webhook_status {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $url = $opt{url};
|
|
my $success = $opt{success};
|
|
my $text = $opt{text};
|
|
|
|
if ( length($text) > 1000 ) {
|
|
$text = substr( $text, 0, 1000 ) . '…';
|
|
}
|
|
|
|
$db->update(
|
|
'webhooks',
|
|
{
|
|
errored => $success ? 0 : 1,
|
|
latest_run => DateTime->now( time_zone => 'Europe/Berlin' ),
|
|
output => $text,
|
|
},
|
|
{
|
|
user_id => $uid,
|
|
url => $url
|
|
}
|
|
);
|
|
}
|
|
|
|
sub set_profile {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $profile = $opt{profile};
|
|
|
|
$db->update(
|
|
'users',
|
|
{ profile => JSON->new->encode($profile) },
|
|
{ id => $uid }
|
|
);
|
|
}
|
|
|
|
sub get_profile {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
|
|
return $db->select( 'users', ['profile'], { id => $uid } )
|
|
->expand->hash->{profile};
|
|
}
|
|
|
|
sub get_relation {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $subject = $opt{subject};
|
|
my $object = $opt{object};
|
|
|
|
my $res_h = $db->select(
|
|
'relations',
|
|
['predicate'],
|
|
{
|
|
subject_id => $subject,
|
|
object_id => $object,
|
|
}
|
|
)->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 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 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 ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
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 {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
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,
|
|
predicate => $predicate_atoi{requests_follow},
|
|
object_id => $uid
|
|
}
|
|
);
|
|
$self->update_notifications(
|
|
db => $db,
|
|
uid => $uid
|
|
);
|
|
|
|
if ($tx) {
|
|
$tx->commit;
|
|
}
|
|
}
|
|
|
|
sub reject_follow_request {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $applicant = $opt{applicant};
|
|
|
|
my $tx;
|
|
if ( not $opt{in_transaction} ) {
|
|
$tx = $db->begin;
|
|
}
|
|
|
|
$db->delete(
|
|
'relations',
|
|
{
|
|
subject_id => $applicant,
|
|
predicate => $predicate_atoi{requests_follow},
|
|
object_id => $uid
|
|
}
|
|
);
|
|
$self->update_notifications(
|
|
db => $db,
|
|
uid => $uid
|
|
);
|
|
|
|
if ($tx) {
|
|
$tx->commit;
|
|
}
|
|
}
|
|
|
|
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 ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
my $uid = $opt{uid};
|
|
my $target = $opt{target};
|
|
|
|
$db->delete(
|
|
'relations',
|
|
{
|
|
subject_id => $uid,
|
|
predicate => $predicate_atoi{follows},
|
|
object_id => $target
|
|
}
|
|
);
|
|
}
|
|
|
|
sub remove_follower {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
$self->unfollow(
|
|
db => $opt{db},
|
|
uid => $opt{follower},
|
|
target => $opt{uid},
|
|
);
|
|
}
|
|
|
|
sub block {
|
|
my ( $self, %opt ) = @_;
|
|
|
|
my $db = $opt{db} // $self->{pg}->db;
|
|
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,
|
|
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 {
|
|
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', '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;
|
|
}
|
|
|
|
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 {
|
|
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 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 ) = @_;
|
|
|
|
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 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 ) = @_;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|