Group
Extension

App-MonM-Notifier/lib/App/MonM/Notifier/Monotifier.pm

package App::MonM::Notifier::Monotifier;
use warnings;
use strict;
use utf8;

=encoding utf-8

=head1 NAME

App::MonM::Monotifier - extension for the monm notifications

=head1 VERSION

Version 1.00

=head1 SYNOPSIS

    # monotifier
    # monotifier show
    # monotifier show <ID>
    # monotifier remove <ID>
    # monotifier clean
    # monotifier truncate

=head1 DESCRIPTION

This is an extension for the monm notifications over different
communication channels

B<Note!> Before using the third-party database, please create the monotifier table

DDL example for MySQL:

    CREATE TABLE IF NOT EXISTS monotifier (
        `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE,
        `to` CHAR(255), -- Recipient name
        `channel` CHAR(255), -- Recipient channel
        `subject` TEXT, -- Message subject
        `message` TEXT, -- Message content (BASE64)
        `attributes` TEXT, -- Message attributes (JSON)
        `published` BIGINT(20), -- The publication time (unixtime)
        `scheduled` BIGINT(20), -- The scheduled time (unixtime)
        `expired` BIGINT(20), -- The expiration time (unixtime)
        `sent` BIGINT(20), -- The send time
        `attempt` INTEGER DEFAULT 0, -- Count of failed attempts
        `status` CHAR(32), -- Status of transaction
        `errcode` INT(11), -- Error code
        `errmsg` TEXT -- Error message
    );

Configuration example for MySQL:

    UseMonotifier yes
    <MoNotifier>
        DSN "DBI:mysql:database=monotifier;host=mysql.example.com"
        User username
        Password password
        Set RaiseError          0
        Set PrintError          0
        Set mysql_enable_utf8   1

        # Expires and timeout values
        Timeout 60
        MaxTime 300
        Expires 1M
    </MoNotifier>

=head1 INTERNAL METHODS

=over 4

=item B<again>

The CTK method for classes extension. For internal use only!

See L<CTK/again>

=item B<raise>

    return $app->raise("Red message");

Sends message to STDERR and returns 0

=item B<store>

    my $store = $app->store();

Returns store object

=back

=head1 HISTORY

See C<Changes> file

=head1 DEPENDENCIES

L<App::MonM>

=head1 TO DO

See C<TODO> file

=head1 SEE ALSO

L<App::MonM>

=head1 AUTHOR

Serż Minus (Sergey Lepenkov) L<https://www.serzik.com> E<lt>abalama@cpan.orgE<gt>

=head1 COPYRIGHT

Copyright (C) 1998-2022 D&D Corporation. All Rights Reserved

=head1 LICENSE

This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

See C<LICENSE> file and L<https://dev.perl.org/licenses/>

=cut

use vars qw/$VERSION/;
$VERSION = '1.00';

use feature qw/say/;

use Encode;
use Encode::Locale;

use File::stat qw//;
use Text::SimpleTable;

use CTK::ConfGenUtil;
use CTK::TFVals qw/ :ALL /;
use CTK::Util qw/dtf tz_diff variant_stf/;

use App::MonM::Const;
use App::MonM::Util qw/
        blue green red yellow cyan magenta gray
        yep nope skip wow
        getExpireOffset getTimeOffset explain explain
    /;

use App::MonM::Notifier::Store;

use parent qw/CTK::App/;

use constant {
    NODE_NAME       => 'notifier',
    NODE_NAME_ALIAS => 'monotifier',
    ROWS_LIMIT      => 1000,
    DATE_FORMAT     => '%YYYY-%MM-%DD %hh:%mm:%ss',
    TABLE_INFO  => [(
        [12,    'NAME'],
        [68,    'VALUE'],
    )],
};

sub again {
    my $self = shift;
       $self->SUPER::again(); # CTK::App again first!!

    # Store
    my $store_conf = hash($self->conf(NODE_NAME) || $self->conf(NODE_NAME_ALIAS));
    $store_conf->{expires} = getExpireOffset(lvalue($store_conf, "expires") || lvalue($store_conf, "expire") || 0);
    $store_conf->{maxtime} = getExpireOffset(lvalue($store_conf, "maxtime") || 0);
    my $store = App::MonM::Notifier::Store->new(%$store_conf);
    $self->{store} = $store;
    #print App::MonM::Util::explain($store);

    return $self; # CTK requires!
}
sub store {
    my $self = shift;
    return $self->{store};
}
sub raise {
    my $self = shift;
    say STDERR red(@_);
    return 0;
}

__PACKAGE__->register_handler(
    handler     => "info",
    description => "Show statistic information",
    code => sub {
### CODE:
    my ($self, $meta, @arguments) = @_;
    my $store = $self->store;

    # General info
    printf("Hostname            : %s\n", HOSTNAME);
    printf("Monotifier version  : %s\n", $self->VERSION);
    printf("Monotifier enabled  : %s\n", lvalue($self->config("usemonotifier")) ? green("Yes") : magenta("No"));
    printf("Date                : %s\n", _fdate());
    printf("Data dir            : %s\n", $self->datadir);
    printf("Config file         : %s\n", $self->configfile);
    printf("Config status       : %s\n", $self->conf("loadstatus") ? green("OK") : magenta("ERROR: not loaded"));
    $self->raise($self->configobj->error) if !$self->configobj->status and length($self->configobj->error);
    #$self->debug(explain($self->config)) if $self->conf("loadstatus") && $self->verbosemode;

    # DB status
    printf("DB DSN              : %s\n", $store->dsn);
    printf("DB status           : %s\n", $store->error ? red("ERROR") : green("OK"));
    my $db_is_ok = $store->error ? 0 : 1;
    if ($db_is_ok && $store->{file} && -e $store->{file}) {
        my $s = File::stat::stat($store->{file})->size;
        printf("DB file             : %s\n", $store->{file});
        printf("DB size             : %s\n", sprintf("%s (%d bytes)", _fbytes($s), $s));
        printf("DB modified         : %s\n", _fdate(File::stat::stat($store->{file})->mtime || 0));
    }
    $self->raise($store->error) unless $db_is_ok;

    return 1;
});

__PACKAGE__->register_handler(
    handler     => "show",
    description => "Show table data",
    code => sub {
### CODE:
    my ($self, $meta, @arguments) = @_;
    my $id = shift(@arguments) || 0;
    my $store = $self->store;
    return $self->raise($store->error) if $store->error;

    # Show message
    if ($id) {
        my %info = $store->getById($id);
        return $self->raise($store->error) if $store->error;
        return skip("No data") unless $info{id};

        # Table
        my $tbl_hdrs = TABLE_INFO;
           $tbl_hdrs->[1][0] = (SCREENWIDTH() - 19);
        my $tbl = Text::SimpleTable->new(@$tbl_hdrs);

        # Stash
        my $exp = $info{expired} || 0;
        $tbl->row("Id", $id);
        $tbl->row("To", $info{to} // '');
        $tbl->row("Channel", $info{channel} // '');
        $tbl->row("Subject", encode( locale => $info{subject} // '' ));
        $tbl->row("Status", $info{status} // '');
        $tbl->row("Published", $info{published} ? dtf(DATE_FORMAT, $info{published}) : '');
        $tbl->row("Scheduled", $info{scheduled} ? dtf(DATE_FORMAT, $info{scheduled}) : '');
        $tbl->row("Expired", dtf(DATE_FORMAT, $exp)) if $exp;
        $tbl->row("Sent", dtf(DATE_FORMAT, $info{sent})) if $info{sent};
        $tbl->row("Attempt", $info{attempt}) if $info{attempt};
        $tbl->row("Errcode", $info{errcode} // 0);
        $tbl->row("Errmsg", encode( locale => $info{errmsg} // '' ));
        $tbl->hr;
        $tbl->row("SUMMARY", ($exp < time) ? "EXPIRED" : $info{status} // '');
        say $tbl->draw();

        # Show attributes (dump)
        if ($self->verbosemode) {
            say "Attributes of channel:";
            print(explain($info{attributes}));
            print "\n";

            # Show message
            printf("%s BEGIN MESSAGE ~~~\n", "~" x (SCREENWIDTH()-18));
            say encode( locale => $info{message} // '' );
            printf("%s END MESSAGE ~~~\n", "~" x (SCREENWIDTH()-16));
        }
    } else {
        my @table = $store->getAll(ROWS_LIMIT);
        return $self->raise($store->error) if $store->error;

        # Check data
        my $n = scalar(@table) || 0;
        if ($n) {
            printf("Number of records: %d\n", $n);
        } else {
            return skip("No data");
        }

        # Table
        # `id`,`to`,`channel`,`subject`,`message`,`attributes`,`published`,     0-6
        # `scheduled`,`expired`,`sent`,`attempt`,`status`,`errcode`,`errmsg`    7-13
        my $tbl_hdrs = [(
            [5,     'ID'],
            [20,    'TO'],
            [20,    'CHANNEL'],
            [32,    'SUBJECT'],
            [8,     'STATUS'],
            [3,     'ERR'],
        )];
        my $tbl = Text::SimpleTable->new(@$tbl_hdrs);
        my @errors;
        foreach my $rec (sort {$a->[0] <=> $b->[0]} @table) {
            $tbl->row(
                $rec->[0] // 0, # id
                variant_stf($rec->[1] // '', 20), # to
                variant_stf($rec->[2] // '', 20), # channel
                variant_stf(encode( locale => $rec->[3] // '' ), 32), # subject
                $rec->[11] // '', # status
                $rec->[12] // 0, # errcode
            );
            push @errors, $rec->[13] if $rec->[12];
        }
        say $tbl->draw();
        if ($self->verbosemode && @errors) {
            foreach my $err (@errors) {
                say magenta($err);
            }
        }
    }

    return 1;
});

__PACKAGE__->register_handler(
    handler     => "remove",
    description => "Remove message by id",
    code => sub {
### CODE:
    my ($self, $meta, @arguments) = @_;
    my $store = $self->store;
    return $self->raise($store->error) if $store->error;
    my $id = shift(@arguments) || 0;
    return $self->raise("Incorrect id") unless $id;

    # Remove message by id
    return $self->raise($store->error) unless $store->delById($id);

    return 1;
});

__PACKAGE__->register_handler(
    handler     => "clean",
    description => "Remove incorrect messages",
    code => sub {
### CODE:
    my ($self, $meta, @arguments) = @_;
    my $store = $self->store;
    return $self->raise($store->error) if $store->error;

    return $self->raise($store->error) unless $store->cleanup();

    return 1;
});

__PACKAGE__->register_handler(
    handler     => "truncate",
    description => "Remove all messages (purge)",
    code => sub {
### CODE:
    my ($self, $meta, @arguments) = @_;
    my $store = $self->store;
    return $self->raise($store->error) if $store->error;

    # Remove messages
    return $self->raise($store->error) unless $store->purge();

    return 1;
});

# Private methods
sub _fbytes {
    my $n = int(shift);
    if ($n >= 1024 ** 3) {
        return sprintf "%.3g GB", $n / (1024 ** 3);
    } elsif ($n >= 1024 ** 2) {
        return sprintf "%.3g MB", $n / (1024.0 * 1024);
    } elsif ($n >= 1024) {
        return sprintf "%.3g KB", $n / 1024.0;
    } else {
        return "$n B";
    }
}
sub _fdate {
    my $d = shift || time;
    my $g = shift || 0;
    return "unknown" unless $d;
    return dtf(DATETIME_GMT_FORMAT, $d, 1) if $g;
    return dtf(DATETIME_FORMAT . " " . tz_diff(), $d);
}

1;

__END__


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.