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