hpr-tools/Community_News/make_meeting

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