aboutsummaryrefslogtreecommitdiff
path: root/bin/events2md.pl
blob: f9363f966295083fe950c9c5a8b33228e31e9470 (plain)
  1. #!/usr/bin/perl
  2. use v5.36;
  3. use utf8;
  4. use open qw(:std :encoding(UTF-8));
  5. use Feature::Compat::Try;
  6. use FindBin qw($Bin);
  7. use lib "$Bin/../lib";
  8. use Getopt::Complete (
  9. 'quiet!' => undef,
  10. 'verbose!' => undef,
  11. 'debug!' => undef,
  12. 'trace!' => undef,
  13. 'output' => undef,
  14. 'skeldir' => 'directories',
  15. 'username' => undef,
  16. 'password' => undef,
  17. 'locale' => undef,
  18. 'timezone' => undef,
  19. '<>' => undef,
  20. );
  21. use IO::Interactive::Tiny;
  22. use Log::Any qw($log);
  23. use Log::Any::Adapter;
  24. use URI;
  25. use DateTime;
  26. use Path::Tiny;
  27. use Text::Xslate;
  28. use POSIX qw(locale_h); # resolve LC_TIME
  29. use locale;
  30. use DateTime::TimeZone;
  31. use Object::Groupware::DAV;
  32. use Object::Groupware::Calendar;
  33. # collect settings from command-line options and defaults
  34. my $SKELDIR = $ARGS{skeldir} || $ENV{SKELDIR} || "$Bin/../templates";
  35. my $BASE_URI = $ARGS{'<>'}[0] || $ENV{CAL_DAV_URL_BASE};
  36. my $CALENDAR_URI = $ARGS{'<>'}[1] || $ENV{CAL_DAV_URL_CALENDAR};
  37. my $USERNAME = $ARGS{username} || $ENV{CAL_DAV_USER};
  38. my $PASSWORD = $ARGS{password} || $ENV{CAL_DAV_PASS};
  39. my $LOCALE = $ARGS{locale} || $ENV{CAL_LANG};
  40. my $TIME_ZONE = $ARGS{timezone};
  41. my $OUTPUT_FILE = $ARGS{output};
  42. # init logging
  43. my $LOGLEVEL = 'warning';
  44. $LOGLEVEL = 'critical' if $ARGS{quiet};
  45. $LOGLEVEL = 'warning' if defined $ARGS{verbose} and !$ARGS{verbose};
  46. $LOGLEVEL = 'info' if $ARGS{verbose};
  47. $LOGLEVEL = 'debug' if $ARGS{debug};
  48. $LOGLEVEL = 'trace' if $ARGS{trace};
  49. if ( IO::Interactive::Tiny::is_interactive() ) {
  50. Log::Any::Adapter->set( 'Screen', default_level => $LOGLEVEL );
  51. }
  52. else {
  53. use Log::Any::Adapter ( 'Stderr', default_level => $LOGLEVEL );
  54. }
  55. # extend DateTime locale with form LONGER
  56. # * omit year and second
  57. # * unabbreviate weekday and month
  58. # * interpose time preposition in combined date and time, where known
  59. my %at = (
  60. C => " 'at' ",
  61. ar => " 'في' ",
  62. da => " 'kl.' ",
  63. de => " 'um' ",
  64. en => " 'at' ",
  65. es => " 'a las' ",
  66. fr => " 'à' ",
  67. he => " 'בשעה' ",
  68. it => " 'alle' ",
  69. ja => "'に'",
  70. no => " 'kl.' ",
  71. ru => " 'в' ",
  72. zh => "'在'",
  73. );
  74. my $dt_locale = DateTime::Locale->load( $LOCALE || setlocale(LC_TIME) );
  75. my ( $locale, $lang ) = $dt_locale->code =~ /^((\w+)(?:-\w+)?)/;
  76. my $dt = DateTime->now( locale => $dt_locale );
  77. my %dt_locale_data = $dt_locale->locale_data;
  78. $dt_locale_data{code} = "${locale}-LONGER";
  79. $dt_locale_data{name} .= ' nouns unabbreviated';
  80. $dt_locale_data{date_format_medium} = $dt->locale->format_for('MMMMEd');
  81. $dt_locale_data{date_format_medium} ||= $dt->locale->format_for('MMMEd');
  82. $dt_locale_data{date_format_medium} =~ s/\bMMM\b/MMMM/;
  83. $dt_locale_data{date_format_medium} =~ s/\bMMM\b/MMMM/;
  84. $dt_locale_data{date_format_medium} =~ s/\bE\b/EEEE/;
  85. $dt_locale_data{time_format_medium} = $dt->locale->format_for('Hm');
  86. $dt_locale_data{datetime_format_medium}
  87. =~ s/^\{1\}\K,? (?=\{0\}$)/$at{$lang}/
  88. if $at{$lang};
  89. # init groupware settings
  90. my %GROUPWARE_OPTIONS = (
  91. dt_locale => DateTime::Locale::FromData->new( \%dt_locale_data ),
  92. dt_time_zone => DateTime::TimeZone->new(
  93. name => ( $ARGS{timezone} || 'local' ),
  94. ),
  95. );
  96. $log->infof(
  97. 'Will use locale %s and time zone %s',
  98. $GROUPWARE_OPTIONS{dt_locale}->code,
  99. $GROUPWARE_OPTIONS{dt_time_zone}->name,
  100. );
  101. # init calendar URIs
  102. $BASE_URI = URI->new($BASE_URI)
  103. or $log->fatal('failed to parse required base URI') && exit 2;
  104. $BASE_URI->scheme
  105. or $BASE_URI->scheme('file');
  106. # get calendar
  107. my $calendar;
  108. if ( $BASE_URI->scheme eq 'http' or $BASE_URI->scheme eq 'https' ) {
  109. $log->infof( 'will use base URI %s', $BASE_URI );
  110. $CALENDAR_URI = URI->new( $CALENDAR_URI || $BASE_URI );
  111. $CALENDAR_URI and $CALENDAR_URI->authority
  112. or $log->fatal('bad calendar URI: must be an internet URI') && exit 2;
  113. $BASE_URI->eq($CALENDAR_URI) and $CALENDAR_URI = undef
  114. or $log->infof( 'will use calendar URI %s', $CALENDAR_URI );
  115. my $session = Object::Groupware::DAV->new(
  116. user => $USERNAME,
  117. pass => $PASSWORD,
  118. uri => $BASE_URI,
  119. %GROUPWARE_OPTIONS,
  120. );
  121. $calendar = $session->get($CALENDAR_URI);
  122. }
  123. elsif ( $BASE_URI->scheme eq 'file' ) {
  124. defined $BASE_URI->file
  125. or $log->fatal('bad base URI: cannot open file') && exit 2;
  126. $log->infof( 'will use base URI %s', $BASE_URI );
  127. # parse local calendar data
  128. $log->debug('parse local calendar data...');
  129. my $path = path( $BASE_URI->file );
  130. if ( $path->is_file ) {
  131. $calendar = Object::Groupware::Calendar->new(
  132. filename => "$path",
  133. %GROUPWARE_OPTIONS,
  134. );
  135. }
  136. else {
  137. my $data;
  138. $path->visit( sub { $data .= $_->slurp_raw if $_->is_file } );
  139. $calendar = Object::Groupware::Calendar->new(
  140. data => $data,
  141. %GROUPWARE_OPTIONS,
  142. );
  143. }
  144. }
  145. # select subset of calendar events
  146. $log->debug('serialize calendar events...');
  147. my $start;
  148. if ( $ENV{CAL_DAV_NOW} ) {
  149. try { require DateTimeX::Easy }
  150. catch ($e) {
  151. $log->fatalf( 'failed parsing CAL_DAV_NOW: %s', $e ) && exit 2
  152. }
  153. $start = DateTimeX::Easy->new( $ENV{CAL_DAV_NOW} );
  154. $log->fatalf(
  155. 'failed parsing CAL_DAV_NOW: unknown start time "%s"',
  156. $ENV{CAL_DAV_NOW}
  157. )
  158. && exit 2
  159. unless defined $start;
  160. }
  161. $start ||= DateTime->now;
  162. my $end = $start->clone->add( months => 6 );
  163. my $span = DateTime::Span->from_datetimes( start => $start, end => $end );
  164. my @events = $calendar->events($span);
  165. # serialize calendar view
  166. if ($OUTPUT_FILE) {
  167. $OUTPUT_FILE = path($OUTPUT_FILE);
  168. $OUTPUT_FILE->parent->mkpath;
  169. $OUTPUT_FILE->remove;
  170. }
  171. my %vars;
  172. for (@events) {
  173. next unless $_->summary;
  174. push @{ $vars{events} }, $_;
  175. }
  176. my %tmpl;
  177. $tmpl{list} = path($SKELDIR)->child('list.md')->slurp_utf8;
  178. my $template = Text::Xslate->new(
  179. path => \%tmpl,
  180. syntax => 'TTerse',
  181. type => 'text',
  182. );
  183. my $content = $template->render( 'list', \%vars );
  184. if ($OUTPUT_FILE) {
  185. $OUTPUT_FILE->append_utf8($content);
  186. }
  187. else {
  188. print $content;
  189. }
  190. 1;