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