aboutsummaryrefslogtreecommitdiff
path: root/bin/events2md.pl
blob: 4272291529eab4c14e32960f28bae25ca74a8c02 (plain)
  1. #!/usr/bin/perl
  2. use v5.14;
  3. use utf8;
  4. use open qw(:std :encoding(UTF-8));
  5. use strictures;
  6. use autodie;
  7. use Feature::Compat::Try;
  8. use FindBin qw($Bin);
  9. use POSIX qw(locale_h);
  10. use locale;
  11. use Encode qw(decode_utf8); # TODO: modernize CalDAV access instead
  12. use Net::Netrc;
  13. use List::Util qw(first);
  14. use IO::Interactive::Tiny;
  15. use Log::Any qw($log);
  16. use Log::Any::Adapter;
  17. use URI;
  18. use IO::Prompter;
  19. use Cal::DAV;
  20. use Data::ICal::DateTime;
  21. use DateTime;
  22. use Path::Tiny;
  23. use Text::Xslate;
  24. if ( IO::Interactive::Tiny::is_interactive() ) {
  25. Log::Any::Adapter->set( 'Screen', default_level => 'info' );
  26. }
  27. # set defaults and parse command-line options
  28. my ( $BASE_URI, $CALENDAR_URI, $SKELDIR, $OUTPUT_FILE );
  29. $BASE_URI = $ENV{CAL_DAV_URL_BASE};
  30. $CALENDAR_URI = $ENV{CAL_DAV_URL_CALENDAR};
  31. $SKELDIR = $ENV{SKELDIR} || "$Bin/../templates";
  32. $BASE_URI ||= shift @ARGV
  33. if @ARGV;
  34. $CALENDAR_URI ||= shift @ARGV
  35. if @ARGV;
  36. $OUTPUT_FILE = shift @ARGV
  37. if @ARGV;
  38. # use system locale to format DateTime objects parsed from iCal data
  39. DateTime->DefaultLocale( setlocale(LC_TIME) );
  40. # resolve calendar URIs
  41. my ( $base_uri, $calendar_uri, $calendar );
  42. $base_uri = URI->new($BASE_URI)
  43. if ($BASE_URI);
  44. $base_uri
  45. or $log->fatal('required base URI not provided') && exit 2;
  46. $base_uri->scheme
  47. or $base_uri->scheme('file');
  48. if ( $base_uri->scheme eq 'http' or $base_uri->scheme eq 'https' ) {
  49. $log->infof( 'will use base URI %s', $base_uri );
  50. $calendar_uri = URI->new( $CALENDAR_URI || $base_uri );
  51. $calendar_uri and $calendar_uri->authority
  52. or $log->fatal('bad calendar URI: must be an internet URI') && exit 2;
  53. $base_uri->eq($calendar_uri) and $calendar_uri = undef
  54. or $log->infof( 'will use calendar URI %s', $calendar_uri );
  55. # resolve credentials
  56. $log->debug('resolve credentials...');
  57. my ( $mach, $user, $pass );
  58. ( $user, $pass ) = split ':', $base_uri->userinfo
  59. if $base_uri->userinfo;
  60. $user ||= $ENV{CAL_DAV_USER};
  61. $pass ||= $ENV{CAL_DAV_PASS};
  62. $mach = Net::Netrc->lookup( $base_uri->host, $user )
  63. if !$user or !$pass;
  64. if ($mach) {
  65. $user ||= $mach->login;
  66. $pass ||= $mach->password;
  67. $log->infof(
  68. 'will use .netrc provided credentials for user %s',
  69. $user
  70. );
  71. }
  72. elsif ( IO::Interactive::Tiny::is_interactive() ) {
  73. $log->warn(
  74. 'will ask for missing info - this will fail in headless mode');
  75. $user ||= prompt 'Enter your username';
  76. $pass ||= prompt 'Enter your password', -echo => '*';
  77. }
  78. $log->debugf( 'resolved credentials for user %s', $user );
  79. # fetch and parse CalDAV calendar data
  80. $log->debug('fetch and parse CalDAV calendar data...');
  81. $calendar = Cal::DAV->new(
  82. user => $user,
  83. pass => $pass,
  84. url => $base_uri,
  85. );
  86. $calendar->get($calendar_uri)
  87. if $calendar_uri;
  88. }
  89. elsif ( $base_uri->scheme eq 'file' ) {
  90. defined $base_uri->file
  91. or $log->fatal('bad base URI: cannot open file') && exit 2;
  92. $log->infof( 'will use base URI %s', $base_uri );
  93. # parse local calendar data
  94. $log->debug('parse local calendar data...');
  95. my $path = path( $base_uri->file );
  96. if ( $path->is_file ) {
  97. $calendar = Data::ICal->new( data => $path->slurp_raw );
  98. }
  99. else {
  100. my $data;
  101. $path->visit( sub { $data .= $_->slurp_raw if $_->is_file } );
  102. $calendar = Data::ICal->new( data => $data );
  103. }
  104. }
  105. if ( $log->is_trace ) {
  106. use DDP;
  107. p $calendar;
  108. }
  109. # TODO: if list is empty and no calendar uri was explicitly supplied,
  110. # warn on stdout with list of abailable collections using this sequence:
  111. # 1. PROPFIND on base-URL for {DAV:}current-user-principal
  112. # 2. PROPFIND for calendar-home-set property in caldav namespace
  113. # 3. PROPFIND with depth: 1
  114. # as documented at <https://stackoverflow.com/a/11673483>
  115. # serialize calendar events
  116. $log->debug('serialize calendar events...');
  117. my $start;
  118. if ( $ENV{CAL_DAV_NOW} ) {
  119. try { require DateTimeX::Easy }
  120. catch ($e) {
  121. $log->fatalf( 'failed parsing CAL_DAV_NOW: %s', $e ) && exit 2
  122. }
  123. $start = DateTimeX::Easy->new( $ENV{CAL_DAV_NOW} );
  124. $log->fatalf(
  125. 'failed parsing CAL_DAV_NOW: unknown start time "%s"',
  126. $ENV{CAL_DAV_NOW}
  127. )
  128. && exit 2
  129. unless defined $start;
  130. }
  131. $start ||= DateTime->now;
  132. my $end = $start->clone->add( months => 6 );
  133. $log->infof( 'will pick events between %s and %s', $start, $end );
  134. my $span = DateTime::Span->from_datetimes( start => $start, end => $end );
  135. my @events = sort {
  136. DateTime->compare( $a->start, $b->start )
  137. || DateTime->compare( $a->end, $b->end )
  138. || get_property_string( $a, 'summary' )
  139. cmp get_property_string( $b, 'summary' )
  140. } $calendar->events($span);
  141. if ( $log->is_trace ) {
  142. use DDP;
  143. p @events;
  144. }
  145. my $output_path;
  146. if ($OUTPUT_FILE) {
  147. $output_path = path($OUTPUT_FILE);
  148. $output_path->parent->mkpath;
  149. $output_path->remove;
  150. }
  151. my %vars;
  152. for (@events) {
  153. next unless $_->summary;
  154. push @{ $vars{events} }, resolve_event( $_, $_->start, $_->end );
  155. }
  156. my %tmpl;
  157. $tmpl{list} = path($SKELDIR)->child('list.md')->slurp_utf8;
  158. my $template = Text::Xslate->new(
  159. path => \%tmpl,
  160. syntax => 'TTerse',
  161. type => 'text',
  162. );
  163. my $content = $template->render( 'list', \%vars );
  164. if ($output_path) {
  165. $output_path->append_utf8($content);
  166. }
  167. else {
  168. print $content;
  169. }
  170. sub resolve_event
  171. {
  172. my ( $entry, $start, $end, $path ) = @_;
  173. if ( $log->is_trace ) {
  174. use DDP;
  175. p $entry;
  176. p $start;
  177. p $end;
  178. p $path;
  179. }
  180. my $summary = get_property_string( $entry, 'summary' );
  181. my $description = get_property_string( $entry, 'description' );
  182. $description =~ s/\n\n[Pp]ris:\s*((?!\n).+)\s*\z//m;
  183. my $price = $1;
  184. my @attendees;
  185. if ( $entry->property('attendee') ) {
  186. for ( @{ $entry->property('attendee') } ) {
  187. push @attendees, decode_utf8 $_->parameters->{'CN'}
  188. || $_->value =~ s/^mailto://r;
  189. }
  190. }
  191. my $location = get_property_string( $entry, 'location' );
  192. my $date_begin = $start->strftime('%A %e. %B');
  193. my $time_begin = $start->strftime('%k.%M');
  194. my $date_end = $end->strftime('%A %e. %B');
  195. my $time_end = $end->strftime('%k.%M');
  196. my $datespan
  197. = ( defined($end) and $date_end ne $date_begin )
  198. ? ucfirst("$date_begin - $date_end")
  199. : ucfirst("$date_begin");
  200. my $timespan
  201. = ( defined($end) and not $_->all_day )
  202. ? ucfirst("$date_begin kl. $time_begin-$time_end")
  203. : undef;
  204. my $time_brief
  205. = $_->all_day
  206. ? $datespan
  207. : ucfirst("$date_begin kl. $time_begin");
  208. my @attachments;
  209. if ( $entry->property('attach') ) {
  210. for ( @{ $entry->property('attach') } ) {
  211. my $uri;
  212. try { $uri = URI->new( $_->value ) }
  213. catch ($e) {
  214. $log->errorf( 'failed to parse URI %s: %s', $uri, $e );
  215. next;
  216. }
  217. $uri->authority and $uri->host
  218. or next;
  219. push @attachments, $uri;
  220. }
  221. }
  222. return {
  223. time_brief => $time_brief,
  224. summary => $summary,
  225. description => $description,
  226. attendees => [@attendees],
  227. location => $location,
  228. timespan => $timespan,
  229. price => $price,
  230. attachments => [@attachments],
  231. };
  232. }
  233. sub get_property_string
  234. {
  235. my ( $entry, $key ) = @_;
  236. return ''
  237. unless $entry->property($key);
  238. return decode_utf8 $entry->property($key)->[0]->value;
  239. }
  240. 1;