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