forked from HPR/hpr-tools
		
	
		
			
	
	
		
			2344 lines
		
	
	
		
			77 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
		
		
			
		
	
	
			2344 lines
		
	
	
		
			77 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
| 
								 | 
							
								#!/usr/bin/env perl
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								#         FILE: process_comments
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								#        USAGE: ./process_comments [-help] [-docmentation|manpage] [-debug=N]
							 | 
						||
| 
								 | 
							
								#               [-[no]dry-run] [-verbose ...] [-[no]verify] [-[no]live]
							 | 
						||
| 
								 | 
							
								#               [-[no]json] [-config=FILE]
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Read new comments received as mail messages (or as JSON files)
							 | 
						||
| 
								 | 
							
								#               and perform moderation actions on them before possibly adding
							 | 
						||
| 
								 | 
							
								#               them to the HPR database.
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								#      OPTIONS: ---
							 | 
						||
| 
								 | 
							
								# REQUIREMENTS: ---
							 | 
						||
| 
								 | 
							
								#         BUGS: ---
							 | 
						||
| 
								 | 
							
								#        NOTES: 2023-12-23 enabled the experimental::try feature and removed
							 | 
						||
| 
								 | 
							
								#               the use of 'Try::Tiny'. Revert by removing "use feature 'try'"
							 | 
						||
| 
								 | 
							
								#               and 'no warnings "experimental::try"'. Remove the '($e)' from
							 | 
						||
| 
								 | 
							
								#               each 'catch' and end the 'catch' braces with a semicolon. Add
							 | 
						||
| 
								 | 
							
								#               back 'use Try::Tiny'.
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								#       AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
							 | 
						||
| 
								 | 
							
								#      VERSION: 0.3.3
							 | 
						||
| 
								 | 
							
								#      CREATED: 2017-09-04 18:06:49
							 | 
						||
| 
								 | 
							
								#     REVISION: 2024-01-10 13:02:51
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								use v5.36; # say and try/catch
							 | 
						||
| 
								 | 
							
								use feature 'try';
							 | 
						||
| 
								 | 
							
								use strict;
							 | 
						||
| 
								 | 
							
								use warnings;
							 | 
						||
| 
								 | 
							
								no warnings "experimental::try";
							 | 
						||
| 
								 | 
							
								use utf8;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								use Carp;
							 | 
						||
| 
								 | 
							
								use Getopt::Long;
							 | 
						||
| 
								 | 
							
								use Pod::Usage;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								use Config::General;
							 | 
						||
| 
								 | 
							
								use List::Util qw{max};
							 | 
						||
| 
								 | 
							
								use HTML::Parser;
							 | 
						||
| 
								 | 
							
								use HTML::Restrict;
							 | 
						||
| 
								 | 
							
								use HTML::Entities;
							 | 
						||
| 
								 | 
							
								use File::Find::Rule;
							 | 
						||
| 
								 | 
							
								use File::Copy;
							 | 
						||
| 
								 | 
							
								use File::Slurper 'read_text';
							 | 
						||
| 
								 | 
							
								#use IO::All;
							 | 
						||
| 
								 | 
							
								use JSON;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								use Mail::Address;
							 | 
						||
| 
								 | 
							
								use Mail::Field;
							 | 
						||
| 
								 | 
							
								use Mail::Internet;
							 | 
						||
| 
								 | 
							
								use MIME::Parser;
							 | 
						||
| 
								 | 
							
								use Encode qw(decode encode);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								use DateTime::Format::ISO8601;
							 | 
						||
| 
								 | 
							
								use IO::Prompter;
							 | 
						||
| 
								 | 
							
								use Template;
							 | 
						||
| 
								 | 
							
								use SQL::Abstract;
							 | 
						||
| 
								 | 
							
								use LWP::UserAgent;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#use TryCatch; # Failing 2020-05-31 due to a problem in Devel::Declare
							 | 
						||
| 
								 | 
							
								#use Try::Tiny; # Using in-built experimental try/catch
							 | 
						||
| 
								 | 
							
								use Log::Handler;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								use DBI;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								use Data::Dumper;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Version number (manually incremented)
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								our $VERSION = '0.3.3';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Script and directory names
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								( my $PROG = $0 ) =~ s|.*/||mx;
							 | 
						||
| 
								 | 
							
								( my $DIR  = $0 ) =~ s|/?[^/]*$||mx;
							 | 
						||
| 
								 | 
							
								$DIR = '.' unless $DIR;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# Declarations
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Constants and other declarations
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								my $basedir    = "$ENV{HOME}/HPR/Comment_system";
							 | 
						||
| 
								 | 
							
								my $configfile = "$basedir/.${PROG}.cfg";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my ( $dbh, $sth1, $h1 );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my ( @ignore, @files );
							 | 
						||
| 
								 | 
							
								my ($comment, %updates, $shortfile);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# The MIME part we're looking for
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								my $target_mimetype = 'application/json';
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# All of the keys in the JSON we're interested in (when checking and adding to
							 | 
						||
| 
								 | 
							
								# the database)
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								my @json_keys = (
							 | 
						||
| 
								 | 
							
								    "eps_id",              "comment_timestamp",
							 | 
						||
| 
								 | 
							
								    "comment_author_name", "comment_title",
							 | 
						||
| 
								 | 
							
								    "comment_text",
							 | 
						||
| 
								 | 
							
								);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Keys we need to clean
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								my @cleanable_keys = (
							 | 
						||
| 
								 | 
							
								    "comment_author_name", "comment_title",
							 | 
						||
| 
								 | 
							
								    "comment_text",
							 | 
						||
| 
								 | 
							
								);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Enable Unicode mode
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								binmode STDOUT, ":encoding(UTF-8)";
							 | 
						||
| 
								 | 
							
								binmode STDERR, ":encoding(UTF-8)";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# Options and arguments
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								my $DEF_DEBUG = 0;
							 | 
						||
| 
								 | 
							
								my $DEF_LIVE  = 0;                              # Gets edited on borg
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Process options
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								my %options;
							 | 
						||
| 
								 | 
							
								Options( \%options );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Default help
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								pod2usage( -msg => "$PROG version $VERSION\n", -verbose => 0, -exitval => 1 )
							 | 
						||
| 
								 | 
							
								    if ( $options{'help'} );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Full documentation if requested with -doc
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								pod2usage(
							 | 
						||
| 
								 | 
							
								    -msg       => "$PROG version $VERSION\n",
							 | 
						||
| 
								 | 
							
								    -verbose   => 2,
							 | 
						||
| 
								 | 
							
								    -exitval   => 1,
							 | 
						||
| 
								 | 
							
								    -noperldoc => 0,
							 | 
						||
| 
								 | 
							
								) if ( $options{'documentation'} );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Collect options
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEF_DEBUG );
							 | 
						||
| 
								 | 
							
								my $cfgfile
							 | 
						||
| 
								 | 
							
								    = ( defined( $options{config} ) ? $options{config} : $configfile );
							 | 
						||
| 
								 | 
							
								my $live     = ( defined( $options{live} )   ? $options{live}   : $DEF_LIVE );
							 | 
						||
| 
								 | 
							
								my $verify   = ( defined( $options{verify} ) ? $options{verify} : 0 );
							 | 
						||
| 
								 | 
							
								my $dry_run  = ( defined( $options{'dry-run'} ) ? $options{'dry-run'} : 0 );
							 | 
						||
| 
								 | 
							
								my $verbose  = ( defined( $options{verbose} )   ? $options{verbose}   : 0 );
							 | 
						||
| 
								 | 
							
								my $jsonmode = ( defined( $options{json} )      ? $options{json}      : 0 );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Report on the options if requested
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								if ($DEBUG > 0) {
							 | 
						||
| 
								 | 
							
								    _debug(1, 'Options');
							 | 
						||
| 
								 | 
							
								    _debug(1,
							 | 
						||
| 
								 | 
							
								        '-config=' . $cfgfile,
							 | 
						||
| 
								 | 
							
								        ($live eq 0 ? '-nolive ' : '-live'),
							 | 
						||
| 
								 | 
							
								        ($verify eq 0 ? '-noverify ' : '-verify'),
							 | 
						||
| 
								 | 
							
								        ($dry_run eq 0 ? '-nodry-run ' : '-dry-run'),
							 | 
						||
| 
								 | 
							
								        '-verbose=[' . $verbose . ']',
							 | 
						||
| 
								 | 
							
								        ($jsonmode eq 0 ? '-nojson ' : '-json'),
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								    _debug(1, '');
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Sanity checks
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								die "Unable to find configuration file: $cfgfile\n" unless ( -e $cfgfile );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# Configuration file - load data
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								my $conf = Config::General->new(
							 | 
						||
| 
								 | 
							
								    -ConfigFile      => $cfgfile,
							 | 
						||
| 
								 | 
							
								    -InterPolateVars => 1,
							 | 
						||
| 
								 | 
							
								    -InterPolateEnv  => 1,
							 | 
						||
| 
								 | 
							
								    -ExtendedAccess  => 1,
							 | 
						||
| 
								 | 
							
								);
							 | 
						||
| 
								 | 
							
								my %config = $conf->getall();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								_debug($DEBUG > 2, 'Configuration settings:', Dumper(\%config));
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my $logfile    = $config{settings}->{logfile};
							 | 
						||
| 
								 | 
							
								my $template   = $config{settings}->{template};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my $maildrop   = $config{settings}->{maildrop};
							 | 
						||
| 
								 | 
							
								my $processed  = $config{settings}->{processed};
							 | 
						||
| 
								 | 
							
								my $rejected   = $config{settings}->{rejected};
							 | 
						||
| 
								 | 
							
								my $banned     = $config{settings}->{banned};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my $jsondir    = $config{settings}->{jsondir};
							 | 
						||
| 
								 | 
							
								my $jprocessed = $config{settings}->{jprocessed};
							 | 
						||
| 
								 | 
							
								my $jrejected  = $config{settings}->{jrejected};
							 | 
						||
| 
								 | 
							
								my $jbanned    = $config{settings}->{jbanned};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my $callback_template = $config{settings}->{callback_template};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								my ($cb_user, $cb_password) = split(":",$config{cms}->{user});
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# Moderation responses and the arguments associated with them (from the
							 | 
						||
| 
								 | 
							
								# configuration data).
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# key => [ 3 element array ]
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# - key is the menu element from do_moderation
							 | 
						||
| 
								 | 
							
								# - the array contains:
							 | 
						||
| 
								 | 
							
								#       - text to be used in messages
							 | 
						||
| 
								 | 
							
								#       - anonymous subroutine which returns the directory into which to place
							 | 
						||
| 
								 | 
							
								#         the file; call it with the $jsonmode variable
							 | 
						||
| 
								 | 
							
								#       - action keyword to send back to the server
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								my %moderation_keys = (
							 | 
						||
| 
								 | 
							
								    'Approve' => [
							 | 
						||
| 
								 | 
							
								        'processed',
							 | 
						||
| 
								 | 
							
								        sub { ( $_[0] ? $jprocessed : $processed ) },
							 | 
						||
| 
								 | 
							
								        'approve'
							 | 
						||
| 
								 | 
							
								    ],
							 | 
						||
| 
								 | 
							
								    'Ban' => [
							 | 
						||
| 
								 | 
							
								        'banned',
							 | 
						||
| 
								 | 
							
								        sub { ( $_[0] ? $jbanned : $banned ) },
							 | 
						||
| 
								 | 
							
								        'block'
							 | 
						||
| 
								 | 
							
								    ],
							 | 
						||
| 
								 | 
							
								    'Reject' => [
							 | 
						||
| 
								 | 
							
								        'rejected',
							 | 
						||
| 
								 | 
							
								        sub { ( $_[0] ? $jrejected : $rejected ) },
							 | 
						||
| 
								 | 
							
								        'delete'
							 | 
						||
| 
								 | 
							
								        ],
							 | 
						||
| 
								 | 
							
								);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Make a RE from the keys
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								my $mkeys = join('|',keys(%moderation_keys));
							 | 
						||
| 
								 | 
							
								my $mkeys_re = qr{^$mkeys$};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# Connect to the database
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								my $dbhost = $config{database}->{host} // '127.0.0.1';
							 | 
						||
| 
								 | 
							
								my $dbport = $config{database}->{port} // 3306;
							 | 
						||
| 
								 | 
							
								my $dbname = $config{database}->{name};
							 | 
						||
| 
								 | 
							
								my $dbuser = $config{database}->{user};
							 | 
						||
| 
								 | 
							
								my $dbpwd  = $config{database}->{password};
							 | 
						||
| 
								 | 
							
								$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
							 | 
						||
| 
								 | 
							
								    $dbuser, $dbpwd, { AutoCommit => 1 } )
							 | 
						||
| 
								 | 
							
								    or croak $DBI::errstr;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#$dbh = DBI->connect( "DBI:MariaDB:database=$dbname;host=$dbhost;port=$dbport",
							 | 
						||
| 
								 | 
							
								#    $dbuser, $dbpwd, { AutoCommit => 1 } )
							 | 
						||
| 
								 | 
							
								#    or croak $DBI::errstr;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Enable client-side UTF8
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								$dbh->{mysqlenable_utf8} = 1;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Set the local timezone to UTC for this connection
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								$dbh->do("set time_zone = '+00:00'") or carp $dbh->errstr;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# Set up logging keeping the default log layout except for the date
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								my $log = Log::Handler->new();
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								$log->add(
							 | 
						||
| 
								 | 
							
								    file => {
							 | 
						||
| 
								 | 
							
								        timeformat => "%F %H:%M:%S (%s)",
							 | 
						||
| 
								 | 
							
								        filename   => $logfile,
							 | 
						||
| 
								 | 
							
								        maxlevel   => 7,
							 | 
						||
| 
								 | 
							
								        minlevel   => 0
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# If mode JSON is selected with -json option we'll check the defined 'json'
							 | 
						||
| 
								 | 
							
								# directory, otherwise we'll look for work in the maildrop.
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								if ($jsonmode) {
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Look for JSON files
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    @ignore = ( '*~', '.*.swp', 'banned', 'processed', 'rejected' );
							 | 
						||
| 
								 | 
							
								    my $jsonfileRE = qr{(\.(?i:json)|[^.]+)$};
							 | 
						||
| 
								 | 
							
								    #<<<
							 | 
						||
| 
								 | 
							
								    @files = File::Find::Rule
							 | 
						||
| 
								 | 
							
								        ->file()
							 | 
						||
| 
								 | 
							
								        ->name($jsonfileRE)
							 | 
						||
| 
								 | 
							
								        ->not( File::Find::Rule->new->name(@ignore) )
							 | 
						||
| 
								 | 
							
								        ->maxdepth(1)
							 | 
						||
| 
								 | 
							
								        ->in($jsondir);
							 | 
						||
| 
								 | 
							
								    #>>>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if ( $DEBUG > 1 ) {
							 | 
						||
| 
								 | 
							
								        _debug(1,
							 | 
						||
| 
								 | 
							
								            "Files found in $jsondir",
							 | 
						||
| 
								 | 
							
								            Dumper( \@files ),
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # There may be nothing there
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    die "No JSON files found; nothing to do\n" unless (@files);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Convert the JSON files into the %updates hash
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    do_JSON_process( \@files, \%updates, $log, $dry_run, $verbose );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								else {
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Look for mail files
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    @ignore = ( '*~', '.*.swp', 'banned', 'processed', 'rejected' );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my $mailfileRE = qr{(\.(?i:eml)|[^.]+)$};
							 | 
						||
| 
								 | 
							
								    #<<<
							 | 
						||
| 
								 | 
							
								    @files = File::Find::Rule
							 | 
						||
| 
								 | 
							
								        ->file()
							 | 
						||
| 
								 | 
							
								        ->name($mailfileRE)
							 | 
						||
| 
								 | 
							
								        ->not( File::Find::Rule->new->name(@ignore) )
							 | 
						||
| 
								 | 
							
								        ->maxdepth(1)
							 | 
						||
| 
								 | 
							
								        ->in($maildrop);
							 | 
						||
| 
								 | 
							
								    #>>>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if ( $DEBUG > 1 ) {
							 | 
						||
| 
								 | 
							
								        _debug(1,
							 | 
						||
| 
								 | 
							
								            "Files found in $maildrop",
							 | 
						||
| 
								 | 
							
								            Dumper( \@files ),
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # There may be nothing there
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    unless (@files) {
							 | 
						||
| 
								 | 
							
								        warn "No mail found; nothing to do\n";
							 | 
						||
| 
								 | 
							
								        exit;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Convert the mail messages into the %updates hash
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    do_mail_process( \@files, \%updates, $log, $dry_run, $verbose );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# Check that comments aren't already in the database
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								do_database_check( $dbh, \%updates, $log, $verify, $dry_run );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								#  Perform the moderation pass
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								do_moderation( $dbh, \%updates, $template, $log, $verify, $dry_run );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								# Dump the data structure if requested
							 | 
						||
| 
								 | 
							
								#
							 | 
						||
| 
								 | 
							
								_debug($DEBUG > 1, '%updates: ' . Dumper(\%updates));
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# For the comments which are valid and approved, add them to the database
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								print '-' x 80, "\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								do_update( $dbh, \%updates, $log, $dry_run, $verbose );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								# Walk the captured data reporting what happened (would happen) moving the
							 | 
						||
| 
								 | 
							
								# files to the appropriate places and telling the server to do things to the
							 | 
						||
| 
								 | 
							
								# originals (if appropriate)
							 | 
						||
| 
								 | 
							
								#-------------------------------------------------------------------------------
							 | 
						||
| 
								 | 
							
								foreach my $file ( sort_by_timestamp( \%updates ) ) {
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Deal with each file depending on the selected action (from the set
							 | 
						||
| 
								 | 
							
								    # 'Approve', 'Ban' and 'Reject')
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    if ( $updates{$file}->{action} =~ $mkeys_re ) {
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Get the elements we need for the function arguments below.
							 | 
						||
| 
								 | 
							
								        # NOTE: Apologies! This is obscure. The action key will be 'Approve',
							 | 
						||
| 
								 | 
							
								        # 'Ban' or 'Reject', and the lookup will return the name of the
							 | 
						||
| 
								 | 
							
								        # destination (for messages), an anonymous subroutine reference which determines
							 | 
						||
| 
								 | 
							
								        # the directory, and a keyword to send to the web server to dispose of
							 | 
						||
| 
								 | 
							
								        # the comment. The subref must be called with $jsonmode as the
							 | 
						||
| 
								 | 
							
								        # argument to get the destination which is different depending on
							 | 
						||
| 
								 | 
							
								        # whether we're in mail or JSON mode. This really needs to be made
							 | 
						||
| 
								 | 
							
								        # simpler and easier to understand when this code is revisited!
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        my ( $name, $destsub, $serverac )
							 | 
						||
| 
								 | 
							
								            = ( @{ $moderation_keys{ $updates{$file}->{action} } } );
							 | 
						||
| 
								 | 
							
								        my $dest = &$destsub($jsonmode);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Print moderation settings
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        _debug($DEBUG > 1,
							 | 
						||
| 
								 | 
							
								            '$name: ' . $name,
							 | 
						||
| 
								 | 
							
								            '$dest: ' . $dest,
							 | 
						||
| 
								 | 
							
								            '$serverac: ' . $serverac,
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Report what we're doing with the file and do it if appropriate
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        report_action( $updates{$file}, $name, $dest, $log,
							 | 
						||
| 
								 | 
							
								            $dry_run, $verbose );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Tell the server what to do, but only if this is the live instance
							 | 
						||
| 
								 | 
							
								        # and we're not in dry-run mode
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        if ($live) {
							 | 
						||
| 
								 | 
							
								            unless (
							 | 
						||
| 
								 | 
							
								                call_back(
							 | 
						||
| 
								 | 
							
								                    $callback_template, $updates{$file}->{JSON}->{key},
							 | 
						||
| 
								 | 
							
								                    $serverac, $cb_user, $cb_password, $log, $dry_run, $verbose
							 | 
						||
| 
								 | 
							
								                )
							 | 
						||
| 
								 | 
							
								                )
							 | 
						||
| 
								 | 
							
								            {
							 | 
						||
| 
								 | 
							
								                print "Server update failed\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        else {
							 | 
						||
| 
								 | 
							
								            unless ($dry_run) {
							 | 
						||
| 
								 | 
							
								                print "Not live; no server action taken\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    elsif ( $updates{$file}->{action} eq 'Ignore' ) {
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # The action here is 'Ignore' which means just leave it alone. Since
							 | 
						||
| 
								 | 
							
								        # only the moderator can choose this, or the failure to find the
							 | 
						||
| 
								 | 
							
								        # episode in the database, we know it's not a broken mail message or
							 | 
						||
| 
								 | 
							
								        # bad JSON.
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        if ( $verbose > 0 ) {
							 | 
						||
| 
								 | 
							
								            print "File '$file' ignored\n";
							 | 
						||
| 
								 | 
							
								            print_errors( $updates{$file}->{errors} );
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    elsif ( $updates{$file}->{skip} > 0 ) {
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # The comment was found to be already in the database so we are
							 | 
						||
| 
								 | 
							
								        # skipping it. We want to report this and move the file to the
							 | 
						||
| 
								 | 
							
								        # 'processed' directory.
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        $shortfile = $updates{$file}->{shortfile};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        unless ($dry_run) {
							 | 
						||
| 
								 | 
							
								            if ( $verbose > 0 ) {
							 | 
						||
| 
								 | 
							
								                print "Moving '$shortfile' to 'processed'\n";
							 | 
						||
| 
								 | 
							
								                print "Reason: comment is already in the database\n";
							 | 
						||
| 
								 | 
							
								                print_errors( $updates{$file}->{errors} );
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            $log->info("Moving $file to 'processed'");
							 | 
						||
| 
								 | 
							
								            unless ( moveFile( $file, ($jsonmode ? $jprocessed : $processed) ) ) {
							 | 
						||
| 
								 | 
							
								                warn "Unable to move $file\n";
							 | 
						||
| 
								 | 
							
								                $log->error("Failed to move $file");
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        else {
							 | 
						||
| 
								 | 
							
								            if ( $verbose > 0 ) {
							 | 
						||
| 
								 | 
							
								                print "Would move '$shortfile' to 'processed' (dry run)\n";
							 | 
						||
| 
								 | 
							
								                print "Reason: comment is already in the database\n";
							 | 
						||
| 
								 | 
							
								                print_errors( $updates{$file}->{errors} );
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    else {
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # We aborted this one early in the processing and validation, so it's
							 | 
						||
| 
								 | 
							
								        # really messed up. We're going to reject it outright with error
							 | 
						||
| 
								 | 
							
								        # messages. We can't tell the server because we don't have the
							 | 
						||
| 
								 | 
							
								        # key to do it.
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        $shortfile = $updates{$file}->{shortfile};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        unless ($dry_run) {
							 | 
						||
| 
								 | 
							
								            if ( $verbose > 0 ) {
							 | 
						||
| 
								 | 
							
								                print "Moving $shortfile to 'rejected'\n";
							 | 
						||
| 
								 | 
							
								                print "Reason: failed validation\n";
							 | 
						||
| 
								 | 
							
								                print_errors( $updates{$file}->{errors} );
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            $log->info("Moving $file to 'rejected'");
							 | 
						||
| 
								 | 
							
								            unless ( moveFile( $file, ($jsonmode ? $jrejected : $rejected) ) ) {
							 | 
						||
| 
								 | 
							
								                warn "Unable to move $file\n";
							 | 
						||
| 
								 | 
							
								                $log->error("Failed to move $file");
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        else {
							 | 
						||
| 
								 | 
							
								            if ( $verbose > 0 ) {
							 | 
						||
| 
								 | 
							
								                print "Would move $shortfile to 'rejected' (dry run)\n";
							 | 
						||
| 
								 | 
							
								                print "Reason: failed validation\n";
							 | 
						||
| 
								 | 
							
								                print_errors( $updates{$file}->{errors} );
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    print '.' x 80, "\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								exit;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: do_JSON_process
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Process any pending JSON files found in a directory search
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $files          arrayref containing the JSON files found
							 | 
						||
| 
								 | 
							
								#               $updates        hashref to contain the details collected from
							 | 
						||
| 
								 | 
							
								#                               the JSON files
							 | 
						||
| 
								 | 
							
								#               $log            log handle
							 | 
						||
| 
								 | 
							
								#               $dry_run        Boolean flagging dry-run mode
							 | 
						||
| 
								 | 
							
								#               $verbose        Integer level of verbosity
							 | 
						||
| 
								 | 
							
								#      RETURNS: Nothing
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: The files containing JSON comments have been found earlier and
							 | 
						||
| 
								 | 
							
								#               passed to this function. They are processed in sorted order.
							 | 
						||
| 
								 | 
							
								#               The JSON is decoded into a hash and is validated, and the
							 | 
						||
| 
								 | 
							
								#               elements cleaned up. The JSON is stored for later in the
							 | 
						||
| 
								 | 
							
								#               $updates structure. If all tests and checks are passed the
							 | 
						||
| 
								 | 
							
								#               entry for this file in the $updates structure is marked as
							 | 
						||
| 
								 | 
							
								#               valid. If anything fails the entry is marked as invalid for
							 | 
						||
| 
								 | 
							
								#               the next stages of processing.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub do_JSON_process {
							 | 
						||
| 
								 | 
							
								    my ( $files, $updates, $log, $dry_run, $verbose ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my ( $shortfile, $dt );
							 | 
						||
| 
								 | 
							
								    my $json = JSON->new;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Examine the files we found
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    foreach my $file ( sort(@$files) ) {
							 | 
						||
| 
								 | 
							
								        print ">> $file\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								        $log->notice("Processing $file") unless $dry_run;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Initialise for this file, marking it as invalid by default.
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        $updates->{$file}              = {};
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{valid}     = 0;
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{moderated} = 0;
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{errors}    = [];
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{action}    = '';
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{file}      = $file;       # For convenience
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{shortfile} = $shortfile = abbreviate_filename($file);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								       #
							 | 
						||
| 
								 | 
							
								       # Open the nominated file and load its contents into a scalar. Because
							 | 
						||
| 
								 | 
							
								       # 'read_text' does its own internal error handling we need try/catch to
							 | 
						||
| 
								 | 
							
								       # intercept it and provide our own.
							 | 
						||
| 
								 | 
							
								       #
							 | 
						||
| 
								 | 
							
								        my $json_text;
							 | 
						||
| 
								 | 
							
								        try {
							 | 
						||
| 
								 | 
							
								            $json_text = read_text($file);
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        catch ($e){
							 | 
						||
| 
								 | 
							
								            warn "Failed to read JSON file $file\n";
							 | 
						||
| 
								 | 
							
								            next;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Decode and process the JSON, returning FALSE if it fails so we get
							 | 
						||
| 
								 | 
							
								        # a log entry (unless it's dry run mode)
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        unless (
							 | 
						||
| 
								 | 
							
								            convert_JSON(
							 | 
						||
| 
								 | 
							
								                $json, $json_text, $updates, $file,
							 | 
						||
| 
								 | 
							
								                $log,  $dry_run,   $verbose
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								        {
							 | 
						||
| 
								 | 
							
								            $log->warning("JSON decode failure; aborting '$shortfile'")
							 | 
						||
| 
								 | 
							
								                unless $dry_run;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: do_mail_process
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Process any pending email found in a directory search
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $files          arrayref containing the mail files found
							 | 
						||
| 
								 | 
							
								#               $updates        hashref to contain the details collected from
							 | 
						||
| 
								 | 
							
								#                               the mail files
							 | 
						||
| 
								 | 
							
								#               $log            log handle
							 | 
						||
| 
								 | 
							
								#               $dry_run        Boolean flagging dry-run mode
							 | 
						||
| 
								 | 
							
								#               $verbose        Integer level of verbosity
							 | 
						||
| 
								 | 
							
								#      RETURNS: Nothing
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: The files containing email have been found earlier and passed
							 | 
						||
| 
								 | 
							
								#               to this function. They are processed in sorted order. Each
							 | 
						||
| 
								 | 
							
								#               file is parsed as a mail file and the various destination
							 | 
						||
| 
								 | 
							
								#               addresses collected. (TODO Check these against permitted
							 | 
						||
| 
								 | 
							
								#               addresses). Each message is expected to be in MIME format with
							 | 
						||
| 
								 | 
							
								#               an attachment, which is extracted. The target mimetype
							 | 
						||
| 
								 | 
							
								#               (application/json) is checked for (TODO This is a global and
							 | 
						||
| 
								 | 
							
								#               shouldn't be). The JSON it contains is decoded into a hash and
							 | 
						||
| 
								 | 
							
								#               is validated, and the elements cleaned up. The JSON is stored
							 | 
						||
| 
								 | 
							
								#               for later in the $updates structure. If all tests and checks
							 | 
						||
| 
								 | 
							
								#               are passed the entry for this file in the $updates structure
							 | 
						||
| 
								 | 
							
								#               is marked as valid. If anything fails the entry is marked as
							 | 
						||
| 
								 | 
							
								#               invalid for the next stages of processing.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub do_mail_process {
							 | 
						||
| 
								 | 
							
								    my ( $files, $updates, $log, $dry_run, $verbose ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my ( $shortfile, $dt );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Prepare for parsing mail and JSON
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    my $parser = MIME::Parser->new;
							 | 
						||
| 
								 | 
							
								    $parser->output_under("/tmp");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my $json = JSON->new;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Examine the files we found
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    foreach my $file ( sort(@$files) ) {
							 | 
						||
| 
								 | 
							
								        print ">> $file\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								        $log->notice("Processing $file") unless $dry_run;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Initialise for this file, marking it as invalid by default.
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        $updates->{$file} = {};
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{valid} = 0;
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{moderated} = 0;
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{errors} = [];
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{action} = '';
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{file} = $file;            # For convenience
							 | 
						||
| 
								 | 
							
								        $updates->{$file}->{shortfile} = $shortfile = abbreviate_filename($file);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Open the current file and load its contents into a Mail::Internet object
							 | 
						||
| 
								 | 
							
								        # TODO: Consider using IO::All
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        open( my $mfh, '<', $file )
							 | 
						||
| 
								 | 
							
								            or die "Failed to open input file '$file' : $!\n";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my $mi_obj = Mail::Internet->new($mfh);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        close($mfh)
							 | 
						||
| 
								 | 
							
								            or warn "Failed to close input file '$file' : $!\n";
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # This should be to comments@hackerpublicradio.org in one of these headers
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        my @addresses = (
							 | 
						||
| 
								 | 
							
								            Mail::Address->parse( $mi_obj->head->get('Resent-to') ),
							 | 
						||
| 
								 | 
							
								            Mail::Address->parse( $mi_obj->head->get('Resent-cc') )
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								        if ( !@addresses ) {
							 | 
						||
| 
								 | 
							
								            @addresses = (
							 | 
						||
| 
								 | 
							
								                Mail::Address->parse( $mi_obj->head->get('To') ),
							 | 
						||
| 
								 | 
							
								                Mail::Address->parse( $mi_obj->head->get('Cc') )
							 | 
						||
| 
								 | 
							
								            );
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Report the headers we're interested in if requested
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        if ($verbose > 1) {
							 | 
						||
| 
								 | 
							
								            foreach my $addr (@addresses) {
							 | 
						||
| 
								 | 
							
								                my $dest = lc( $addr->address );
							 | 
						||
| 
								 | 
							
								                print "To: $dest\n";
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            print '~ ' x 40,"\n";
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # TODO Check the message is actually for us
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        my ( $ct, $cte, @body, $body, $results );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Parse the message in the assumption it's MIME. Report any errors and
							 | 
						||
| 
								 | 
							
								        # warnings.
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        my $entity = $parser->parse_data( $mi_obj->as_string() );
							 | 
						||
| 
								 | 
							
								        unless ($entity) {
							 | 
						||
| 
								 | 
							
								            $results = $parser->results;
							 | 
						||
| 
								 | 
							
								            foreach my $pmsg ( $results->msgs ) {
							 | 
						||
| 
								 | 
							
								                print STDERR "$pmsg\n";
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # If we got multiple MIME parts then look for the first 'application/json'
							 | 
						||
| 
								 | 
							
								        # element and save it as the body we'll work on.
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        if ( scalar( $entity->parts ) > 0 ) {
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # MIME message
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            if ( $DEBUG > 1 ) {
							 | 
						||
| 
								 | 
							
								                _debug(1, 'Message is MIME with multiple parts');
							 | 
						||
| 
								 | 
							
								                foreach my $ent ( $entity->parts ) {
							 | 
						||
| 
								 | 
							
								                    _debug(1, "Type: " . $ent->mime_type);
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Get the first MIME part of the type we want and turn it into an
							 | 
						||
| 
								 | 
							
								            # array.
							 | 
						||
| 
								 | 
							
								            # TODO: Might it be better to make a string here since later we simply
							 | 
						||
| 
								 | 
							
								            # convert the array to a string again.
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            foreach my $ent ( $entity->parts ) {
							 | 
						||
| 
								 | 
							
								                if ( $ent->mime_type eq $target_mimetype ) {
							 | 
						||
| 
								 | 
							
								                    $ct = $ent->mime_type;
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                # FIXME: Using 'bodyhandle' is supposed to decode the entity
							 | 
						||
| 
								 | 
							
								                # body, but this is untested. Also, we have to use 'as_string'
							 | 
						||
| 
								 | 
							
								                # then split on "\n" and stick the newlines back on the array
							 | 
						||
| 
								 | 
							
								                # elements, which seems a bit weird. Have to monitor this to
							 | 
						||
| 
								 | 
							
								                # see how it behaves.
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                #@body = @{ $ent->body() };
							 | 
						||
| 
								 | 
							
								                    @body = map {"$_\n"}
							 | 
						||
| 
								 | 
							
								                        split( "\n", $ent->bodyhandle->as_string );
							 | 
						||
| 
								 | 
							
								                    _debug($DEBUG > 1,
							 | 
						||
| 
								 | 
							
								                        'Length of body = ' . scalar(@body) . ' lines'
							 | 
						||
| 
								 | 
							
								                    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                    #
							 | 
						||
| 
								 | 
							
								                    # If we got a non-empty body then exit the loop
							 | 
						||
| 
								 | 
							
								                    #
							 | 
						||
| 
								 | 
							
								                    last if @body;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # We found no suitable part so there's nothing to process here. We'll
							 | 
						||
| 
								 | 
							
								            # let the later phases detect this though
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            unless (@body) {
							 | 
						||
| 
								 | 
							
								                saveError( "MIME message has no valid '$target_mimetype' element",
							 | 
						||
| 
								 | 
							
								                    $updates->{$file}->{errors} , $log, $dry_run, $verbose );
							 | 
						||
| 
								 | 
							
								                next;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        else {
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # The message has no MIME parts
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            saveError( "Message has no attachment",
							 | 
						||
| 
								 | 
							
								                $updates->{$file}->{errors}, $log, $dry_run, $verbose );
							 | 
						||
| 
								 | 
							
								            next;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Display the body unless we're not very verbose
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        $body = join( "", @body );
							 | 
						||
| 
								 | 
							
								        print "$body\n" if $verbose > 2;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Decode and process the JSON, returning FALSE if it fails so we get
							 | 
						||
| 
								 | 
							
								        # a log entry (unless it's dry run mode)
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        unless (
							 | 
						||
| 
								 | 
							
								            convert_JSON(
							 | 
						||
| 
								 | 
							
								                $json, $body, $updates, $file,
							 | 
						||
| 
								 | 
							
								                $log,  $dry_run,   $verbose
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								            )
							 | 
						||
| 
								 | 
							
								        {
							 | 
						||
| 
								 | 
							
								            $log->warning("JSON decode failure; aborting '$shortfile'")
							 | 
						||
| 
								 | 
							
								                unless $dry_run;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    continue {
							 | 
						||
| 
								 | 
							
								        print '-' x 80, "\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: do_database_check
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Look to see if the collected comments are already in the
							 | 
						||
| 
								 | 
							
								#               database
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $dbh            database handle
							 | 
						||
| 
								 | 
							
								#               $updates        structure (hashref) holding collected
							 | 
						||
| 
								 | 
							
								#                               information about the files and comments in
							 | 
						||
| 
								 | 
							
								#                               them
							 | 
						||
| 
								 | 
							
								#               $log            log handle
							 | 
						||
| 
								 | 
							
								#               $verify         Boolean flagging the need to verify
							 | 
						||
| 
								 | 
							
								#               TODO: Is this right?
							 | 
						||
| 
								 | 
							
								#               $dry_run        Boolean flagging dry-run mode
							 | 
						||
| 
								 | 
							
								#      RETURNS: Nothing
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Looks for each comment in %$updates in the database. The
							 | 
						||
| 
								 | 
							
								#               search criteria are the comment timestamp (in database format)
							 | 
						||
| 
								 | 
							
								#               and the comment author name. The timestamp is expected to have
							 | 
						||
| 
								 | 
							
								#               been converted from full ISO format (2024-01-10T11:16:11Z) to
							 | 
						||
| 
								 | 
							
								#               the format MySQL likes (2024-01-10 11:16:11). the original
							 | 
						||
| 
								 | 
							
								#               date is in the element 'orig_timestamp'. If there is a match
							 | 
						||
| 
								 | 
							
								#               in the database then the comment is to be skipped, and we mark
							 | 
						||
| 
								 | 
							
								#               the element appropriately and display and log a message. If no
							 | 
						||
| 
								 | 
							
								#               match we can proceed without skipping;
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub do_database_check {
							 | 
						||
| 
								 | 
							
								    my ( $dbh, $updates, $log, $verify, $dry_run ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my ( $sth1, $h1, $shortfile, $message );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Look for a comment with details matching an incoming one. If found
							 | 
						||
| 
								 | 
							
								    # someone else has approved it so we need to skip it.
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    $sth1 = $dbh->prepare(
							 | 
						||
| 
								 | 
							
								        q{SELECT id FROM comments
							 | 
						||
| 
								 | 
							
								        WHERE comment_timestamp = ?
							 | 
						||
| 
								 | 
							
								        AND comment_author_name = ?}
							 | 
						||
| 
								 | 
							
								    ) or die $DBI::errstr;
							 | 
						||
| 
								 | 
							
								    if ( $dbh->err ) {
							 | 
						||
| 
								 | 
							
								        warn $dbh->errstr;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    foreach my $file ( sort_by_timestamp($updates) ) {
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Make a shortcut to the JSON part, and collect the short form of the
							 | 
						||
| 
								 | 
							
								        # filename.
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        my $ptr = $updates->{$file}->{JSON};
							 | 
						||
| 
								 | 
							
								        #my $errors = $updates->{$file}->{errors};
							 | 
						||
| 
								 | 
							
								        $shortfile = $updates{$file}->{shortfile};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Look for the comment in the database
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        $sth1->execute( $ptr->{comment_timestamp},
							 | 
						||
| 
								 | 
							
								            $ptr->{comment_author_name} );
							 | 
						||
| 
								 | 
							
								        if ( $dbh->err ) {
							 | 
						||
| 
								 | 
							
								            warn $dbh->errstr;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Did we find it?
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        if ( $h1 = $sth1->fetchrow_hashref() ) {
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Found in the database. Mark this comment to be skipped and save
							 | 
						||
| 
								 | 
							
								            # an error report
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            #$updates->{$file}->{valid} = 0;
							 | 
						||
| 
								 | 
							
								            $updates->{$file}->{skip} = 1;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            $message = "Comment in file '$shortfile' is already in the database";
							 | 
						||
| 
								 | 
							
								            print "$message\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								            $log->warn($message) unless $dry_run;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        else {
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Not found
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            $updates->{$file}->{skip} = 0;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: do_moderation
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Moderate all the valid comments in the 'updates' structure
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $dbh            database handle
							 | 
						||
| 
								 | 
							
								#               $updates        structure (hashref) holding collected
							 | 
						||
| 
								 | 
							
								#                               information about the files and comments in
							 | 
						||
| 
								 | 
							
								#                               them
							 | 
						||
| 
								 | 
							
								#               $tt_template    path to the template we're using
							 | 
						||
| 
								 | 
							
								#               $log            log handle
							 | 
						||
| 
								 | 
							
								#               $verify         Boolean flagging the need to verify
							 | 
						||
| 
								 | 
							
								#               $dry_run        Boolean flagging dry-run mode
							 | 
						||
| 
								 | 
							
								#      RETURNS: Nothing
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Performs the moderation pass on the accumulated comments in
							 | 
						||
| 
								 | 
							
								#               the $updates structure. This is a hash (referenced by
							 | 
						||
| 
								 | 
							
								#               $updates) containing the filename as the key. Each of these
							 | 
						||
| 
								 | 
							
								#               lead to a hashref containing the JSON data (in a hashref), an
							 | 
						||
| 
								 | 
							
								#               arrayref of error messages and various flags denoting whether
							 | 
						||
| 
								 | 
							
								#               the comment is valid, has been moderated and what action the
							 | 
						||
| 
								 | 
							
								#               moderator took.
							 | 
						||
| 
								 | 
							
								#               The flags and error messages are manipulated here depending on
							 | 
						||
| 
								 | 
							
								#               what choice is made during the moderation process.
							 | 
						||
| 
								 | 
							
								#               The contents of the JSON hash (or a subset thereof) and the
							 | 
						||
| 
								 | 
							
								#               data about the show returned from a database query are
							 | 
						||
| 
								 | 
							
								#               displayed for the moderator using a template, and a menu of
							 | 
						||
| 
								 | 
							
								#               'Approve', 'Ban', 'Reject', Ignore' is presented.
							 | 
						||
| 
								 | 
							
								#               On exit the structure referenced by $updates has been modified
							 | 
						||
| 
								 | 
							
								#               to signal what should be done with each of the comments.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: TODO: References globals and should not.
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub do_moderation {
							 | 
						||
| 
								 | 
							
								    my ( $dbh, $updates, $tt_template, $log, $verify, $dry_run ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my ( $sth1, $h1, $cdt, $age );
							 | 
						||
| 
								 | 
							
								    my $agelimit = 60 * 60 * 24;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # We'll get details of the show each comment is associated with using this
							 | 
						||
| 
								 | 
							
								    # query. It will be passed to the TT² template.
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    $sth1 = $dbh->prepare(
							 | 
						||
| 
								 | 
							
								        q{SELECT e.id, e.date, e.title, h.host FROM eps e
							 | 
						||
| 
								 | 
							
								        JOIN hosts h ON e.hostid = h.hostid
							 | 
						||
| 
								 | 
							
								        WHERE e.id = ?}
							 | 
						||
| 
								 | 
							
								    ) or die $DBI::errstr;
							 | 
						||
| 
								 | 
							
								    if ( $dbh->err ) {
							 | 
						||
| 
								 | 
							
								        warn $dbh->errstr;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # We'll display each comment with a TT² template
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    my $tt = Template->new(
							 | 
						||
| 
								 | 
							
								        {   ABSOLUTE     => 1,
							 | 
						||
| 
								 | 
							
								            ENCODING     => 'utf8',
							 | 
						||
| 
								 | 
							
								            INCLUDE_PATH => $basedir,
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								    my $vars = {};
							 | 
						||
| 
								 | 
							
								    my $choice;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Process the valid comments, and not marked as 'skip'
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    foreach my $file ( sort_by_timestamp($updates) ) {
							 | 
						||
| 
								 | 
							
								        if ( $updates->{$file}->{valid} > 0 && $updates->{$file}->{skip} == 0 ) {
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Make shortcuts
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            my $ptr = $updates->{$file}->{JSON};
							 | 
						||
| 
								 | 
							
								            my $errors = $updates->{$file}->{errors};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Check the age of the comment if we're verifying, and ignore if
							 | 
						||
| 
								 | 
							
								            # less than the age limit. The original timestamp should have been
							 | 
						||
| 
								 | 
							
								            # reserved even though we had to reformat for MySQL/MariaDB.
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            if ($verify) {
							 | 
						||
| 
								 | 
							
								                $cdt = DateTime::Format::ISO8601->parse_datetime(
							 | 
						||
| 
								 | 
							
								                    $ptr->{orig_timestamp}
							 | 
						||
| 
								 | 
							
								                );
							 | 
						||
| 
								 | 
							
								                $age = DateTime->now->epoch - $cdt->epoch;
							 | 
						||
| 
								 | 
							
								                if ($age < $agelimit) {
							 | 
						||
| 
								 | 
							
								                    print "Choice 'Ignore' auto-selected due to comment age\n";
							 | 
						||
| 
								 | 
							
								                    $updates->{$file}->{action} = 'Ignore';
							 | 
						||
| 
								 | 
							
								                    $log->info("Ignored comment in $file")
							 | 
						||
| 
								 | 
							
								                        unless $dry_run;
							 | 
						||
| 
								 | 
							
								                    $updates->{$file}->{moderated} = 0;
							 | 
						||
| 
								 | 
							
								                    next;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Get the show details from the database
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            $sth1->execute( $ptr->{eps_id} );
							 | 
						||
| 
								 | 
							
								            if ( $dbh->err ) {
							 | 
						||
| 
								 | 
							
								                warn $dbh->errstr;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Do we know this episode?
							 | 
						||
| 
								 | 
							
								            # NOTE: If the database we're using is the clone this might fail
							 | 
						||
| 
								 | 
							
								            # because it's out of step with the live one
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            if ( $h1 = $sth1->fetchrow_hashref() ) {
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                # Populate the variables for the template
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                $vars->{file}    = $file;
							 | 
						||
| 
								 | 
							
								                $vars->{db}      = $h1;
							 | 
						||
| 
								 | 
							
								                $vars->{comment} = $ptr;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                # Call the template and print the result
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                my $document;
							 | 
						||
| 
								 | 
							
								                $tt->process( $tt_template, $vars, \$document,
							 | 
						||
| 
								 | 
							
								                    { binmode => ':utf8' } )
							 | 
						||
| 
								 | 
							
								                    || die $tt->error(), "\n";
							 | 
						||
| 
								 | 
							
								                print $document;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                # Prompt for moderation. Note: these prompts are used to flag
							 | 
						||
| 
								 | 
							
								                # the choice in the %updates hash, so don't change them unless
							 | 
						||
| 
								 | 
							
								                # you also change them elsewhere.
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                $choice = prompt(
							 | 
						||
| 
								 | 
							
								                    'Choose...',
							 | 
						||
| 
								 | 
							
								                    -menu => [ 'Approve', 'Ban', 'Reject', 'Ignore' ],
							 | 
						||
| 
								 | 
							
								                    '>'
							 | 
						||
| 
								 | 
							
								                );
							 | 
						||
| 
								 | 
							
								                if ($choice) {
							 | 
						||
| 
								 | 
							
								                    print "Choice '$choice' selected\n";
							 | 
						||
| 
								 | 
							
								                    if ( $choice eq 'Approve' ) {
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{action} = $choice;
							 | 
						||
| 
								 | 
							
								                        $log->info("Approved comment in $file")
							 | 
						||
| 
								 | 
							
								                            unless $dry_run;
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{moderated} = 1;
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                    elsif ( $choice eq 'Ban' ) {
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{action} = $choice;
							 | 
						||
| 
								 | 
							
								                        saveError(
							 | 
						||
| 
								 | 
							
								                            "Comment in '$file' banned by the moderator",
							 | 
						||
| 
								 | 
							
								                            $errors,
							 | 
						||
| 
								 | 
							
								                            $log, $dry_run, 0
							 | 
						||
| 
								 | 
							
								                        );
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{valid}     = 0;
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{moderated} = 1;
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                    elsif ( $choice eq 'Reject' ) {
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{action} = $choice;
							 | 
						||
| 
								 | 
							
								                        saveError(
							 | 
						||
| 
								 | 
							
								                            "Comment in '$file' rejected by the moderator",
							 | 
						||
| 
								 | 
							
								                            $errors,
							 | 
						||
| 
								 | 
							
								                            $log, $dry_run, 0
							 | 
						||
| 
								 | 
							
								                        );
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{valid}     = 0;
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{moderated} = 1;
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                    elsif ( $choice eq 'Ignore' ) {
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{action} = $choice;
							 | 
						||
| 
								 | 
							
								                        $log->info("Ignored comment in $file")
							 | 
						||
| 
								 | 
							
								                            unless $dry_run;
							 | 
						||
| 
								 | 
							
								                        $updates->{$file}->{moderated} = 0;
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                else {
							 | 
						||
| 
								 | 
							
								                    print "\n";
							 | 
						||
| 
								 | 
							
								                    undef $choice; # Seem to need this after aborting 'prompt'
							 | 
						||
| 
								 | 
							
								                    print "Ignored this comment\n";
							 | 
						||
| 
								 | 
							
								                    $log->info("Ignored comment in $file (default action)")
							 | 
						||
| 
								 | 
							
								                        unless $dry_run;
							 | 
						||
| 
								 | 
							
								                    $updates->{$file}->{moderated} = 0;
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                #print "\n";
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            else {
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                # We couldn't find the episode in the database
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                printf "Episode %s is not known\n", $ptr->{eps_id};
							 | 
						||
| 
								 | 
							
								                $updates->{$file}->{action} = 'Ignore';
							 | 
						||
| 
								 | 
							
								                saveError(
							 | 
						||
| 
								 | 
							
								                    "Episode referenced in '$file' is not known",
							 | 
						||
| 
								 | 
							
								                    $errors,
							 | 
						||
| 
								 | 
							
								                    $log, $dry_run, 0
							 | 
						||
| 
								 | 
							
								                );
							 | 
						||
| 
								 | 
							
								                $updates->{$file}->{moderated} = 0;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: do_update
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Add valid, approved comments to the database (or pretend to do
							 | 
						||
| 
								 | 
							
								#               so if in dry-run mode)
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $dbh            database handle
							 | 
						||
| 
								 | 
							
								#               $updates        structure (hashref) holding collected
							 | 
						||
| 
								 | 
							
								#                               information about the files and comments in
							 | 
						||
| 
								 | 
							
								#                               them
							 | 
						||
| 
								 | 
							
								#               $log            log handle
							 | 
						||
| 
								 | 
							
								#               $dry_run        Boolean flagging dry-run mode
							 | 
						||
| 
								 | 
							
								#               $verbose        Integer level of verbosity
							 | 
						||
| 
								 | 
							
								#      RETURNS: Nothing
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Performs a database update for all valid approved comments in
							 | 
						||
| 
								 | 
							
								#               the structure referenced by $updates unless in dry-run mode
							 | 
						||
| 
								 | 
							
								#               where what would be done will be reported.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: TODO: References globals and should not.
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub do_update {
							 | 
						||
| 
								 | 
							
								    my ( $dbh, $updates, $log, $dry_run, $verbose ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my $sql = SQL::Abstract->new;
							 | 
						||
| 
								 | 
							
								    my ( $stmt, @bind, $show, $count );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    $count = 0;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    foreach my $file ( sort_by_timestamp($updates) ) {
							 | 
						||
| 
								 | 
							
								        if (   $updates->{$file}->{valid} > 0
							 | 
						||
| 
								 | 
							
								            && $updates->{$file}->{action} eq 'Approve' )
							 | 
						||
| 
								 | 
							
								        {
							 | 
						||
| 
								 | 
							
								            $count++;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            $show = $updates->{$file}->{JSON}->{eps_id};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            my $shortfile = $updates{$file}->{shortfile};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Build the statement
							 | 
						||
| 
								 | 
							
								            # NOTE: The JSON keys we wish to use match the column names in the
							 | 
						||
| 
								 | 
							
								            # 'comments' table. The second argument to 'insert' needs to be
							 | 
						||
| 
								 | 
							
								            # a hashref, so we use @json_keys to list the keys we need, with
							 | 
						||
| 
								 | 
							
								            # the whole expression returning an anonymous hash (hashref)
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            ( $stmt, @bind )
							 | 
						||
| 
								 | 
							
								                = $sql->insert( 'comments',
							 | 
						||
| 
								 | 
							
								                { %{ $updates->{$file}->{JSON} }{@json_keys} } );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Display the statement we constructed if requested
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            _debug($DEBUG > 1,
							 | 
						||
| 
								 | 
							
								                "$stmt\n'",
							 | 
						||
| 
								 | 
							
								                join( "','", @bind )
							 | 
						||
| 
								 | 
							
								            );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Take action or report potential changes
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            unless ($dry_run) {
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                # Apply the SQL
							 | 
						||
| 
								 | 
							
								                #
							 | 
						||
| 
								 | 
							
								                $dbh->do( $stmt, undef, @bind );
							 | 
						||
| 
								 | 
							
								                if ( $dbh->err ) {
							 | 
						||
| 
								 | 
							
								                    #
							 | 
						||
| 
								 | 
							
								                    # Something broke during the database update. Warn about
							 | 
						||
| 
								 | 
							
								                    # it, save the error and make the action 'Ignore' so the
							 | 
						||
| 
								 | 
							
								                    # file is kept for further investigation (or to be
							 | 
						||
| 
								 | 
							
								                    # edited). Marking the file as invalid may be overkill.
							 | 
						||
| 
								 | 
							
								                    #
							 | 
						||
| 
								 | 
							
								                    warn $dbh->errstr;
							 | 
						||
| 
								 | 
							
								                    saveError(
							 | 
						||
| 
								 | 
							
								                        $dbh->errstr,
							 | 
						||
| 
								 | 
							
								                        $updates{$file}->{errors},
							 | 
						||
| 
								 | 
							
								                        $log, $dry_run, $verbose
							 | 
						||
| 
								 | 
							
								                    );
							 | 
						||
| 
								 | 
							
								#                    $updates{$file}->{valid} = 0;
							 | 
						||
| 
								 | 
							
								                    $updates->{$file}->{action} = 'Ignore';
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								                else {
							 | 
						||
| 
								 | 
							
								                    $log->info("$file: inserted comment for show $show");
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								                    if ( $verbose > 0 ) {
							 | 
						||
| 
								 | 
							
								                        print "Updated show $show from file $shortfile\n";
							 | 
						||
| 
								 | 
							
								                    }
							 | 
						||
| 
								 | 
							
								                }
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            else {
							 | 
						||
| 
								 | 
							
								                print
							 | 
						||
| 
								 | 
							
								                    "Would have added comment for show $show from file $shortfile\n"
							 | 
						||
| 
								 | 
							
								                    if $verbose > 0;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if ($verbose > 0 && $count > 0) {
							 | 
						||
| 
								 | 
							
								        print "\n", '-' x 80, "\n";
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: convert_JSON
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Converts the JSON from a file or an email into an internal
							 | 
						||
| 
								 | 
							
								#               data structure
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $json           JSON object
							 | 
						||
| 
								 | 
							
								#               $jtext          JSON in text format
							 | 
						||
| 
								 | 
							
								#               $updates        hashref to contain the details collected from
							 | 
						||
| 
								 | 
							
								#                               the JSON and elsewhere
							 | 
						||
| 
								 | 
							
								#               $file           the file we're processing (hash key)
							 | 
						||
| 
								 | 
							
								#               $log            log handle
							 | 
						||
| 
								 | 
							
								#               $dry_run        Boolean flagging dry-run mode
							 | 
						||
| 
								 | 
							
								#               $verbose        Integer level of verbosity
							 | 
						||
| 
								 | 
							
								#      RETURNS: 
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: 
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub convert_JSON {
							 | 
						||
| 
								 | 
							
								    my ( $json, $jtext, $updates, $file, $log, $dry_run, $verbose ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my ($comment, $dt, $shortfile);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    $shortfile = $updates{$file}->{shortfile};
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Decode the JSON into a hashref
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    try {
							 | 
						||
| 
								 | 
							
								        $comment = $json->decode($jtext);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # If the JSON decode failed we have a problem
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    catch ($e){
							 | 
						||
| 
								 | 
							
								        saveError(
							 | 
						||
| 
								 | 
							
								            "JSON in file '$shortfile' failed to decode",
							 | 
						||
| 
								 | 
							
								            $updates->{$file}->{errors},
							 | 
						||
| 
								 | 
							
								            $log, $dry_run, $verbose
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								        return 0;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Validate the comment fields: do we have all we expected?
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    unless (validate_comment($comment)) {
							 | 
						||
| 
								 | 
							
								        saveError(
							 | 
						||
| 
								 | 
							
								            "JSON in file '$shortfile' failed to validate",
							 | 
						||
| 
								 | 
							
								            $updates->{$file}->{errors},
							 | 
						||
| 
								 | 
							
								            $log, $dry_run, $verbose
							 | 
						||
| 
								 | 
							
								        );
							 | 
						||
| 
								 | 
							
								        return 0;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Perform some cleaning
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    foreach my $key (@cleanable_keys) {
							 | 
						||
| 
								 | 
							
								        $comment->{$key} = clean_comment_field( $comment->{$key} );
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # ISO 8601 timestamps cause MySQL problems (or have started to) so we
							 | 
						||
| 
								 | 
							
								    # parse them and convert to an acceptable format. We keep the original
							 | 
						||
| 
								 | 
							
								    # format in $verify is set and we have to check its age.
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    try {
							 | 
						||
| 
								 | 
							
								        $dt = DateTime::Format::ISO8601->parse_datetime(
							 | 
						||
| 
								 | 
							
								            $comment->{comment_timestamp} );
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    catch ($e){
							 | 
						||
| 
								 | 
							
								        die "Failed to parse comment timestamp\n";
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    $comment->{orig_timestamp} = $comment->{comment_timestamp};
							 | 
						||
| 
								 | 
							
								    $comment->{comment_timestamp} = $dt->ymd . ' ' . $dt->hms;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Stash the JSON for later
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    $updates->{$file}->{JSON} = $comment;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # This file is valid we think
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    $updates->{$file}->{valid} = 1;
							 | 
						||
| 
								 | 
							
								    $log->info("$file seems valid") unless $dry_run;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return 1;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: report_action
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Does all of the necessary reporting as the email or JSON files
							 | 
						||
| 
								 | 
							
								#               are being acted upon
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $struct         hashref holding collected comment data for
							 | 
						||
| 
								 | 
							
								#                               a given mail file
							 | 
						||
| 
								 | 
							
								#               $destname       name of destination
							 | 
						||
| 
								 | 
							
								#               $dest           destination directory
							 | 
						||
| 
								 | 
							
								#               $log            log handle
							 | 
						||
| 
								 | 
							
								#               $dry_run        Boolean flagging dry-run mode
							 | 
						||
| 
								 | 
							
								#               $verbose        Integer level of verbosity
							 | 
						||
| 
								 | 
							
								#      RETURNS: Nothing
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: In dry run mode reports what would have been done along with
							 | 
						||
| 
								 | 
							
								#               any errors, but only in verbose mode. Otherwise reports the
							 | 
						||
| 
								 | 
							
								#               action (in verbose mode) and logs it too. It also moves the
							 | 
						||
| 
								 | 
							
								#               mail file to the requested directory.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub report_action {
							 | 
						||
| 
								 | 
							
								    my ( $struct, $destname, $dest, $log, $dry_run, $verbose ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Get values with a hashref slice
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    my ( $file, $shortfile ) = @$struct{ 'file', 'shortfile' };
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    unless ($dry_run) {
							 | 
						||
| 
								 | 
							
								        if ($verbose > 0) {
							 | 
						||
| 
								 | 
							
								            print "Moving '$shortfile' to '$destname'\n";
							 | 
						||
| 
								 | 
							
								            print_errors($struct->{$file}->{errors});
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        $log->info("Moving '$file' to '$destname'");
							 | 
						||
| 
								 | 
							
								        unless ( moveFile( $file, $dest ) ) {
							 | 
						||
| 
								 | 
							
								            warn "Unable to move $shortfile\n";
							 | 
						||
| 
								 | 
							
								            $log->error("Failed to move $shortfile");
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    else {
							 | 
						||
| 
								 | 
							
								        if ($verbose > 0) {
							 | 
						||
| 
								 | 
							
								            print "Would move '$shortfile' to '$destname' (dry run)\n";
							 | 
						||
| 
								 | 
							
								            print_errors($struct->{$file}->{errors});
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: call_back
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Invokes a PHP interface on the HPR server through which
							 | 
						||
| 
								 | 
							
								#               a comment can be marked as fully processed
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $format         sprintf template for building the URL
							 | 
						||
| 
								 | 
							
								#               $key            the key from the JSON
							 | 
						||
| 
								 | 
							
								#               $action         the action to be taken
							 | 
						||
| 
								 | 
							
								#               $user           the username from the configuration
							 | 
						||
| 
								 | 
							
								#               $pass           the password from the configuration
							 | 
						||
| 
								 | 
							
								#               $log            log handle
							 | 
						||
| 
								 | 
							
								#               $dry_run        Boolean flagging dry-run mode
							 | 
						||
| 
								 | 
							
								#               $verbose        Integer level of verbosity
							 | 
						||
| 
								 | 
							
								#      RETURNS: True (1) if the call performed as it should, otherwise false
							 | 
						||
| 
								 | 
							
								#               (0)
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Checks that the correct parameters have been supplied, then
							 | 
						||
| 
								 | 
							
								#               builds the URL we're going to use from the format string, key
							 | 
						||
| 
								 | 
							
								#               and action. If in dry-run mode this URL is reported and
							 | 
						||
| 
								 | 
							
								#               nothing more. Otherwise the URL is queried with an HTTP 'GET'.
							 | 
						||
| 
								 | 
							
								#               If the response shows success then the action is considered to
							 | 
						||
| 
								 | 
							
								#               have been taken. The returned message is displayed (in verbose
							 | 
						||
| 
								 | 
							
								#               mode) and success is logged. If the 'GET' failed then
							 | 
						||
| 
								 | 
							
								#               a warning message is generated along with a warning log entry.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub call_back {
							 | 
						||
| 
								 | 
							
								    my ( $format, $key, $action, $user, $pass, $log, $dry_run, $verbose ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my ( $ua, $url, $response, $code, $message, $content );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Check calling parameters
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    unless ($key) {
							 | 
						||
| 
								 | 
							
								        warn "Invalid call to 'call_back' subroutine; missing key\n";
							 | 
						||
| 
								 | 
							
								        return 0;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    unless ( $action =~ /^(approve|block|delete)$/ ) {
							 | 
						||
| 
								 | 
							
								        warn "Invalid call to 'call_back' subroutine; invalid action\n";
							 | 
						||
| 
								 | 
							
								        return 0;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Build the URL
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    $url = sprintf( $format, $key, $action );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Perform the call to clean up the JSON files, etc, or just say what would
							 | 
						||
| 
								 | 
							
								    # have been done
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    unless ($dry_run) {
							 | 
						||
| 
								 | 
							
								        print "Making the server call:\nGET $url\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $ua = LWP::UserAgent->new;
							 | 
						||
| 
								 | 
							
								        $ua->timeout(180);                  # Module default is 180 sec = 3 mins
							 | 
						||
| 
								 | 
							
								        $ua->credentials("hub.hackerpublicradio.org:443", "cms", $user, $pass);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        $response = $ua->get($url);
							 | 
						||
| 
								 | 
							
								        if ( $response->is_success ) {
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Server call succeeded
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            $code = $response->code;
							 | 
						||
| 
								 | 
							
								            $message = $response->message;
							 | 
						||
| 
								 | 
							
								            $content = $response->decoded_content;
							 | 
						||
| 
								 | 
							
								            if ($verbose > 0) {
							 | 
						||
| 
								 | 
							
								                print "Success: $code/$message\n";
							 | 
						||
| 
								 | 
							
								                print "Content: $content\n" if $content;
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            $log->info("Successful server message $key/$action: " .
							 | 
						||
| 
								 | 
							
								                $response->status_line);
							 | 
						||
| 
								 | 
							
								            return 1;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        else {
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            # Server call failed
							 | 
						||
| 
								 | 
							
								            #
							 | 
						||
| 
								 | 
							
								            warn $response->status_line . "\n";
							 | 
						||
| 
								 | 
							
								            $log->error("Failed server message $key/$action: " .
							 | 
						||
| 
								 | 
							
								                $response->status_line);
							 | 
						||
| 
								 | 
							
								            # Try to log as much as possible
							 | 
						||
| 
								 | 
							
								            $log->error( $response->decoded_content )
							 | 
						||
| 
								 | 
							
								                if $response->decoded_content;
							 | 
						||
| 
								 | 
							
								            return 0;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    else {
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        # Always a success in dry-run mode
							 | 
						||
| 
								 | 
							
								        #
							 | 
						||
| 
								 | 
							
								        print "Would have made the server call:\nGET $url\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								        return 1;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: print_errors
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Prints the contents of the error array if there's anything in
							 | 
						||
| 
								 | 
							
								#               it (i.e. not empty)
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $errors         arrayref possibly containing errors
							 | 
						||
| 
								 | 
							
								#      RETURNS: Nothing
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Simplifies printing in the case when there's nothing to print
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub print_errors {
							 | 
						||
| 
								 | 
							
								    my ($errors) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return unless defined($errors);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    print "Errors: ", join( "\n", @{$errors} ), "\n"
							 | 
						||
| 
								 | 
							
								        if @{$errors};
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: saveError
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Saves an error message in an error structure and logs it if in
							 | 
						||
| 
								 | 
							
								#               live mode
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $message        error message text
							 | 
						||
| 
								 | 
							
								#               $struct         structure (arrayref) to hold the text
							 | 
						||
| 
								 | 
							
								#               $log            log handle
							 | 
						||
| 
								 | 
							
								#               $dry_run        Boolean flagging dry-run mode
							 | 
						||
| 
								 | 
							
								#               $verbose        Integer level of verbosity
							 | 
						||
| 
								 | 
							
								#      RETURNS: Nothing
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Simplifies the saving of an error message and logging it
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub saveError {
							 | 
						||
| 
								 | 
							
								    my ( $message, $struct, $log, $dry_run, $verbose ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    $log->warn($message) unless $dry_run;
							 | 
						||
| 
								 | 
							
								    push( @{$struct}, $message );
							 | 
						||
| 
								 | 
							
								    print "$message\n" if $verbose > 0;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: validate_comment
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Checks that all of the required fields are present in the
							 | 
						||
| 
								 | 
							
								#               JSON comment
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $comment        - a pointer to the JSON hash
							 | 
						||
| 
								 | 
							
								#      RETURNS: True (1) if valid, otherwise false (0)
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION:
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub validate_comment {
							 | 
						||
| 
								 | 
							
								    my ($comment) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    foreach my $key (@json_keys) {
							 | 
						||
| 
								 | 
							
								        return 0 unless defined($comment->{$key});
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return 1;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: count_tags
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Counts the number of start tags in what might be HTML
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $string         String to examine
							 | 
						||
| 
								 | 
							
								#      RETURNS: An integer count of the number of start tags found
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Uses HTML::Parser to parse the input string. A handler is
							 | 
						||
| 
								 | 
							
								#               defined which accumulates start and end tags in an array. The
							 | 
						||
| 
								 | 
							
								#               number of start tags found is returned to the caller.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub count_tags {
							 | 
						||
| 
								 | 
							
								    my ($string) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my @accum;
							 | 
						||
| 
								 | 
							
								    chomp($string);
							 | 
						||
| 
								 | 
							
								    return 0 unless $string;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my $p = HTML::Parser->new(
							 | 
						||
| 
								 | 
							
								        api_version => 3,
							 | 
						||
| 
								 | 
							
								        handlers    => {
							 | 
						||
| 
								 | 
							
								            start => [ \@accum, "event,text" ],
							 | 
						||
| 
								 | 
							
								            end   => [ \@accum, "event,text" ]
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								    $p->parse($string);
							 | 
						||
| 
								 | 
							
								    $p->eof;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return scalar(@accum);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: clean_comment_field
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Cleans a string field from the comment
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $field - the field to be cleaned
							 | 
						||
| 
								 | 
							
								#      RETURNS: The cleaned string
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: We want to remove all HTML. We also want to add HTML entities
							 | 
						||
| 
								 | 
							
								#               where these may be needed. If there are already entities in
							 | 
						||
| 
								 | 
							
								#               the data we will decode them first then re-encode so we don't
							 | 
						||
| 
								 | 
							
								#               get double encoding.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub clean_comment_field {
							 | 
						||
| 
								 | 
							
								    my ($field) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Remove all HTML
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    my $hr = HTML::Restrict->new();
							 | 
						||
| 
								 | 
							
								    $field = $hr->process($field);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Entities. Make sure all existing entities are decoded first, then encode
							 | 
						||
| 
								 | 
							
								    # the result. Note that the unsafe characters are the reverse of what's
							 | 
						||
| 
								 | 
							
								    # listed. The second argument to encode_entities uses regular expression
							 | 
						||
| 
								 | 
							
								    # character class syntax (as if in [] in a RE). The set is all non-plain
							 | 
						||
| 
								 | 
							
								    # ASCII exclusing newline, carriage return and '<>&'.
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    decode_entities($field);
							 | 
						||
| 
								 | 
							
								    $field
							 | 
						||
| 
								 | 
							
								        = encode_entities( $field, '^\n\r\x20-\x25\x27-\x3b\x3d\x3f-\x7e' );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return $field;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: moveFile
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Moves a file (renames it) taking account of whether the name
							 | 
						||
| 
								 | 
							
								#               exists at the destination
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $file           file to move
							 | 
						||
| 
								 | 
							
								#               $dest           destination of the file
							 | 
						||
| 
								 | 
							
								#      RETURNS: True (1) if the move succeeded, otherwise false (0)
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: A wrapper around the File::Copy 'move' function. If the target
							 | 
						||
| 
								 | 
							
								#               file exists then a version number is appended to the name so
							 | 
						||
| 
								 | 
							
								#               that the original is not clobbered.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub moveFile {
							 | 
						||
| 
								 | 
							
								    my ( $file, $dest ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my ( $target, $basename, $prefix, $vsn );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return 0 unless -e $file;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # Handle a destination directory
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    if ( -d $dest ) {
							 | 
						||
| 
								 | 
							
								        ( $basename = $file ) =~ s|.*/||mx;
							 | 
						||
| 
								 | 
							
								        $target = "$dest/$basename";
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    else {
							 | 
						||
| 
								 | 
							
								        $target = $dest;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    # If the target exists we'll have to do some work, otherwise just do the
							 | 
						||
| 
								 | 
							
								    # move
							 | 
						||
| 
								 | 
							
								    #
							 | 
						||
| 
								 | 
							
								    if ( -e $target ) {
							 | 
						||
| 
								 | 
							
								        if ( ( $prefix, $vsn ) = ( $target =~ /(\.)(\d+)$/ ) ) {
							 | 
						||
| 
								 | 
							
								            while ( -e "$prefix$vsn" ) {
							 | 
						||
| 
								 | 
							
								                $vsn++;
							 | 
						||
| 
								 | 
							
								                $target = "$prefix$vsn";
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            return move( $file, $target );
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        else {
							 | 
						||
| 
								 | 
							
								            $vsn    = 1;
							 | 
						||
| 
								 | 
							
								            $prefix = $target;
							 | 
						||
| 
								 | 
							
								            $target = "$prefix.$vsn";
							 | 
						||
| 
								 | 
							
								            while ( -e "$prefix.$vsn" ) {
							 | 
						||
| 
								 | 
							
								                $vsn++;
							 | 
						||
| 
								 | 
							
								                $target = "$prefix.$vsn";
							 | 
						||
| 
								 | 
							
								            }
							 | 
						||
| 
								 | 
							
								            return move( $file, $target );
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    else {
							 | 
						||
| 
								 | 
							
								        return move( $file, $dest );
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: abbreviate_filename
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Abbreviates a long pathname by shrinking the directory names.
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $filename       The name of the file
							 | 
						||
| 
								 | 
							
								#      RETURNS: A string containing the abbreviated name
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Splits the filename, assembles the directories as just their
							 | 
						||
| 
								 | 
							
								#               first character, and then appends the filename. A bit
							 | 
						||
| 
								 | 
							
								#               primitive but it works.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub abbreviate_filename {
							 | 
						||
| 
								 | 
							
								    my ($filename) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my @elements = split('/',$filename);
							 | 
						||
| 
								 | 
							
								    my @shortened = map {substr($_,0,1)} @elements[0..$#elements-1];
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return join('/',@shortened,$elements[-1]);
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: sort_by_timestamp
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Sorts the main hash by timestamp rather than the normal sort
							 | 
						||
| 
								 | 
							
								#               by key
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $struct         hashref pointing to the main structure
							 | 
						||
| 
								 | 
							
								#      RETURNS: A list of hash keys in order of timestamp
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Generates a list of keys by sorting based on the comment
							 | 
						||
| 
								 | 
							
								#               timestamp in the JSON hash. Performs the sort by doing
							 | 
						||
| 
								 | 
							
								#               a string comparison since the dates are in ISO8601
							 | 
						||
| 
								 | 
							
								#               'YYYY-MM-DDTHH:MM:SSZ' format (e.g. 2017-10-06T06:22:55Z).
							 | 
						||
| 
								 | 
							
								#               This seems to be safe.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub sort_by_timestamp {
							 | 
						||
| 
								 | 
							
								    my ($struct) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my @result = sort {
							 | 
						||
| 
								 | 
							
								        $struct->{$a}->{JSON}->{comment_timestamp}
							 | 
						||
| 
								 | 
							
								            cmp $struct->{$b}->{JSON}->{comment_timestamp}
							 | 
						||
| 
								 | 
							
								    } keys(%$struct);
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return @result;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: _debug
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Prints debug reports
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $active         Boolean: 1 for print, 0 for no print
							 | 
						||
| 
								 | 
							
								#               @messages       List of messages to print
							 | 
						||
| 
								 | 
							
								#      RETURNS: Nothing
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION: Outputs messages to STDERR if $active is true. For each
							 | 
						||
| 
								 | 
							
								#               message it removes any trailing newline and then adds one in
							 | 
						||
| 
								 | 
							
								#               the 'print' so the caller doesn't have to bother. Prepends
							 | 
						||
| 
								 | 
							
								#               each message with 'D> ' to show it's a debug message.
							 | 
						||
| 
								 | 
							
								#       THROWS: No exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: None
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: N/A
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub _debug {
							 | 
						||
| 
								 | 
							
								    my ( $active, @messages ) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return unless $active;
							 | 
						||
| 
								 | 
							
								    foreach my $msg (@messages) {
							 | 
						||
| 
								 | 
							
								        chomp($msg);
							 | 
						||
| 
								 | 
							
								        print STDERR "D> $msg\n";
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#===  FUNCTION  ================================================================
							 | 
						||
| 
								 | 
							
								#         NAME: Options
							 | 
						||
| 
								 | 
							
								#      PURPOSE: Processes command-line options
							 | 
						||
| 
								 | 
							
								#   PARAMETERS: $optref     Hash reference to hold the options
							 | 
						||
| 
								 | 
							
								#      RETURNS: Undef
							 | 
						||
| 
								 | 
							
								#  DESCRIPTION:
							 | 
						||
| 
								 | 
							
								#       THROWS: no exceptions
							 | 
						||
| 
								 | 
							
								#     COMMENTS: none
							 | 
						||
| 
								 | 
							
								#     SEE ALSO: n/a
							 | 
						||
| 
								 | 
							
								#===============================================================================
							 | 
						||
| 
								 | 
							
								sub Options {
							 | 
						||
| 
								 | 
							
								    my ($optref) = @_;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    my @options = (
							 | 
						||
| 
								 | 
							
								        "help",     "documentation|manpage",
							 | 
						||
| 
								 | 
							
								        "debug=i",  "dry-run!",
							 | 
						||
| 
								 | 
							
								        "verbose+", "verify!",
							 | 
						||
| 
								 | 
							
								        "live!",    "json!",
							 | 
						||
| 
								 | 
							
								        "config=s",
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    if ( !GetOptions( $optref, @options ) ) {
							 | 
						||
| 
								 | 
							
								        pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 );
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    return;
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								__END__
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
							 | 
						||
| 
								 | 
							
								#  Application Documentation
							 | 
						||
| 
								 | 
							
								#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
							 | 
						||
| 
								 | 
							
								#{{{
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 NAME
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								process_comments
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=over 4
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Process incoming comment files as email messages or JSON files
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=back
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 VERSION
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This documentation refers to process_comments version 0.3.3
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 USAGE
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    ./process_comments [-help] [-doc] [-debug=N] [-[no]dry-run]
							 | 
						||
| 
								 | 
							
								        [-verbose ...] [-[no]live] [-[no]json] [-config=FILE]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    ./process_comments -dry-run
							 | 
						||
| 
								 | 
							
								    ./process_comments -debug=3 -dry-run
							 | 
						||
| 
								 | 
							
								    ./process_comments -verbose -verbose
							 | 
						||
| 
								 | 
							
								    ./process_comments -help
							 | 
						||
| 
								 | 
							
								    ./process_comments -documentation
							 | 
						||
| 
								 | 
							
								    ./process_comments -json
							 | 
						||
| 
								 | 
							
								    ./process_comments -verb -live -config=.process_comments_live.cfg
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 OPTIONS
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=over 8
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<-help>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints a brief help message describing the usage of the program, and then exits.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<-documentation> or B<-manpage>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the entire embedded documentation for the program, then exits.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<-debug=N>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Enables debugging mode when N > 0 (zero is the default). The levels are:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=over 4
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<1>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Reports on the options chosen. Running with the B<-dry-run> option prevents
							 | 
						||
| 
								 | 
							
								problems with comments if any options have been incorrectly used.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<2>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints all of the information described at the previous level.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the names of files found in the mail or JSON spool areas.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the internal details of the email, listing the MIME parts (if there are any).
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the length of the MIME part  matching the desired type, in lines.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the entirety of the internal structure holding details of the mail file
							 | 
						||
| 
								 | 
							
								and the comment it contains. This follows the moderation pass.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the SQL that has been constructed to update the database.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<3>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints all of the information described at the previous levels.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the contents of the B<%config> hash built from the configuration file.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=back
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<-[no]dry-run>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Controls the program's B<dry-run> mode. It is off by default. In dry-run mode
							 | 
						||
| 
								 | 
							
								the program reports what it would do but makes no changes. When off the
							 | 
						||
| 
								 | 
							
								program makes all the changes it is designed to perform.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<-verbose>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This option may be repeated. For each repetition the level of verbosity is
							 | 
						||
| 
								 | 
							
								increased. By default no verbosity is in effect and the program prints out the
							 | 
						||
| 
								 | 
							
								minimal amount of information.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Verbosity levels:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=over 4
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<1>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the name of each mail (or JSON) file as it's processed.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints any error messages during message validation, which are also being
							 | 
						||
| 
								 | 
							
								logged (unless in dry-run mode) and saved for reporting later.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints a notification if the comment is added to the database (or that this
							 | 
						||
| 
								 | 
							
								would have happened in dry-run mode).
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints messages about the moving of each mail (or JSON) file from the
							 | 
						||
| 
								 | 
							
								processing area, along with any errors accumulated for that file. In dry-run
							 | 
						||
| 
								 | 
							
								mode simply indicates what would have happened.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the response code received from the server when invoking the interface
							 | 
						||
| 
								 | 
							
								for updating comment files there. If in dry-run mode the message produced
							 | 
						||
| 
								 | 
							
								merely indicates what would have happened.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								If validation failed earlier on then further information is produced about the
							 | 
						||
| 
								 | 
							
								final actions taken on these files.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<2>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the addresses each mail message is being sent to (unless in JSON mode).
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<3>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Prints the JSON contents of each mail message (or of each JSON file).
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=back
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<-[no]verify>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This option controls whether the script imposes a delay on comments. The idea
							 | 
						||
| 
								 | 
							
								is that if the comment feature is used to rant on a subject or to pass
							 | 
						||
| 
								 | 
							
								misinformation, delaying them will help to defuse the situation.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The default state is B<-noverify>; a delay is not imposed. Selecting
							 | 
						||
| 
								 | 
							
								B<-verify> means that comments have to be at least 24 hours old before they
							 | 
						||
| 
								 | 
							
								are processed. The length of the delay cannot currently be changed without
							 | 
						||
| 
								 | 
							
								altering the script.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This feature of the script has hardly been used and is perhaps against the
							 | 
						||
| 
								 | 
							
								spirit of HPR, so it will probably be removed in a future version.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<-[no]live>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This option determines whether the program runs in live mode or not. The
							 | 
						||
| 
								 | 
							
								default varies depending on which system it is being run on.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								IT SHOULD NOT USUALLY BE NECESSARY TO USE THIS!
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								In live mode the program makes changes to the live database and sends messages
							 | 
						||
| 
								 | 
							
								to the live web interface when a comment has been processed. With live mode
							 | 
						||
| 
								 | 
							
								off the program assumes it is writing to a clone of the database and it does
							 | 
						||
| 
								 | 
							
								not inform the webserver that a comment has been processed.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The default for the copy of the program on borg is that live mode is ON.
							 | 
						||
| 
								 | 
							
								Otherwise the default is that live mode is OFF. The setting is determined by
							 | 
						||
| 
								 | 
							
								the sed script called B<fixup.sed> on borg. This needs to be run whenever
							 | 
						||
| 
								 | 
							
								a new version of the program is released. This is done as follows:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    sed -i -f fixup.sed process_comments
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<-[no]json>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This option selects JSON mode, which makes the script behave in a different
							 | 
						||
| 
								 | 
							
								way from the default mode (B<-nojson> or MAIL mode) where it processes email
							 | 
						||
| 
								 | 
							
								containing comments.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								In JSON mode the script looks in a sub-directory called B<json/> where it
							 | 
						||
| 
								 | 
							
								expects to find JSON files. The normal way in which these files arrive in this
							 | 
						||
| 
								 | 
							
								directory is by using B<scp> to copy them from the HPR server (the directory
							 | 
						||
| 
								 | 
							
								is B</home/hpr/comments>). This is a provision in case the normal route of
							 | 
						||
| 
								 | 
							
								sending out email messages has failed for some reason. It also saves the user
							 | 
						||
| 
								 | 
							
								from setting up the mail handling infrastructure that would otherwise be
							 | 
						||
| 
								 | 
							
								needed.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								In JSON mode the mail handling logic is not invoked, files are searched for in
							 | 
						||
| 
								 | 
							
								the B<json/> directory and each file is processed, moderation is requested and
							 | 
						||
| 
								 | 
							
								the comment is added to the database. In `B<-live>` mode the server is informed
							 | 
						||
| 
								 | 
							
								that the comment has been processed.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The B<json/> directory needs to have three sub-directories: B<processed>,
							 | 
						||
| 
								 | 
							
								B<banned> and B<rejected>. The script will place the processed files into
							 | 
						||
| 
								 | 
							
								these sub-directories according to the moderation choice made. This makes it
							 | 
						||
| 
								 | 
							
								easier to see what actions were taken and helps avoid repeated processing of
							 | 
						||
| 
								 | 
							
								the same comment.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<-config=FILE>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This option defines a configuration file other than the default
							 | 
						||
| 
								 | 
							
								B<.hpr_db.cfg>. The file must be formatted as described below in the section
							 | 
						||
| 
								 | 
							
								B<CONFIGURATION AND ENVIRONMENT>.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=back
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 DESCRIPTION
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								A script to process new comments, moderate them and add them to the HPR
							 | 
						||
| 
								 | 
							
								database.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								In the new HPR comment system (released September 2017) a new web form is
							 | 
						||
| 
								 | 
							
								presented in association with each show. The form can be used to submit
							 | 
						||
| 
								 | 
							
								a comment on the show in question and takes some standard fields: the name of
							 | 
						||
| 
								 | 
							
								the commenter, the title of the comment and the body of the comment itself.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Once the comment has been submitted its contents are formatted as a JSON
							 | 
						||
| 
								 | 
							
								object and are sent as a mail attachment to the address
							 | 
						||
| 
								 | 
							
								B<comments@hackerpublicradio.org>.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Recipients of these mail messages can then perform actions on the comments
							 | 
						||
| 
								 | 
							
								they contain to cause them to be added to the HPR database. These actions are:
							 | 
						||
| 
								 | 
							
								approve the comment, block it (because it is inappropriate or some form of
							 | 
						||
| 
								 | 
							
								Spam and we want to prevent any further messages from the associated IP
							 | 
						||
| 
								 | 
							
								address), or reject it (delete it). There is also an ignore option which skips
							 | 
						||
| 
								 | 
							
								the current comment in this run of the script so it can be revisited later.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This script can process an entire email message which has been saved to a file
							 | 
						||
| 
								 | 
							
								or a file containing the JSON object (as in the email attachment). When
							 | 
						||
| 
								 | 
							
								processing email it is expected that it will be found in a maildrop directory,
							 | 
						||
| 
								 | 
							
								and when finished the messages will be placed in sub-directories according to
							 | 
						||
| 
								 | 
							
								what actions were carried out. A similar logic is used for JSON files; they
							 | 
						||
| 
								 | 
							
								are expected to be in a drop area and are moved to sub-directories after
							 | 
						||
| 
								 | 
							
								processing.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The directories used as spool areas are defined in the configuration file(s)
							 | 
						||
| 
								 | 
							
								described below.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head2 CHANGES
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head3 2024
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								An alternative method of comment approval has been added using PHP on the
							 | 
						||
| 
								 | 
							
								server B<https://hub.hackerpublicradio.org>. This script was originally
							 | 
						||
| 
								 | 
							
								designed around receiving email messages containing JSON or collecting JSON
							 | 
						||
| 
								 | 
							
								files. The assumption was that it was the sole agent processing these messages
							 | 
						||
| 
								 | 
							
								or files and would therefore be the only way that comments could enter the
							 | 
						||
| 
								 | 
							
								database.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								With the new server route it became possible for an emailed comment to be
							 | 
						||
| 
								 | 
							
								received when that comment was already in the database.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								As of version B<0.3.2> the script has been enhanced to check whether comments
							 | 
						||
| 
								 | 
							
								received as mail messages (or JSON files) have already been processed. If this
							 | 
						||
| 
								 | 
							
								is found to be the case the incoming data is skipped and stored in the
							 | 
						||
| 
								 | 
							
								'B<processed>' directory (because the comment has been approved elsewhere).
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								There is still scope for confusion - if for example, a comment has been
							 | 
						||
| 
								 | 
							
								rejected on the server, the script will see it as a potential addition to the
							 | 
						||
| 
								 | 
							
								database. The moderators need to coordinate their decisions!
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The ultimate solution will be to have just one way of moderating and
							 | 
						||
| 
								 | 
							
								installing comments which is available to any of the "Janitors".
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head2 MAIL HANDLING
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								One way of handling incoming mail is to use a mail client which is capable of
							 | 
						||
| 
								 | 
							
								saving messages sent to the above address in the spool area mentioned earlier.
							 | 
						||
| 
								 | 
							
								For example, Thunderbird can do this by use of a filter and a plugin. Other
							 | 
						||
| 
								 | 
							
								MUA's will have similar capabilities.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								When this script is run on the mail spool area it will process all of the
							 | 
						||
| 
								 | 
							
								files it finds. For each file it will check its validity in various ways,
							 | 
						||
| 
								 | 
							
								display the comment then offer a moderation menu. The moderation options are
							 | 
						||
| 
								 | 
							
								described below.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head3 APPROVE
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								If a comment is approved then it will be added to the database, the associated
							 | 
						||
| 
								 | 
							
								mail file will be moved to a sub-directory (by default called 'B<processed>'),
							 | 
						||
| 
								 | 
							
								and the HPR server will be notified of this action.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head3 BAN
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								If a comment is banned then it will not be added to the database. The mail
							 | 
						||
| 
								 | 
							
								file will be moved to the sub-directory 'B<banned>' and the HPR server will be
							 | 
						||
| 
								 | 
							
								informed that the IP address associated with the comment should be placed on
							 | 
						||
| 
								 | 
							
								a black list.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head3 REJECT
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								If a comment is rejected it is not written to the database, the mail file is
							 | 
						||
| 
								 | 
							
								moved to the sub-directory 'B<rejected>' and the HPR server informed that the
							 | 
						||
| 
								 | 
							
								comment can be deleted.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head3 IGNORE
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								If a comment is ignored it is simply left in the mail spool and no further
							 | 
						||
| 
								 | 
							
								processing done on it. It will be eligible for processing again when the
							 | 
						||
| 
								 | 
							
								script is next run.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head2 JSON FILE HANDLING
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								As described under the description of the B<-[no]json> option, the script
							 | 
						||
| 
								 | 
							
								allows the processing of a multiple JSON files, each containing a single
							 | 
						||
| 
								 | 
							
								comment. These files are expected to be in another spool area (defined int he
							 | 
						||
| 
								 | 
							
								configuration file). The JSON is checked and all of the comment fields are
							 | 
						||
| 
								 | 
							
								validated, then the moderation process is begun.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Moderation in this case consists of the same steps as described above except
							 | 
						||
| 
								 | 
							
								that no mail file actions are taken and the JSON file is moved to
							 | 
						||
| 
								 | 
							
								a sub-directory after processing.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 DIAGNOSTICS
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=over 4
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Unable to find configuration file ...>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: fatal
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The nominated configuration file referenced in B<-config=FILE> was not found.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<No mail found; nothing to do>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: fatal
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								No mail files were found in the mail spool area requiring processing.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<No JSON files found; nothing to do>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: fatal
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								No JSON files were found in the JSON spool area requiring processing.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Failed to read JSON file '...' ...>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: fatal
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								A JSON file in the spool area could not be read with a JSON parser.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Failed to parse comment timestamp ...>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: fatal
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The timestamp must be converted to a format compatible with MySQL/MariaDB but
							 | 
						||
| 
								 | 
							
								during this process the parse failed.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Failed to open input file '...' ...>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: fatal
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								A mail file in the spool area could not be opened.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Failed to move ...>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: warning
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								A mail file could not be moved to the relevant sub-directory.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Failed to close input file '...' ...>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: warning
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								A mail file in the spool area could not be closed.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Various error messages from the database subsystem>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: fatal, warning
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								An action on the database has been flagged as an error.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Various error messages from the Template toolkit>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: fatal
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								An action relating to the template used for the display of the comment has
							 | 
						||
| 
								 | 
							
								been flagged as an error.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Invalid call to 'call_back' subroutine; missing key>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: warning
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The routine 'call_back' was called incorrectly. The key was missing.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Invalid call to 'call_back' subroutine; invalid action>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: warning
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The routine 'call_back' was called incorrectly. The action was invalid.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=item B<Error from remote server indicating failure>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Type: warning
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								While attempting to send an action to the remote server with the 'call_back'
							 | 
						||
| 
								 | 
							
								subroutine an error message was received.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=back
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 CONFIGURATION AND ENVIRONMENT
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head2 CONFIGURATION
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The configuration file design was changed for version B<0.2.7> and beyond.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The script expects a configuration file to provide it with various program
							 | 
						||
| 
								 | 
							
								settings, database credentials and HPR CMS credentials.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The default configuration file name is B<.process_comments.cfg> and is for use
							 | 
						||
| 
								 | 
							
								with the local clone of the hpr database.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This file has the general structure:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <<include .process_comments_settings.cfg>>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <<include .hpr_db.cfg>>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <cms>
							 | 
						||
| 
								 | 
							
								     user = "dummy:dummy"
							 | 
						||
| 
								 | 
							
								 </cms>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								It uses B<Config::General> 'B<include>' directives to include the file
							 | 
						||
| 
								 | 
							
								B<.process_comments_settings.cfg> which holds the program settings. It then
							 | 
						||
| 
								 | 
							
								includes the file B<.hpr_db.cfg> which containss credentials for the cloned
							 | 
						||
| 
								 | 
							
								database. Finally it defines dummy credentials for the HPR CMS since this
							 | 
						||
| 
								 | 
							
								feature is not used by default.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The more useful configuration is in B<.process_comments_live.cfg>:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <<include .process_comments_settings.cfg>>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <<include .hpr_livedb.cfg>>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <cms>
							 | 
						||
| 
								 | 
							
								    <<include .hpradmin_curlrc>>
							 | 
						||
| 
								 | 
							
								 </cms>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The basic format of the main configuration data after the inclusions have been
							 | 
						||
| 
								 | 
							
								performed is then:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <settings>
							 | 
						||
| 
								 | 
							
								    # Holds program settings
							 | 
						||
| 
								 | 
							
								 </settings>
							 | 
						||
| 
								 | 
							
								 <database>
							 | 
						||
| 
								 | 
							
								    # Holds database settings
							 | 
						||
| 
								 | 
							
								 </database>
							 | 
						||
| 
								 | 
							
								 <cms>
							 | 
						||
| 
								 | 
							
								    # Holds CMS credentials
							 | 
						||
| 
								 | 
							
								 </cms>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head3 CONTENTS OF B<.process_comments_settings.cfg>:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This section is enclosed in '<settings>' tags:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <settings>
							 | 
						||
| 
								 | 
							
								     PROG       = process_comments
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								     #
							 | 
						||
| 
								 | 
							
								     # Defaults
							 | 
						||
| 
								 | 
							
								     #
							 | 
						||
| 
								 | 
							
								     basedir    = "$HOME/HPR/Comment_system"
							 | 
						||
| 
								 | 
							
								     configfile = "$basedir/.hpr_db.cfg"
							 | 
						||
| 
								 | 
							
								     logfile    = "$basedir/logs/${PROG}.log"
							 | 
						||
| 
								 | 
							
								     template   = "$basedir/${PROG}.tpl"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								     #
							 | 
						||
| 
								 | 
							
								     # Mail message stash area
							 | 
						||
| 
								 | 
							
								     #
							 | 
						||
| 
								 | 
							
								     maildrop   = "$HOME/HPR/CommentDrop"
							 | 
						||
| 
								 | 
							
								     processed  = "$maildrop/processed"
							 | 
						||
| 
								 | 
							
								     rejected   = "$maildrop/rejected"
							 | 
						||
| 
								 | 
							
								     banned     = "$maildrop/banned"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								     #
							 | 
						||
| 
								 | 
							
								     # JSON stash area
							 | 
						||
| 
								 | 
							
								     #
							 | 
						||
| 
								 | 
							
								     jsondir    = "$basedir/json"
							 | 
						||
| 
								 | 
							
								     jprocessed = "$jsondir/processed"
							 | 
						||
| 
								 | 
							
								     jrejected  = "$jsondir/rejected"
							 | 
						||
| 
								 | 
							
								     jbanned    = "$jsondir/banned"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								     #
							 | 
						||
| 
								 | 
							
								     # How to tell the server the comment's processed
							 | 
						||
| 
								 | 
							
								     #
							 | 
						||
| 
								 | 
							
								     callback_template = \
							 | 
						||
| 
								 | 
							
								             "https://hackerpublicradio.org/comment_process.php?key=%s&action=%s"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 </settings>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head3 CONTENTS OF B<.hpr_db.cfg> AND B<.hpr_livedb.cfg>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The default file providing this part is just for use with the default
							 | 
						||
| 
								 | 
							
								database. The section is enclosed in '<database>' tags:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <database>
							 | 
						||
| 
								 | 
							
								     host = 127.0.0.1
							 | 
						||
| 
								 | 
							
								     port = PORT
							 | 
						||
| 
								 | 
							
								     name = DATABASE
							 | 
						||
| 
								 | 
							
								     user = USERNAME
							 | 
						||
| 
								 | 
							
								     password = PASSWORD
							 | 
						||
| 
								 | 
							
								 </database>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								These settings can alternatively be used to connect to an SSH tunnel which has
							 | 
						||
| 
								 | 
							
								been connected  from a remote system (like borg) to the live database.
							 | 
						||
| 
								 | 
							
								Assuming the port chosen for this is 3307 something like the following could
							 | 
						||
| 
								 | 
							
								be used:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 <database>
							 | 
						||
| 
								 | 
							
								     host = 127.0.0.1
							 | 
						||
| 
								 | 
							
								     port = 3307
							 | 
						||
| 
								 | 
							
								     name = hpr_hpr
							 | 
						||
| 
								 | 
							
								     user = hpr_hpr
							 | 
						||
| 
								 | 
							
								     password = "**censored**"
							 | 
						||
| 
								 | 
							
								 </database>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								A typical Bash script for opening a tunnel might be:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 #!/bin/bash
							 | 
						||
| 
								 | 
							
								 SSHPORT=22
							 | 
						||
| 
								 | 
							
								 LOCALPORT=3307
							 | 
						||
| 
								 | 
							
								 REMOTEPORT=3306
							 | 
						||
| 
								 | 
							
								 ssh -p ${SSHPORT} -f -N -L localhost:${LOCALPORT}:localhost:${REMOTEPORT} hpr@hackerpublicradio.org
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head3 CONTENTS OF B<.hpradmin_curlrc>
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This is a file used by the 'curl' command to contain credentials to access the
							 | 
						||
| 
								 | 
							
								CMS on the HPR server. The format needs to be:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								 user = hpradmin:**censored**
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This is one of the formats allowed by 'curl' and is also compatible with the
							 | 
						||
| 
								 | 
							
								B<Config::General> module used by this script.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head2 TEMPLATE
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The program displays the comment that is currently being processed for
							 | 
						||
| 
								 | 
							
								moderation. It uses a template along with the Perl B<Template> module to do
							 | 
						||
| 
								 | 
							
								this. By default this template is called B<process_comments.tpl>. This can be
							 | 
						||
| 
								 | 
							
								changed by changing the line in the "settings" configuration file which sets
							 | 
						||
| 
								 | 
							
								the 'template' variable.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The template is provided with the following data:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    file        a scalar containing the name of the file being processed
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    db          a hash containing the details of the show to which the
							 | 
						||
| 
								 | 
							
								                comment relates, returned from a database query:
							 | 
						||
| 
								 | 
							
								                id              the episode number
							 | 
						||
| 
								 | 
							
								                date            the date of the episode
							 | 
						||
| 
								 | 
							
								                title           the episode title
							 | 
						||
| 
								 | 
							
								                host            the host name
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    comment     a hash containing the fields from the comment:
							 | 
						||
| 
								 | 
							
								                eps_id                  the episode number
							 | 
						||
| 
								 | 
							
								                comment_timestamp       date and time of the comment
							 | 
						||
| 
								 | 
							
								                comment_author_name     comment author
							 | 
						||
| 
								 | 
							
								                comment_title           comment title
							 | 
						||
| 
								 | 
							
								                comment_text            comment text
							 | 
						||
| 
								 | 
							
								                justification           justification for posting (if
							 | 
						||
| 
								 | 
							
								                                        relevant)
							 | 
						||
| 
								 | 
							
								                key                     unique comment key
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 DEPENDENCIES
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    Carp
							 | 
						||
| 
								 | 
							
								    Config::General
							 | 
						||
| 
								 | 
							
								    DBI
							 | 
						||
| 
								 | 
							
								    Data::Dumper
							 | 
						||
| 
								 | 
							
								    DateTime::Format::ISO8601
							 | 
						||
| 
								 | 
							
								    Encode
							 | 
						||
| 
								 | 
							
								    File::Copy
							 | 
						||
| 
								 | 
							
								    File::Find::Rule
							 | 
						||
| 
								 | 
							
								    File::Slurper
							 | 
						||
| 
								 | 
							
								    Getopt::Long
							 | 
						||
| 
								 | 
							
								    HTML::Entities
							 | 
						||
| 
								 | 
							
								    HTML::Restrict
							 | 
						||
| 
								 | 
							
								    IO::Prompter
							 | 
						||
| 
								 | 
							
								    JSON
							 | 
						||
| 
								 | 
							
								    LWP::UserAgent
							 | 
						||
| 
								 | 
							
								    List::Util
							 | 
						||
| 
								 | 
							
								    Log::Handler
							 | 
						||
| 
								 | 
							
								    MIME::Parser
							 | 
						||
| 
								 | 
							
								    Mail::Address
							 | 
						||
| 
								 | 
							
								    Mail::Field
							 | 
						||
| 
								 | 
							
								    Mail::Internet
							 | 
						||
| 
								 | 
							
								    Pod::Usage
							 | 
						||
| 
								 | 
							
								    SQL::Abstract
							 | 
						||
| 
								 | 
							
								    Template
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								The Perl B<experimental:try> option is used instead of the original
							 | 
						||
| 
								 | 
							
								B<Try::Tiny> package.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 BUGS AND LIMITATIONS
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								There are no known bugs in this module.
							 | 
						||
| 
								 | 
							
								Please report problems to Dave Morriss (Dave.Morriss@gmail.com)
							 | 
						||
| 
								 | 
							
								Patches are welcome.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 AUTHOR
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Dave Morriss (Dave.Morriss@gmail.com)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=head1 LICENCE AND COPYRIGHT
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								Copyright (c) 2017 - 2024 Dave Morriss (Dave.Morriss@gmail.com). All rights
							 | 
						||
| 
								 | 
							
								reserved.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This module is free software; you can redistribute it and/or
							 | 
						||
| 
								 | 
							
								modify it under the same terms as Perl itself. See perldoc perlartistic.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								This program is distributed in the hope that it will be useful,
							 | 
						||
| 
								 | 
							
								but WITHOUT ANY WARRANTY; without even the implied warranty of
							 | 
						||
| 
								 | 
							
								MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								=cut
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								#}}}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# [zo to open fold, zc to close]
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
							 |