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