about summary refs log blame commit diff stats
path: root/ara.pl
blob: 50bc4cf6659bf757b1e945384fa9554a88fa5b35 (plain) (tree)
1
2
3
4
5
6
7
8
9



               
 


                        
               
               
                 
                     
                               

                                    


                                                                 
 
                                            







                                           
 


                         
                         
                                        

 
                                                                           

                                                                         
 

                                    






                                                      
                                                                 

                                                                 





                                     



                                
                                
                                
                            
                            
                              
                            

                                                                      
                              
                                      
                                             
 

                            

                                                


                      



                                                    








                                                   


                                                                      




                                                                     


                       




                                                                   
                                             

                                                                          
 

                                                            

                                                                  



                   


                                                                      
  
 

                                                                      
                                        
                                   
                                        

 
                                 
               
 
                                 

                                      
                                                                
        
                            

                                        

                              



                                                                     



                                                                       

                         
                                                             

                                     
 

                                                       
 
                                                     
 

                                                 
 




                                   
        
                                         



                                        


                                        
                                                     
 
                         


                                                                        







                                                                      
                                                                 







                                                                  
 



                                                                      
 
 



                                                                
 






                                                                   
                                        
 

                             
 


                                        

                                                                      
                              


                                          
                                                                   







                                                                      
 
                                        
                              
 
                             

                                                                









                                                                    

                           

                                                      
                                   

                                                                          
                                       

                                                                         
                                                                     
                







                                                                      

         


                                                                           
 



                                                                      


                                                            
                                                                   

                                                     
                                                    
                                                  
                                                       
                                                    
                                                
                                                
                                                    
                    
                                                              


                                                                   

                                                   
                                                    
                                                 
                                                     
                                                    
                                                
                                              
                                                    
                    
                                                              


                                                             

                                              
                                                 
                                            
                                                
                                                 
                                            
                                         
                                                 
                    
                                                        
             

         


                                                             

                                                                            




                                                                    
     
                    

 
                     








                                                                
        
                                                  
                         
 
#!/usr/bin/perl

use strict;
use warnings;

use lib::relative 'lib';
use StateNotes;

use Term::Size;
use Path::Tiny;
use Time::Moment;
use Text::ASCIITable;
use Number::Format::SouthAsian;
use Getopt::Long qw( GetOptions );
use JSON::MaybeXS qw( decode_json );
use Term::ANSIColor qw( :pushpop colored );

local $SIG{__WARN__} = sub { print colored( $_[0], 'yellow' ); };

use constant is_OpenBSD => $^O eq "openbsd";
require OpenBSD::Unveil
    if is_OpenBSD;
sub unveil {
    if (is_OpenBSD) {
        return OpenBSD::Unveil::unveil(@_);
    } else {
        return 1;
    }
}

# Unveil @INC.
foreach my $path (@INC) {
    unveil( $path, 'rx' )
        or die "Unable to unveil: $!\n";
}

my ( $use_local_file, $get_latest, $state_notes, $rows_to_print, $no_delta,
     $no_total, @to_hide, %hide, @to_show, %show, $no_words, $show_delta,
     $auto_hide );

sub HelpMessage {
    print LOCALCOLOR GREEN "Options:
    --local     Use local data
    --latest    Fetch latest data
    --notes     Print State Notes
    --rows=i    Number of rows to print (i is Integer)
    --showdelta Print delta values for all rows
    --nodelta   Don't print changes in values
    --nowords   Don't format numbers with words
    --autohide  Automatically hide columns according to term size
    --hide      Hide states, columns from table (space seperated)
    --show      Show only these states (space seperated)";
    print LOCALCOLOR CYAN "
    --help    Print this help message
";
    exit;
}

GetOptions(
    "local" => \$use_local_file,
    "latest" => \$get_latest,
    "notes" => \$state_notes,
    "rows=i" => \$rows_to_print,
    "showdelta" => \$show_delta,
    "nodelta" => \$no_delta,
    "nototal" => \$no_total,
    "autohide" => \$auto_hide,
    "nowords" => \$no_words,
    "hide=s{1,}" => \@to_hide, # Getopt::Long docs say that this is an
                               # experimental feature with a warning.
    "show=s{1,}" => \@to_show,
    "help|h" => sub { HelpMessage() },
) or die "Error in command line arguments\n";

if ( $use_local_file
         and $get_latest ) {
    warn "Cannot use --local & --latest together
Overriding --latest option\n";
    undef $get_latest;
}

# To not break --nototal we add "India" to @to_hide.
push @to_hide, "india"
    if $no_total;

if ( $auto_hide ) {
    my ( $t_columns, $t_rows ) = Term::Size::chars;

    push @to_hide, "updated" if $t_columns < 110;
    push @to_hide, "active" if $t_columns < 100;
    undef $show_delta if $t_columns < 80;
    $no_delta = 0 if $t_columns < 80;
}

# Creating %hide and undefining all %hash{@to_hide}, after this we
# check if %hash{@to_hide} exists with exists keyword. Read this as
# "undef these keys from the hash". https://perldoc.pl/perldata#Slices
undef @hide{ @to_hide }
    if scalar @to_hide; # Array can't be empty, will fail.
                        # Alternatively can do @hide{ @to_hide } = ()
                        # which will work even if @to_hide is empty.

undef @show{ @to_show }
    if scalar @to_show;

# Alias updated to last updated. This will allow user to just enter
# updated in hide option.
undef $hide{'last updated'}
    if exists $hide{updated};

# Warn when user tries to hide these columns.
warn "Cannot hide state column\n" if exists $hide{state};
warn "Cannot hide notes column\n" if exists $hide{notes} and $state_notes;

my $cache_dir = $ENV{XDG_CACHE_HOME} || "$ENV{HOME}/.cache";

# %unveil contains list of paths to unveil with their permissions.
my %unveil = (
    "/usr" => "rx",
    "/var" => "rx",
    "/etc" => "rx",
    "/dev" => "rx",
    # Unveil the whole cache directory because HTTP::Tiny fetches file
    # like ara.jsonXXXXXXXXXX where each 'X' is a random number.
    $cache_dir => "rwc",
);

# Unveil each path from %unveil. We use sort because otherwise keys is
# random order everytime.
foreach my $path ( sort keys %unveil ) {
    unveil( $path, $unveil{$path} )
        or die "Unable to unveil: $!\n";
}

my $file = "$cache_dir/ara.json";
my $file_mtime;

# If $file exists then get mtime.
if ( -e $file ) {
    my $file_stat = path($file)->stat;
    $file_mtime = Time::Moment->from_epoch( $file_stat->mtime );
} else {
    if ( $use_local_file ) {
        warn "File '$file' doesn't exist
Fetching latest...\n";
        undef $use_local_file;
    }
}

# Fetch latest data only if the local data is older than 8 minutes or
# if the file doesn't exist.
if ( not $use_local_file
         and ( not -e $file
               or $file_mtime < Time::Moment->now_utc->minus_minutes(8)
               or $get_latest ) ) {
    require HTTP::Simple;

    # Ignore a warning, next line would've printed a warning.
    no warnings 'once';
    $HTTP::Simple::UA->verify_SSL(1);

    # Fetch latest data from api.
    my $url = 'https://api.covid19india.org/data.json';

    my $status = HTTP::Simple::getstore($url, $file);

    die "Failed to fetch latest data\n"
        unless HTTP::Simple::is_success($status);
}

# Slurp api response to $file_data.
my $file_data = path($file)->slurp;

# Block further unveil calls.
unveil()
    or die "Unable to lock unveil: $!\n";

# Decode $file_data to $json_data.
my $json_data = decode_json($file_data);

# Get statewise information.
my $statewise = $json_data->{statewise};

my ( $covid_19_data, $notes_table, @months, $today );

unless ( $state_notes ) {
    # Map month number to Months.
    @months = qw( lol Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );

    my @columns;
    push @columns, 'State'; # User cannot hide state column.
    push @columns, 'Confirmed' unless exists $hide{confirmed};
    push @columns, 'Active' unless exists $hide{active};
    push @columns, 'Recovered' unless exists $hide{recovered};
    push @columns, 'Deaths' unless exists $hide{deaths};
    push @columns, 'Last Updated' unless exists $hide{'last updated'};

    $covid_19_data = Text::ASCIITable->new( { allowANSI => 1 } );
    $covid_19_data->setCols( @columns );

    my %alignment;
    $alignment{Confirmed} = "left" unless exists $hide{confirmed};
    $alignment{Recovered} = "left" unless exists $hide{recovered};
    $alignment{Deaths} = "left" unless exists $hide{deaths};

    $covid_19_data->alignCol( \%alignment );

    $today = Time::Moment
        ->now_utc
        ->plus_hours(5)
        ->plus_minutes(30); # Current time in 'Asia/Kolkata' TimeZone.
}

my %format_with_words;
%format_with_words = ( words => 1, decimals => 2 )
    unless $no_words;
my $fmt = Number::Format::SouthAsian->new( %format_with_words );

my $rows_printed = 0;
foreach my $i ( 0 ... scalar @$statewise - 1 ) {
    # $rows_printed is incremented at the end of this foreach loop.
    if ( $rows_to_print ) {
        last if $rows_printed == $rows_to_print;
    }

    my $state = $statewise->[$i]{state};

    $state = "India"
        if $state eq "Total";

    $state = "Unassigned"
        if $state eq "State Unassigned";

    # If user has asked to show specific states then forget about hide
    # option.
    if ( scalar keys %show ) {
        next
            unless exists $show{lc $state}
            or ( length $state > 16
                 and exists $show{lc $statewise->[$i]{statecode}});
    } else {
        next
            if exists $hide{lc $state}
            # User sees the statecode if length $state > 16 so we also
            # match against that.
            or ( length $state > 16
                 and exists $hide{lc $statewise->[$i]{statecode}});
    }

    $state = $statewise->[$i]{statecode}
        if length $state > 16;

    unless ( $state_notes ) {
        my $update_info;
        my $lastupdatedtime = $statewise->[$i]{lastupdatedtime};
        my $last_update_dmy;

        # Previously dates were in dd/mm/YYYY format, currently some
        # are in d/m/YYYY format. This block fixes issues with
        # d/m/YYYY format.
        if ( substr( $lastupdatedtime, 1, 1 ) eq "/" ) {
            $last_update_dmy = substr( $lastupdatedtime, 0, 9 );
        } else {
            $last_update_dmy = substr( $lastupdatedtime, 0, 10 );
        }

        # Add $update_info.
        if ( $last_update_dmy
                 eq $today->strftime( "%d/%m/%Y" ) ) {
            $update_info = "Today";
        } elsif ( $last_update_dmy
                      eq $today->minus_days(1)->strftime( "%d/%m/%Y" ) ) {
            $update_info = "Yesterday";
        } elsif ( $last_update_dmy
                      eq $today->plus_days(1)->strftime( "%d/%m/%Y" ) ) {
            $update_info = "Tomorrow"; # Hopefully we don't see this.
        } else {
            # Previously dates were in dd/mm/YYYY format, currently
            # some are in d/m/YYYY format. This block fixes issues
            # with d/m/YYYY format.
            $update_info = ( substr( $lastupdatedtime, 1, 1 ) eq "/" )
                ? ($months[substr( $lastupdatedtime, 2, 1 )] . " "
                   . substr( $lastupdatedtime, 0, 1 ))
                : ($months[substr( $lastupdatedtime, 3, 2 )] . " "
                   . substr( $lastupdatedtime, 0, 2 ));
        }

        my $confirmed = $fmt->format_number("$statewise->[$i]{confirmed}");
        my $recovered = $fmt->format_number("$statewise->[$i]{recovered}");
        my $deaths = $fmt->format_number("$statewise->[$i]{deaths}");

        # Add delta only if it was updated Today or if user has asked.
        if ( $show_delta
                 or ( $update_info eq "Today"
                      and not $no_delta ) ) {
            # Only delta number will get highlighted.
            $_ .= " " for ($confirmed, $recovered, $deaths);

            my $delta_confirmed = $statewise->[$i]{deltaconfirmed};
            if ( $delta_confirmed > 2000 ) {
                $confirmed .= LOCALCOLOR BOLD MAGENTA
                    sprintf "%+d", $delta_confirmed;
            } elsif ( $delta_confirmed > 1000  ) {
                $confirmed .= LOCALCOLOR BRIGHT_MAGENTA
                    sprintf "%+d", $delta_confirmed;
            } elsif ( $delta_confirmed > 500 ) {
                $confirmed .= LOCALCOLOR MAGENTA
                    sprintf "%+d", $delta_confirmed;
            } else {
                $confirmed .= sprintf "%+d", $delta_confirmed;
            }

            my $delta_recovered = $statewise->[$i]{deltarecovered};
            if ( $delta_recovered > 2000 ) {
                $recovered .= LOCALCOLOR BOLD GREEN
                    sprintf "%+d", $delta_recovered;
            } elsif ( $delta_recovered > 1000 ) {
                $recovered .= LOCALCOLOR BRIGHT_GREEN
                    sprintf "%+d", $delta_recovered;
            } elsif ( $delta_recovered > 500 ) {
                $recovered .= LOCALCOLOR GREEN
                    sprintf "%+d", $delta_recovered;
            } else {
                $recovered .= sprintf "%+d", $delta_recovered;
            }

            my $delta_deaths = $statewise->[$i]{deltadeaths};
            if ( $delta_deaths > 200 ) {
                $deaths .= LOCALCOLOR BOLD RED
                    sprintf "%+d", $delta_deaths;
            } elsif ( $delta_deaths > 50 ) {
                $deaths .= LOCALCOLOR BRIGHT_RED
                    sprintf "%+d", $delta_deaths;
            } elsif ( $delta_deaths > 25 ) {
                $deaths .= LOCALCOLOR RED
                    sprintf "%+d", $delta_deaths;
            } else {
                $deaths .= sprintf "%+d", $delta_deaths;
            }
        }

        my @row;
        push @row, $state;
        push @row, $confirmed unless exists $hide{confirmed};
        push @row, $fmt->format_number($statewise->[$i]{active}, words => 0)
            unless exists $hide{active};
        push @row, $recovered unless exists $hide{recovered};
        push @row, $deaths unless exists $hide{deaths};
        push @row, $update_info unless exists $hide{'last updated'};

        $covid_19_data->addRow( @row );
    }
    $rows_printed++;
}

if ( $state_notes ) {
    my ( $state_notes_table, $rows_in_table ) = StateNotes::get(
        $statewise,
        \%hide,
        \%show,
        $rows_to_print
    );

    die "No rows in table\n" unless $rows_in_table;
    print $state_notes_table;
} else {
    die "No rows in table\n" unless $rows_printed;
    print $covid_19_data;
}