summaryrefslogtreecommitdiff
path: root/bin/events2md.pl
blob: abcfd757a3f27ae7a19050bd228700b076733869 (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. if ( $event->{ATTENDEE} ) {
  170. $description .= "\nMed ";
  171. $description .= join ' og ',
  172. map { $_->{CN} || $_->{value} =~ s/^mailto://r }
  173. @{ $event->{ATTENDEE} };
  174. $description .= '.';
  175. }
  176. my $location = unescape( $event->{LOCATION} );
  177. my $time_begin = ucfirst( $event->{DTSTART}->strftime('%A') );
  178. $time_begin .= $event->{DTSTART}->strftime(' %e. %B kl. %k.%M');
  179. my $time_end = $event->{DTEND}->strftime('%k.%M');
  180. my %attachments;
  181. if ( $calendar_event->property('attach') ) {
  182. for ( @{ $calendar_event->property('attach') } ) {
  183. my $uri = try { URI->new( $_->value ) }
  184. or next;
  185. $uri->authority and $uri->host
  186. or next;
  187. push @{ $attachments{ $uri->host } }, $uri;
  188. }
  189. }
  190. $_ = <<"STRING";
  191. ### $time_begin. $summary
  192. $description
  193. STRING
  194. $_ .= "**Mødested:** $location \n"
  195. if $location;
  196. $_ .= "**Tid:** ${time_begin}-${time_end}. \n"
  197. if $time_begin and $time_end;
  198. $_ .= "**Pris:** $price \n"
  199. if $price;
  200. $_ .= "[Køb billet på Billetto]($attachments{'billetto.dk'}[0]) \n"
  201. if $attachments{'billetto.dk'};
  202. $_ .= "\n---\n\n";
  203. if ($path) {
  204. $path->append_utf8($_);
  205. }
  206. else {
  207. print $_;
  208. }
  209. }
  210. # unescape characters in iCalendar TEXT content strings
  211. # should fully cover iCalendar 2.0 (rfc5545 and rfc6868)
  212. sub unescape
  213. {
  214. ($_) = @_;
  215. s/\^n|\\[Nn]/\n/g;
  216. s/\^\'/\"/g;
  217. s/\^\^/^/g;
  218. s/\\([\\;,])/$1/g;
  219. return $_;
  220. }
  221. 1;