#!/usr/bin/perl use v5.36; use utf8; use open qw(:std :encoding(UTF-8)); use Feature::Compat::Try; use FindBin qw($Bin); use lib "$Bin/../lib"; use IO::Interactive::Tiny; use Log::Any qw($log); use Log::Any::Adapter; use URI; use DateTime; use Path::Tiny; use Text::Xslate; use POSIX qw(locale_h); # resolve LC_TIME use locale; use DateTime::TimeZone; use Object::Groupware::DAV; use Object::Groupware::Calendar; if ( IO::Interactive::Tiny::is_interactive() ) { Log::Any::Adapter->set( 'Screen', default_level => 'info' ); } # set defaults and parse command-line options my ($BASE_URI, $CALENDAR_URI, $SKELDIR, $OUTPUT_FILE, $CALENDAR_LANG, $CALENDAR_TIME_ZONE, %GROUPWARE_OPTIONS ); $BASE_URI = $ENV{CAL_DAV_URL_BASE}; $CALENDAR_URI = $ENV{CAL_DAV_URL_CALENDAR}; $SKELDIR = $ENV{SKELDIR} || "$Bin/../templates"; $BASE_URI ||= shift @ARGV if @ARGV; $CALENDAR_URI ||= shift @ARGV if @ARGV; $OUTPUT_FILE = shift @ARGV if @ARGV; $CALENDAR_LANG = $ENV{CAL_LANG} || setlocale(LC_TIME); $CALENDAR_TIME_ZONE = DateTime::TimeZone->new( name => ( $ENV{CAL_TIME_ZONE} || 'local' ), ); # extend DateTime locale with form LONGER # * omit year and second # * unabbreviate weekday and month # * interpose time preposition in combined date and time, where known my %at = ( C => " 'at' ", ar => " 'في' ", da => " 'kl.' ", de => " 'um' ", en => " 'at' ", es => " 'a las' ", fr => " 'à' ", he => " 'בשעה' ", it => " 'alle' ", ja => "'に'", no => " 'kl.' ", ru => " 'в' ", zh => "'在'", ); my $dt_locale = DateTime::Locale->load($CALENDAR_LANG); my ( $locale, $lang ) = $dt_locale->code =~ /^((\w+)(?:-\w+)?)/; my $dt = DateTime->now( locale => $dt_locale ); my %dt_locale_data = $dt_locale->locale_data; $dt_locale_data{code} = "${locale}-LONGER"; $dt_locale_data{name} .= ' nouns unabbreviated'; $dt_locale_data{date_format_medium} = $dt->locale->format_for('MMMMEd'); $dt_locale_data{date_format_medium} ||= $dt->locale->format_for('MMMEd'); $dt_locale_data{date_format_medium} =~ s/\bMMM\b/MMMM/; $dt_locale_data{date_format_medium} =~ s/\bMMM\b/MMMM/; $dt_locale_data{date_format_medium} =~ s/\bE\b/EEEE/; $dt_locale_data{time_format_medium} = $dt->locale->format_for('Hm'); $dt_locale_data{datetime_format_medium} =~ s/^\{1\}\K,? (?=\{0\}$)/$at{$lang}/ if $at{$lang}; %GROUPWARE_OPTIONS = ( dt_locale => DateTime::Locale::FromData->new( \%dt_locale_data ), dt_time_zone => $CALENDAR_TIME_ZONE, ); $log->infof( 'Will use locale %s and time zone %s', $GROUPWARE_OPTIONS{dt_locale}->code, $GROUPWARE_OPTIONS{dt_time_zone}->name, ); # resolve calendar URIs my ( $base_uri, $calendar_uri, $calendar ); $base_uri = URI->new($BASE_URI) if ($BASE_URI); $base_uri or $log->fatal('required base URI not provided') && exit 2; $base_uri->scheme or $base_uri->scheme('file'); if ( $base_uri->scheme eq 'http' or $base_uri->scheme eq 'https' ) { $log->infof( 'will use base URI %s', $base_uri ); $calendar_uri = URI->new( $CALENDAR_URI || $base_uri ); $calendar_uri and $calendar_uri->authority or $log->fatal('bad calendar URI: must be an internet URI') && exit 2; $base_uri->eq($calendar_uri) and $calendar_uri = undef or $log->infof( 'will use calendar URI %s', $calendar_uri ); my $session = Object::Groupware::DAV->new( user => $ENV{CAL_DAV_USER}, pass => $ENV{CAL_DAV_PASS}, uri => $base_uri, %GROUPWARE_OPTIONS, ); $calendar = $session->get($calendar_uri); } elsif ( $base_uri->scheme eq 'file' ) { defined $base_uri->file or $log->fatal('bad base URI: cannot open file') && exit 2; $log->infof( 'will use base URI %s', $base_uri ); # parse local calendar data $log->debug('parse local calendar data...'); my $path = path( $base_uri->file ); if ( $path->is_file ) { $calendar = Object::Groupware::Calendar->new( filename => "$path", %GROUPWARE_OPTIONS, ); } else { my $data; $path->visit( sub { $data .= $_->slurp_raw if $_->is_file } ); $calendar = Object::Groupware::Calendar->new( data => $data, %GROUPWARE_OPTIONS, ); } } if ( $log->is_trace ) { use DDP; p $calendar; } # select subset of calendar events $log->debug('serialize calendar events...'); my $start; if ( $ENV{CAL_DAV_NOW} ) { try { require DateTimeX::Easy } catch ($e) { $log->fatalf( 'failed parsing CAL_DAV_NOW: %s', $e ) && exit 2 } $start = DateTimeX::Easy->new( $ENV{CAL_DAV_NOW} ); $log->fatalf( 'failed parsing CAL_DAV_NOW: unknown start time "%s"', $ENV{CAL_DAV_NOW} ) && exit 2 unless defined $start; } $start ||= DateTime->now; my $end = $start->clone->add( months => 6 ); my $span = DateTime::Span->from_datetimes( start => $start, end => $end ); my @events = $calendar->events($span); # serialize calendar view my $output_path; if ($OUTPUT_FILE) { $output_path = path($OUTPUT_FILE); $output_path->parent->mkpath; $output_path->remove; } my %vars; for (@events) { next unless $_->summary; push @{ $vars{events} }, $_; } my %tmpl; $tmpl{list} = path($SKELDIR)->child('list.md')->slurp_utf8; my $template = Text::Xslate->new( path => \%tmpl, syntax => 'TTerse', type => 'text', ); my $content = $template->render( 'list', \%vars ); if ($output_path) { $output_path->append_utf8($content); } else { print $content; } 1;