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
|