First karma working achieved.

This commit is contained in:
sergiotarxz 2022-02-07 00:30:41 +01:00
parent 1cc0432e97
commit 06f832d8eb
8 changed files with 764 additions and 29 deletions

View File

@ -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");
}
}
}

118
bin/doctor_karma.pl Executable file
View File

@ -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
);
}
}

88
lib/DoctorKarma/Config.pm Normal file
View File

@ -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;

240
lib/DoctorKarma/DAO/User.pm Normal file
View File

@ -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

70
lib/DoctorKarma/DB.pm Normal file
View File

@ -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;

39
lib/DoctorKarma/Logger.pm Normal file
View File

@ -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;

View File

@ -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;

130
lib/DoctorKarma/Telegram.pm Normal file
View File

@ -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;