From 06f832d8ebaf6c5f81836e66d7dc160db6a8250f Mon Sep 17 00:00:00 2001 From: sergiotarxz Date: Mon, 7 Feb 2022 00:30:41 +0100 Subject: [PATCH] First karma working achieved. --- bin/doctor_agustin.pl | 29 ---- bin/doctor_karma.pl | 118 +++++++++++++++++ lib/DoctorKarma/Config.pm | 88 +++++++++++++ lib/DoctorKarma/DAO/User.pm | 240 ++++++++++++++++++++++++++++++++++ lib/DoctorKarma/DB.pm | 70 ++++++++++ lib/DoctorKarma/Logger.pm | 39 ++++++ lib/DoctorKarma/Model/User.pm | 79 +++++++++++ lib/DoctorKarma/Telegram.pm | 130 ++++++++++++++++++ 8 files changed, 764 insertions(+), 29 deletions(-) delete mode 100755 bin/doctor_agustin.pl create mode 100755 bin/doctor_karma.pl create mode 100644 lib/DoctorKarma/Config.pm create mode 100644 lib/DoctorKarma/DAO/User.pm create mode 100644 lib/DoctorKarma/DB.pm create mode 100644 lib/DoctorKarma/Logger.pm create mode 100644 lib/DoctorKarma/Model/User.pm create mode 100644 lib/DoctorKarma/Telegram.pm diff --git a/bin/doctor_agustin.pl b/bin/doctor_agustin.pl deleted file mode 100755 index e956c7c..0000000 --- a/bin/doctor_agustin.pl +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env perl - -use v5.30.0; - -use strict; -use warnings; - -use Data::Dumper; -use JSON; - -use DoctorKarma::Config; -use DoctorKarma::Telegram; -use DoctorKarma::Logger; - -my $config = DoctorKarma::Config->new; -my $logger = DoctorKarma::Logger->new; -my $telegram = - DoctorKarma::Telegram->new( telegram_token => $config->telegram_token ); -while (1) { - my $updates = $telegram->get_updates; - for my $update ($updates->@*) { - if (exists $update->{message}{text}) { - my $message = $update->{message}{text}; - my $user_id = $update->{message}{from}{id}; - my $username = $update->{message}{from}{username}; - $logger->log_info("'$message' received from $username:$user_id"); - } - } -} diff --git a/bin/doctor_karma.pl b/bin/doctor_karma.pl new file mode 100755 index 0000000..4d30895 --- /dev/null +++ b/bin/doctor_karma.pl @@ -0,0 +1,118 @@ +#!/usr/bin/env perl + +use v5.30.0; + +use strict; +use warnings; + +use Data::Dumper; +use JSON; + +use DoctorKarma::Config; +use DoctorKarma::Telegram; +use DoctorKarma::Logger; +use DoctorKarma::DB; + +use DoctorKarma::DAO::User; +use DoctorKarma::Model::User; + +my $config = DoctorKarma::Config->new; +my $logger = DoctorKarma::Logger->new; +my $telegram = + DoctorKarma::Telegram->new( telegram_token => $config->telegram_token ); +my $db = DoctorKarma::DB->dbh; + +while (1) { + my $updates = $telegram->get_updates; + for my $update ( $updates->@* ) { + my $message = $update->{message}; + if ( defined $message ) { + proccess_new_message($message); + } + } +} + +sub proccess_new_message { + my $message = shift; + my $reply_to_message = $message->{reply_to_message}; + + update_user ($message); + if ( defined $message->{text} ) { + process_message_with_text($message); + } +} + +sub process_message_with_text { + my $message = shift; + + my $text = $message->{text}; + my $username = $message->{from}{username}; + my $user_id = $message->{from}{id}; + my $first_name = $message->{from}{first_name}; + my $reply_to_message = $message->{reply_to_message}; + + my $log_username = defined $username ? "\@$username" : ''; + $logger->log_info( + "'$message->{text}' received from $first_name:$log_username:$user_id"); + if ( $text eq '+1' && defined $reply_to_message ) { + add_karma_to_replied_message_user ($message); + } +} + +sub add_karma_to_replied_message_user { + my $message = shift; + + my $reply_to_message = $message->{reply_to_message}; + my $user_giving_id = $message->{from}{id}; + my $user_receiving_id = $reply_to_message->{from}{id}; + + my $user_dao = DoctorKarma::DAO::User->new( dbh => $db ); + my $sending_karma_user = $user_dao->recover_id( id => $user_giving_id ); + + if ( $user_giving_id == $user_receiving_id ) { + $logger->log_info(<<"EOF"); +User @{[$sending_karma_user->first_name]}:@{[$sending_karma_user->username]}:@{[$sending_karma_user->id_user]} tried to give karma to itself, refusing. +EOF + return; + } + + my $receiving_karma_user = + $user_dao->recover_id( id => $user_receiving_id ); + if (!defined $receiving_karma_user) { + update_user ($reply_to_message); + $receiving_karma_user = $user_dao->recover_id (id => $user_receiving_id); + } + $user_dao->add_1_karma(user => $receiving_karma_user); + +} + +sub update_user { + my $message = shift; + + my $user_id = $message->{from}{id}; + my $username = $message->{from}{username}; + my $first_name = $message->{from}{first_name}; + + my $user_dao = DoctorKarma::DAO::User->new( dbh => $db ); + my $user = $user_dao->recover_id( id => $user_id ); + if ( !defined $user ) { + $user = DoctorKarma::Model::User->new( + id_user => $user_id, + (defined $username) ? (username => $username) : (), + karma => 0 + ); + $user_dao->store(user => $user); + } + if ( ( $user->username // '' ) ne ( $username // '' ) ) { + $user_dao->update_username( + user => $user, + username => $username + ); + } + if ( ( $user->first_name // '' ) ne $first_name ) { + $user_dao->update_firstname( + user => $user, + first_name => $first_name + ); + } +} diff --git a/lib/DoctorKarma/Config.pm b/lib/DoctorKarma/Config.pm new file mode 100644 index 0000000..371ba3f --- /dev/null +++ b/lib/DoctorKarma/Config.pm @@ -0,0 +1,88 @@ +package DoctorKarma::Config; + +use v5.30.0; + +use strict; +use warnings; + +use Term::ReadLine; + +use Const::Fast; +use JSON; +use Path::Tiny; + +use DoctorKarma::Logger; + +sub HOME { $ENV{HOME} } +sub CONFIG_DIR { "@{[HOME]}/.config/doctorkarma" } +sub CONFIG_FILE { "@{[CONFIG_DIR]}/config.json" } + +sub new { + my $class = shift; + my $self = bless {}, $class; + $self->_create_config_file_if_not_exists; + return $self; +} + +sub logger { + my $self = shift; + if ( !defined $self->{logger} ) { + my $logger = DoctorKarma::Logger->new; + $self->{logger} = $logger; + } + return $self->{logger}; +} + +sub _config { + my $self = shift; + if ( !defined $self->{config} ) { + if ( !-f CONFIG_FILE ) { + $self->logger->log_error( + qq(@{[CONFIG_FILE]} is not a plain file, unable to read config.) + ); + die; + } + my $config = decode_json( path(CONFIG_FILE)->slurp_utf8 ); + $self->{config} = $config; + } + return $self->{config}; +} + +sub telegram_token { + my $self = shift; + if ( !defined $self->{telegram_token} ) { + my $config = $self->_config; + $self->{telegram_token} = $config->{telegram_token}; + } + return $self->{telegram_token}; +} + +sub _create_config_file_if_not_exists { + my $self = shift; + if ( !-e CONFIG_FILE ) { + $self->logger->log_info(q(Config file not detected)); + $self->_create_config_file; + } + +} + +sub _create_config_file { + my $self = shift; + my $logger = $self->logger; + if ( !-e CONFIG_DIR ) { + $logger->log_info(qq(Creating @{[CONFIG_DIR]})); + eval { path(CONFIG_DIR)->mkpath; }; + if ($@) { + $logger->log_error( + qq(Unable to create directory @{[CONFIG_DIR]}: $@)); + die; + } + } + my $term = Term::ReadLine->new('Doctor Agustín'); + my $token = $term->readline('Telegram token:'); + my $config_contents = { telegram_token => $token }; + $config_contents = encode_json($config_contents); + + path(CONFIG_FILE)->spew_utf8($config_contents); +} +1; diff --git a/lib/DoctorKarma/DAO/User.pm b/lib/DoctorKarma/DAO/User.pm new file mode 100644 index 0000000..048ed6b --- /dev/null +++ b/lib/DoctorKarma/DAO/User.pm @@ -0,0 +1,240 @@ +package DoctorKarma::DAO::User; + +use v5.30.0; + +use strict; +use warnings; + +use Types::Standard qw/Str Int InstanceOf ArrayRef Maybe HasMethods/; +use Params::ValidationCompiler qw(validation_for); + +{ + my $validator = validation_for( + params => { + dbh => { type => HasMethods [ 'selectrow_hashref', 'do' ] } + } + ); + + sub new { + my $class = shift; + my %params = $validator->(@_); + my $self = bless {}, $class; + $self->{dbh} = $params{dbh}; + return $self; + } +} + +sub _logger { + my $self = shift; + if ( !defined $self->{logger} ) { + $self->{logger} = DoctorKarma::Logger->new; + } + return $self->{logger}; +} + +sub _db { + my $self = shift; + return $self->{dbh}; +} + +{ + my $validator = validation_for( + params => { + user => { type => InstanceOf ['DoctorKarma::Model::User'] } + } + ); + + sub store { + my $self = shift; + my %params = $validator->(@_); + + my $user = $params{user}; + my $db = $self->_db; + my $logger = $self->_logger; + + my $insert = <<'EOF'; +INSERT INTO users (id, first_name, username, karma) VALUES (?, ?, ?, 0); +EOF + my $username = "\@@{[$user->username]}:" // ''; + my $user_id = $user->id_user; + my $success = 0 + + $db->do( $insert, {}, $user->id_user, $user->username, $user->karma ); + if ($success) { + $logger->log_info("${username}${user_id} registered."); + return 1; + } + } +} + +{ + my $validator = validation_for( + params => { + user => { type => InstanceOf ['DoctorKarma::Model::User'] }, + first_name => { type => Str }, + } + ); + + sub update_firstname { + my $self = shift; + my %params = $validator->(@_); + + my $user = $params{user}; + my $first_name = $params{first_name}; + + my $db = $self->_db; + my $logger = $self->_logger; + + my $success = 0 + $db->do( <<'EOF', {}, $first_name, $user->id_user ); +UPDATE users SET first_name = ? WHERE id = ?; +EOF + if ($success) { + my $old_first_name = $user->first_name // 'NULL'; + $logger->log_info(<<"EOF"); +Updated first_name for id @{[$user->id_user]} +From: ${old_first_name} -> ${first_name}. +EOF + return 1; + } + } +} + +{ + my $validator = validation_for( + params => { + user => { type => InstanceOf ['DoctorKarma::Model::User'] }, + } + ); + + sub add_1_karma { + my $self = shift; + my %params = $validator->(@_); + + my $user = $params{user}; + + my $db = $self->_db; + my $logger = $self->_logger; + + $db->do( <<'EOF', {}, $user->id_user ); +UPDATE users SET karma=karma+1 WHERE id = ?; +EOF + my $user_with_new_karma = $self->recover_id( id => $user->id_user ); + $user->karma( $user_with_new_karma->karma ); + $logger->log_info( 'User ' + . $user->first_name . ':' + . $user->username . ':' + . $user->id_user + . ' has now ' + . $user->karma + . ' of karma.' ); + } +} + +{ + my $validator = validation_for( + params => { + user => { type => InstanceOf ['DoctorKarma::Model::User'] }, + username => { type => Maybe [Str] }, + } + ); + + sub update_username { + my $self = shift; + my %params = $validator->(@_); + + my $user = $params{user}; + my $username = $params{username}; + + my $db = $self->_db; + my $logger = $self->_logger; + + my $success = 0 + $db->do( <<'EOF', {}, $username, $user->id_user ); +UPDATE users SET username = ? WHERE id = ?; +EOF + if ($success) { + my $old_username = $user->username ? '@' . $user->username : 'NULL'; + $username = $username ? "\@$username" : 'NULL'; + $logger->log_info(<<"EOF"); +Updated username for id @{[$user->id_user]} +From: ${old_username} -> ${username}. +EOF + return 1; + } + } +} + +{ + my $validator = validation_for( + params => { + id => { type => Int }, + } + ); + + sub recover_id { + my $self = shift; + my %params = $validator->(@_); + + my $user_id = $params{id}; + my $query = <<'EOF'; +SELECT id as id_user, first_name, username, karma, last_karma_given_date +FROM users +WHERE id = ?; +EOF + return $self->_recover_by_query( + query => $query, + arguments => [$user_id] + ); + } +} + +{ + my $validator = validation_for( + params => { + username => { type => Str }, + } + ); + + sub recover_username { + my $self = shift; + my %params = $validator->(@_); + + my $username = $params{username}; + my $query = <<'EOF'; +SELECT id as user_id, first_name, username, karma, last_karma_given_date +FROM users +WHERE username = ?; +EOF + return $self->_recover_by_query( + query => $query, + arguments => [$username] + ); + } +} + +{ + my $validator = validation_for( + params => { + query => { type => Str }, + arguments => { type => ArrayRef }, + } + ); + + sub _recover_by_query { + my $self = shift; + my %params = $validator->(@_); + + my $query = $params{query}; + my $arguments = $params{arguments}; + + my $db = $self->_db; + my $user_db = $db->selectrow_hashref( $query, {}, @$arguments ); + + if ( defined $user_db ) { + for my $key_field ( keys %$user_db ) { + $user_db->{$key_field} // delete $user_db->{$key_field}; + } + return DoctorKarma::Model::User->new(%$user_db); + } + return undef; + } +} +1 diff --git a/lib/DoctorKarma/DB.pm b/lib/DoctorKarma/DB.pm new file mode 100644 index 0000000..59458a0 --- /dev/null +++ b/lib/DoctorKarma/DB.pm @@ -0,0 +1,70 @@ +package DoctorKarma::DB; + +use v5.30.0; + +use strict; +use warnings; + +use DBI; +use Const::Fast; + +use DoctorKarma::Config; + +const my $dbname => "@{[DoctorKarma::Config::CONFIG_DIR()]}/database.sqlite"; + +my @migrations = ( + 'CREATE TABLE options ( + key TEXT PRIMARY KEY, + value TEXT + );', + 'CREATE TABLE users ( + id INTEGER PRIMARY KEY, + username TEXT, + karma INTEGER, + last_karma_given_date TEXT + )', + 'ALTER TABLE users + ADD COLUMN first_name TEXT', +); + +sub dbh { + my $dbh = DBI->connect("dbi:SQLite:dbname=$dbname", '', '' , { + AutoCommit => 1, + RaiseError => 1, + }); + state $migrations_run = 0; + if (!$migrations_run) { + run_migrations($dbh); + $migrations_run = 1; + } + return $dbh; +} + +sub run_migrations { + my $dbh = shift; + my $current_migration = _get_current_migration_number($dbh); + say $current_migration; + if ($current_migration < scalar @migrations) { + my @needed_migrations = @migrations[$current_migration .. $#migrations]; + for my $migration (@needed_migrations) { + $dbh->do($migration); + if (!(0+$dbh->do('UPDATE options SET value = ? WHERE key = "migration"', undef, ++$current_migration))) { + $dbh->do('INSERT INTO options (key, value) VALUES ("migration", ?)', undef, $current_migration); + } + } + } +} + +sub _get_current_migration_number { + my $dbh = shift; + local $dbh->{RaiseError} = 0; + my $migration = $dbh->selectrow_hashref(<<'EOF', {}); +SELECT value FROM options WHERE key = 'migration' +EOF + my $value = 0; + if (defined $migration) { + $value = $migration->{value}; + } + return $value; +} +1; diff --git a/lib/DoctorKarma/Logger.pm b/lib/DoctorKarma/Logger.pm new file mode 100644 index 0000000..d135b1a --- /dev/null +++ b/lib/DoctorKarma/Logger.pm @@ -0,0 +1,39 @@ +package DoctorKarma::Logger; + +use Carp; +use Sys::Syslog; + +sub new { + my $class = shift; + my $self = bless {}, $class; + return $self; +} + +sub _log { + my $self = shift; + my $level = shift; + my $message = shift; + my $critical = shift; + + openlog ('DoctorKarma', $critical ? '': 'perror', 'user'); + syslog ($level, $message); + closelog(); +} + +sub log_error { + my $self = shift; + $self->_log (LOG_ERR, shift); +} + +sub log_critical { + my $self = shift; + my $error = shift; + $self->_log (LOG_ERR, $error, 1); + confess $error; +} + +sub log_info { + my $self = shift; + $self->_log (LOG_INFO, shift); +} +1; diff --git a/lib/DoctorKarma/Model/User.pm b/lib/DoctorKarma/Model/User.pm new file mode 100644 index 0000000..25bb6e7 --- /dev/null +++ b/lib/DoctorKarma/Model/User.pm @@ -0,0 +1,79 @@ +package DoctorKarma::Model::User; + +use v5.30.0; + +use strict; +use warnings; + +use Types::Standard qw/Str Int InstanceOf/; +use Params::ValidationCompiler qw(validation_for); + +{ + my $validator = validation_for( + params => { + id_user => { type => Int }, + username => { type => Str, optional => 1 }, + first_name => { type => Str, optional => 1 }, + karma => { type => Int }, + last_karma_given_date => { type => InstanceOf ['DateTime'], optional => 1 }, + } + ); + + sub new { + my $class = shift; + my %params = $validator->(@_); + my $self = bless \%params, $class; + return $self; + } +} + + +sub first_name { + my $self = shift; + if (exists $self->{first_name}) { + return $self->{first_name}; + } + return; +} + +sub id_user { + my $self = shift; + return $self->{id_user}; +} + +{ + my $validator = + validation_for( params => [ { type => Str, optional => 1 } ] ); + + sub username { + my $self = shift; + my ($new_username) = $validator->(@_); + if ( defined $new_username ) { + $self->{username} = $new_username; + } + if ( exists $self->{username} ) { + return $self->{username}; + } + return; + } +} + +sub last_karma_given_date { + my $self = shift; + return $self->{last_karma_given_date}; +} + +{ + my $validator = + validation_for( params => [ { type => Int, optional => 1 } ] ); + + sub karma { + my $self = shift; + my ($new_karma) = $validator->(@_); + if ( defined $new_karma ) { + $self->{new_karma} = $new_karma; + } + return $self->{karma}; + } +} +1; diff --git a/lib/DoctorKarma/Telegram.pm b/lib/DoctorKarma/Telegram.pm new file mode 100644 index 0000000..ff74e36 --- /dev/null +++ b/lib/DoctorKarma/Telegram.pm @@ -0,0 +1,130 @@ +package DoctorKarma::Telegram; + +use v5.30.0; + +use strict; +use warnings; + +use Types::Standard qw/Str Int Ref/; +use Params::ValidationCompiler qw(validation_for); + +use Mojo::UserAgent; +use JSON; + +use DoctorKarma::Logger; + +{ + my $validator = validation_for( + params => { + telegram_token => { type => Str }, + } + ); + + sub new { + my $class = shift; + my $self = bless {}, $class; + my %params = $validator->(@_); + my $telegram_token = $params{telegram_token}; + $self->{telegram_token} = $telegram_token; + return $self; + } +} + +sub _logger { + my $self = shift; + if (!defined $self->{logger}) { + $self->{logger} = DoctorKarma::Logger->new; + } + return $self->{logger}; +} + +sub _user_agent { + my $self = shift; + if ( !defined $self->{user_agent} ) { + $self->{user_agent} = Mojo::UserAgent->new; + } + return $self->{user_agent}; +} + +{ + my $validator = validation_for( + params => { + method => { type => Str }, + body => { type => Ref }, + } + ); + + sub _request { + my $self = shift; + my %params = $validator->(@_); + my $method = $params{method}; + my $body = $params{body}; + my $ua = $self->_user_agent; + my $logger = $self->_logger; + my $url = $self->_generate_url( method => $method ); + my $response = decode_json( + $ua->post( $url => {} => json => $body )->result->body ); + unless ($response->{ok}) { + $logger->log_critical($response->{description}); + } + return $response; + } +} + +sub get_updates { + my $self = shift; + my $last_update = $self->_last_update; + if ( !defined $last_update ) { + $last_update = 0; + } + my $response = $self->_request( + method => q/getUpdates/, + body => { offset => $last_update + 1 } + ); + if ( scalar $response->{result}->@* ) { + $last_update = $response->{result}[-1]{update_id}; + $self->_set_last_update($last_update); + } + return $response->{result}; +} + +{ + my $validator = validation_for( + params => { + method => { type => Str }, + } + ); + + sub _generate_url { + my $self = shift; + my %params = $validator->(@_); + my $method = $params{method}; + my $telegram_token = $self->_telegram_token; + my $url = qq(https://api.telegram.org/bot$telegram_token/$method); + return $url; + } +} + +{ + my $validator = validation_for( params => [ { type => Int } ] ); + + sub _set_last_update { + my $self = shift; + my ($last_update) = $validator->(@_); + $self->{last_update} = $last_update; + } +} + +sub _last_update { + my $self = shift; + if ( !exists $self->{last_update} ) { + return undef; + } + return $self->{last_update}; +} + +sub _telegram_token { + my $self = shift; + return $self->{telegram_token}; +} +1;