forked from HPR/hpr-tools
		
	
		
			
	
	
		
			485 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
		
		
			
		
	
	
			485 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
|  | #!/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 |