forked from HPR/hpr-tools
		
	
		
			
				
	
	
		
			485 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			485 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env perl
 | |
| #===============================================================================
 | |
| #
 | |
| #         FILE: make_meeting
 | |
| #
 | |
| #        USAGE: ./make_meeting
 | |
| #
 | |
| #  DESCRIPTION: Makes a recurrent iCalendar meeting to be loaded into
 | |
| #               a calendar. This is apparently necessary when the 'RRULE'
 | |
| #               recurrence description is not adequate.
 | |
| #
 | |
| #      OPTIONS: None
 | |
| # REQUIREMENTS: Needs modules Getopt::Long, Data::ICal, Date::Parse and
 | |
| #               Date::Calc
 | |
| #         BUGS: ---
 | |
| #        NOTES: Based on a script distributed with the HPR episode "iCalendar
 | |
| #               Hacking"
 | |
| #       AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
 | |
| #      LICENCE: Copyright (c) year 2012-2024 Dave Morriss
 | |
| #      VERSION: 0.2.2
 | |
| #      CREATED: 2012-10-13 15:34:01
 | |
| #     REVISION: 2024-05-24 22:45:56
 | |
| #
 | |
| #===============================================================================
 | |
| 
 | |
| use 5.010;
 | |
| use strict;
 | |
| use warnings;
 | |
| 
 | |
| use Getopt::Long;
 | |
| 
 | |
| use Data::ICal;
 | |
| use Data::ICal::Entry::Event;
 | |
| use Data::ICal::Entry::Todo;
 | |
| 
 | |
| use Date::Parse;
 | |
| use Date::Calc qw{:all};
 | |
| use Date::ICal;
 | |
| 
 | |
| #use Data::Dumper;
 | |
| 
 | |
| #
 | |
| # Version number (manually incremented)
 | |
| #
 | |
| our $VERSION = '0.2.2';
 | |
| 
 | |
| #
 | |
| # Script name
 | |
| #
 | |
| ( my $PROG = $0 ) =~ s|.*/||mx;
 | |
| ( my $DIR  = $0 ) =~ s|/?[^/]*$||mx;
 | |
| $DIR = '.' unless $DIR;
 | |
| 
 | |
| #
 | |
| # Enable Unicode mode
 | |
| #
 | |
| binmode STDOUT, ":encoding(UTF-8)";
 | |
| binmode STDERR, ":encoding(UTF-8)";
 | |
| 
 | |
| #-------------------------------------------------------------------------------
 | |
| # Declarations
 | |
| #-------------------------------------------------------------------------------
 | |
| my ( @startdate, @rdate, @events );
 | |
| 
 | |
| #
 | |
| # Attributes for the calendar message
 | |
| #
 | |
| #my $server = 'ch1.teamspeak.cc';
 | |
| #my $port   = 64747;
 | |
| my $server = 'chatter.skyehaven.net';
 | |
| my $port   = 64738;
 | |
| 
 | |
| #-------------------------------------------------------------------------------
 | |
| # Options and arguments
 | |
| #-------------------------------------------------------------------------------
 | |
| my $DEF_COUNT   = 12;
 | |
| #my $DEF_SUMMARY = 'Send out CNews email';
 | |
| 
 | |
| #
 | |
| # Process options
 | |
| #
 | |
| my %options;
 | |
| Options( \%options );
 | |
| 
 | |
| #
 | |
| # Default help
 | |
| #
 | |
| usage() if ( $options{'help'} );
 | |
| 
 | |
| #
 | |
| # Collect options
 | |
| #
 | |
| my $count = ( defined( $options{count} ) ? $options{count} : $DEF_COUNT );
 | |
| my $reminder = ( defined( $options{reminder} ) ? $options{reminder} : 0 );
 | |
| my $force    = ( defined( $options{force} )    ? $options{force}    : 0 );
 | |
| 
 | |
| #my $reminder_summary = ( defined( $options{summary} ) ? $options{summary} :
 | |
| #    $DEF_SUMMARY );
 | |
| 
 | |
| #
 | |
| # Two reminders: 8 days ahead reminder to check with Ken, 5 days ahead
 | |
| # reminder to send out the email.
 | |
| #
 | |
| my %reminders = (
 | |
|     email => [ -5, 'Send out CNews email' ],
 | |
|     check => [ -8, 'Check CNews date with Ken' ],
 | |
| );
 | |
| 
 | |
| #
 | |
| # Use the date provided or the default
 | |
| #
 | |
| if ( defined( $options{from} ) ) {
 | |
|     #
 | |
|     # Parse the date, convert to start of month and (optionally) validate it
 | |
|     #
 | |
|     @startdate = convert_date( $options{from}, $force );
 | |
| }
 | |
| else {
 | |
|     #
 | |
|     # Use the current date
 | |
|     #
 | |
|     @startdate = Today();
 | |
| }
 | |
| 
 | |
| #
 | |
| # Date and time values
 | |
| #
 | |
| # TODO: These should be in a configuration file, and should ideally be capable
 | |
| # of having a time zone defined (default UTC, as now).
 | |
| #
 | |
| my $monday = 1;    # Day of week number 1-7, Monday-Sunday
 | |
| 
 | |
| my @starttime = ( 13, 00, 00 );    # UTC
 | |
| my @endtime   = ( 15, 00, 00 );
 | |
| 
 | |
| my @todostart = ( 9,  00, 00 );    # UTC
 | |
| my @todoend   = ( 17, 00, 00 );
 | |
| 
 | |
| #
 | |
| # Format of an ISO UTC datetime
 | |
| #
 | |
| my $fmt = "%02d%02d%02dT%02d%02d%02dZ";
 | |
| 
 | |
| #
 | |
| # Constants for the event
 | |
| #
 | |
| my $calname     = 'HPR Community News';
 | |
| my $timezone    = 'UTC';
 | |
| my $location    = "$server port: $port";
 | |
| my $summary     = 'HPR Community News Recording Dates';
 | |
| my $description = <<ENDDESC;
 | |
| Mumble settings
 | |
| -------------------
 | |
| Server Name:    Anything you like
 | |
| Server Address: $server
 | |
| Port:           $port
 | |
| Name:           Your name or alias is fine
 | |
| 
 | |
| Information about Mumble can be found here:
 | |
| http://hackerpublicradio.org/recording.php
 | |
| ENDDESC
 | |
| 
 | |
| #
 | |
| # Compute the next recording date from the starting date (@startdate will be
 | |
| # today's date or the start of the explicitly selected month provided via
 | |
| # -from=DATE. We want day of the week to be Monday, the first in the month,
 | |
| # then to go back 1 day from that to get to the Sunday! Simple)
 | |
| #
 | |
| @startdate = make_date( \@startdate, $monday, 1, -1 );
 | |
| @rdate = @startdate;
 | |
| 
 | |
| #
 | |
| # Create the calendar object
 | |
| #
 | |
| my $calendar = Data::ICal->new();
 | |
| 
 | |
| #
 | |
| # Some calendar properties
 | |
| #
 | |
| $calendar->add_properties(
 | |
|     'X-WR-CALNAME'  => $calname,
 | |
|     'X-WR-TIMEZONE' => $timezone,
 | |
| );
 | |
| 
 | |
| #
 | |
| # Create the event object
 | |
| #
 | |
| my $vevent = Data::ICal::Entry::Event->new();
 | |
| 
 | |
| #
 | |
| # Add some event properties
 | |
| #
 | |
| $vevent->add_properties(
 | |
|     summary     => $summary,
 | |
|     location    => $location,
 | |
|     description => $description,
 | |
|     dtstart     => sprintf( $fmt, @startdate, @starttime ),
 | |
|     dtend       => sprintf( $fmt, @startdate, @endtime ),
 | |
| );
 | |
| 
 | |
| #
 | |
| # Add recurring dates. (Note that this generates RDATE entries rather than
 | |
| # 1 entry with multiple dates; this is because this module doesn't seem to
 | |
| # have the ability to generate the concatenated entry. The two modes of
 | |
| # expressing the repeated dates seem to be equivalent.)
 | |
| #
 | |
| for my $i ( 1 .. $count ) {
 | |
|     #
 | |
|     # Recording date computation from the start of the month
 | |
|     #
 | |
|     @rdate = make_date( \@rdate, $monday, 1, -1 );
 | |
| 
 | |
|     #
 | |
|     # Save the current recording date to make an array of arrayrefs
 | |
|     #
 | |
|     push( @events, [@rdate] );
 | |
| 
 | |
|     #
 | |
|     # Add this date to the multi-date event
 | |
|     #
 | |
|     $vevent->add_property( rdate =>
 | |
|             [ sprintf( $fmt, @rdate, @starttime ), { value => 'DATE-TIME' } ],
 | |
|     );
 | |
| 
 | |
|     #
 | |
|     # Increment the meeting date for the next one. If we're early in the month
 | |
|     # by one day otherwise to the beginning of the next month. This is
 | |
|     # necessary because otherwise make_date will skip months.
 | |
|     #
 | |
|     if ( $rdate[2] < 7 ) {
 | |
|         @rdate = Add_Delta_Days( @rdate, 1 );
 | |
|     }
 | |
|     else {
 | |
|         @rdate = ( ( Add_Delta_YM( @rdate, 0, 1 ) )[ 0 .. 1 ], 1 );
 | |
|     }
 | |
| }
 | |
| 
 | |
| #
 | |
| # Add the event into the calendar
 | |
| #
 | |
| $calendar->add_entry($vevent);
 | |
| 
 | |
| #
 | |
| # Are we to add reminders?
 | |
| #
 | |
| if ($reminder) {
 | |
|     #
 | |
|     # Loop through the cache of recording dates
 | |
|     #
 | |
|     for my $i ( 0 .. $count - 1 ) {
 | |
|         #
 | |
|         # Loop through the reminders hash
 | |
|         #
 | |
|         for my $key (keys(%reminders)) {
 | |
|             #
 | |
|             # A new Todo entry each iteration
 | |
|             #
 | |
|             my $vtodo = Data::ICal::Entry::Todo->new();
 | |
| 
 | |
|             #
 | |
|             # Get a recording date from the cache and subtract 5 days from it to
 | |
|             # get the preceding Monday
 | |
|             #
 | |
|             @rdate = @{ $events[$i] };
 | |
|             @rdate = Add_Delta_Days( @rdate, $reminders{$key}->[0] );
 | |
| 
 | |
|             #
 | |
|             # Add the date as the date part of the Todo
 | |
|             #
 | |
|             $vtodo->add_properties(
 | |
|                 summary => $reminders{$key}->[1],
 | |
|                 status  => 'INCOMPLETE',
 | |
|                 dtstart => Date::ICal->new(
 | |
|                     ical => sprintf( $fmt, @rdate, @todostart )
 | |
|                 )->ical,
 | |
|                 due => Date::ICal->new(
 | |
|                     ical => sprintf( $fmt, @rdate, @todoend )
 | |
|                 )->ical,
 | |
|             );
 | |
| 
 | |
|             #
 | |
|             # Add to the calendar
 | |
|             #
 | |
|             $calendar->add_entry($vtodo);
 | |
| 
 | |
|         }
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| #
 | |
| # Print the result
 | |
| #
 | |
| print $calendar->as_string;
 | |
| 
 | |
| exit;
 | |
| 
 | |
| #===  FUNCTION  ================================================================
 | |
| #         NAME: convert_date
 | |
| #      PURPOSE: Convert a textual date (ideally YYYY-MM-DD) to a Date::Calc
 | |
| #               date for the start of the given month.
 | |
| #   PARAMETERS: $textdate       date in text form
 | |
| #               $force          Boolean defining whether to skip validating
 | |
| #                               the date
 | |
| #      RETURNS: The start of the month in the textual date in Date::Calc
 | |
| #               format
 | |
| #  DESCRIPTION: Parses the date string and makes a Date::Calc date from the
 | |
| #               result where the day part is 1. Optionally checks that the
 | |
| #               date isn't in the past, though $force = 1 ignores this check.
 | |
| #       THROWS: No exceptions
 | |
| #     COMMENTS: Requires Date::Calc and Date::Parse
 | |
| #               Note the validation 'die' has a non-generic message
 | |
| #     SEE ALSO: N/A
 | |
| #===============================================================================
 | |
| sub convert_date {
 | |
|     my ( $textdate, $force ) = @_;
 | |
| 
 | |
|     my ( @today, @parsed, @startdate );
 | |
| 
 | |
|     #
 | |
|     # Reference date
 | |
|     #
 | |
|     @today = Today();
 | |
| 
 | |
|     #
 | |
|     # Parse and perform rudimentary validation on the $textdate date. Function
 | |
|     # 'strptime' returns "($ss,$mm,$hh,$day,$month,$year,$zone,$century)".
 | |
|     #
 | |
|     # The Date::Calc date $startdate[0] gets the returned year or the current
 | |
|     # year if no year was parsed, $startdate[1] gets the parsed month or the
 | |
|     # current month if no month was parsed, and $startdate[2] gets a day of 1.
 | |
|     #
 | |
|     @parsed    = strptime($textdate);
 | |
|     die "Unable to parse date '$textdate'\n" unless @parsed;
 | |
| 
 | |
|     @startdate = (
 | |
|         ( defined( $parsed[5] ) ? $parsed[5] + 1900 : $today[0] ),    # year
 | |
|         ( defined( $parsed[4] ) ? $parsed[4] + 1 : $today[1] ), 1
 | |
|     );
 | |
| 
 | |
|     #
 | |
|     # Unless we've overridden the check there should be a positive or zero
 | |
|     # difference in days between the target date and today's date to prevent
 | |
|     # going backwards in time.
 | |
|     #
 | |
|     unless ($force) {
 | |
|         unless ( Delta_Days( @today[ 0, 1 ], 1, @startdate ) ge 0 ) {
 | |
|             warn "Invalid date $textdate (in the past)\n";
 | |
|             die "Use -force to create a back-dated calendar\n";
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return @startdate;
 | |
| 
 | |
| }
 | |
| 
 | |
| #===  FUNCTION  ================================================================
 | |
| #         NAME: make_date
 | |
| #      PURPOSE: Make the event date for recurrence
 | |
| #   PARAMETERS: $refdate        An arrayref to the reference date array
 | |
| #                               (usually today's date)
 | |
| #               $dow            Day of week for the event date (1-7, 1=Monday)
 | |
| #               $n              The nth day of the week ($dow) in the given
 | |
| #                               month required for the event date ($dow=1,
 | |
| #                               $n=1 means first Monday)
 | |
| #               $offset         Number of days to offset the computed date
 | |
| #      RETURNS: The resulting date as a list for Date::Calc
 | |
| #  DESCRIPTION: We want to compute a simple date with an offset, such as
 | |
| #               "the Sunday before the first Monday of the month". We do
 | |
| #               this by computing a pre-offset date (first Monday of month)
 | |
| #               then apply the offset (Sunday before).
 | |
| #       THROWS: No exceptions
 | |
| #     COMMENTS: TODO Needs more testing to be considered truly universal
 | |
| #     SEE ALSO:
 | |
| #===============================================================================
 | |
| sub make_date {
 | |
|     my ( $refdate, $dow, $n, $offset ) = @_;
 | |
| 
 | |
|     #
 | |
|     # Compute the required date: the "$n"th day of week "$dow" in the year and
 | |
|     # month in @$refdate. This could be a date in the past.
 | |
|     #
 | |
|     my @date = Nth_Weekday_of_Month_Year( @$refdate[ 0, 1 ], $dow, $n );
 | |
| 
 | |
|     #
 | |
|     # If the computed date plus the offset is before the base date advance
 | |
|     # a month
 | |
|     #
 | |
|     if ( Day_of_Year(@date) + $offset < Day_of_Year(@$refdate) ) {
 | |
|         #
 | |
|         # Add a month and recompute
 | |
|         #
 | |
|         @date = Add_Delta_YM( @date, 0, 1 );
 | |
|         @date = Nth_Weekday_of_Month_Year( @date[ 0, 1 ], $dow, $n );
 | |
|     }
 | |
| 
 | |
|     #
 | |
|     # Apply the day offset
 | |
|     #
 | |
|     @date = Add_Delta_Days( @date, $offset ) if $offset;
 | |
| 
 | |
|     #
 | |
|     # Return a list
 | |
|     #
 | |
|     return (@date);
 | |
| }
 | |
| 
 | |
| #===  FUNCTION  ================================================================
 | |
| #         NAME: ISO8601_Date
 | |
| #      PURPOSE: Format a Date::Calc date in ISO8601 format
 | |
| #   PARAMETERS: @date   - a date in the Date::Calc format
 | |
| #      RETURNS: Text string containing a YYYY-MM-DD date
 | |
| #  DESCRIPTION: Just a convenience to allow a simple call like
 | |
| #               $str = ISO8601_Date(@date)
 | |
| #       THROWS: No exceptions
 | |
| #     COMMENTS: None
 | |
| #     SEE ALSO: N/A
 | |
| #===============================================================================
 | |
| sub ISO8601_Date {
 | |
|     my (@date) = (@_)[ 0, 1, 2 ];
 | |
| 
 | |
|     if ( check_date(@date) ) {
 | |
|         return sprintf( "%04d-%02d-%02d", @date );
 | |
|     }
 | |
|     else {
 | |
|         return "*Invalid Date*";
 | |
|     }
 | |
| }
 | |
| 
 | |
| #===  FUNCTION  ================================================================
 | |
| #         NAME: usage
 | |
| #      PURPOSE: Display a usage message and exit
 | |
| #   PARAMETERS: None
 | |
| #      RETURNS: To command line level with exit value 1
 | |
| #  DESCRIPTION: Builds the usage message using global values
 | |
| #       THROWS: no exceptions
 | |
| #     COMMENTS: none
 | |
| #     SEE ALSO: n/a
 | |
| #===============================================================================
 | |
| sub usage {
 | |
|     print STDERR <<EOD;
 | |
| Usage: $PROG [options] [FILE...]
 | |
| 
 | |
| $PROG v$VERSION
 | |
| 
 | |
| Makes a recurrent iCalendar meeting to be loaded into a calendar. Optionally
 | |
| adds reminders in the form of TODO items in relation to each meeting.
 | |
| 
 | |
|     -help               Display this information
 | |
|     -from=DATE          Start date for the calendar
 | |
|     -count=N            Number of entries; default 12
 | |
|     -[no]force          Allow a -from=DATE date before today; default not
 | |
|     -[no]reminder       Add a reminder TODO item; default no
 | |
| 
 | |
| EOD
 | |
| #    -summary=TEXT       Alternative text for the reminder (default 'Send out
 | |
| #                        CNews email')
 | |
|     exit(1);
 | |
| }
 | |
| 
 | |
| #===  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", "from=s", "count=i", "force!", "reminder!");
 | |
| #        "summary|rs=s" );
 | |
| 
 | |
|     if ( !GetOptions( $optref, @options ) ) {
 | |
|         usage();
 | |
|     }
 | |
| 
 | |
|     return;
 | |
| }
 | |
| 
 | |
| # vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
 |