#!/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 = <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 <