Group
Extension

Mojolicious-Plugin-Ical/lib/Mojolicious/Plugin/Ical.pm

package Mojolicious::Plugin::Ical;
use Mojo::Base 'Mojolicious::Plugin';

use POSIX         ();
use Sys::Hostname ();
use Text::vFile::asData;

our $VERSION = '1.00';

sub register {
  my ($self, $app, $config) = @_;

  $config->{handler} //= 'ical';

  $self->{properties} = $config->{properties} || {};
  $self->{properties}{calscale}      ||= 'GREGORIAN';
  $self->{properties}{method}        ||= 'PUBLISH';
  $self->{properties}{prodid}        ||= sprintf '-//%s//NONSGML %s//EN', Sys::Hostname::hostname, $app->moniker;
  $self->{properties}{version}       ||= '2.0';
  $self->{properties}{x_wr_caldesc}  ||= '';
  $self->{properties}{x_wr_calname}  ||= $app->moniker;
  $self->{properties}{x_wr_timezone} ||= POSIX::strftime('%Z', localtime);

  $self->{vfile} ||= Text::vFile::asData->new;

  $app->helper('reply.ical' => sub { $self->_reply_ical(@_) });
  $app->types->type(ical => 'text/calendar');

  if ($config->{handler}) {
    $app->renderer->add_handler(
      $config->{handler},
      sub {
        my ($renderer, $c, $output, $options) = @_;
        return undef unless my $ical = $c->stash('ical');
        $$output = join '', map {"$_\n"} $self->_render_ical($c, $ical);
        return 1;
      }
    );
  }
}

sub _event_to_properties {
  my ($event, $defaults) = @_;
  my $properties = {};

  for my $k (keys %$event) {
    my $v = $event->{$k} //= '';
    my $p = _vkey($k);
    if (UNIVERSAL::isa($v, 'Mojo::Date')) {
      $v = $v->to_datetime;
      $v =~ s![:-]!!g;    # 1994-11-06T08:49:37Z => 19941106T084937Z
    }
    $properties->{$p} = [{value => $v}];
  }

  $properties->{DTSTAMP}  ||= [{value => $defaults->{now}}];
  $properties->{SEQUENCE} ||= [{value => 0}];
  $properties->{STATUS}   ||= [{value => 'CONFIRMED'}];
  $properties->{TRANSP}   ||= [{value => 'OPAQUE'}];
  $properties->{UID}      ||= [{value => sprintf '%s@%s', _md5($event), $defaults->{hostname}}];
  $properties;
}

sub _render_ical {
  my ($self, $c, $data) = @_;
  my %properties = %{$data->{properties} || {}};
  my $ical       = {};
  my %defaults;

  $ical->{objects}    = [];
  $ical->{properties} = {};
  $ical->{type}       = 'VCALENDAR';

  $properties{calscale}      ||= $self->{properties}{calscale};
  $properties{method}        ||= $self->{properties}{method};
  $properties{prodid}        ||= $self->{properties}{prodid};
  $properties{version}       ||= $self->{properties}{version};
  $properties{x_wr_caldesc}  ||= $self->{properties}{x_wr_caldesc};
  $properties{x_wr_calname}  ||= $self->{properties}{x_wr_calname};
  $properties{x_wr_timezone} ||= $self->{properties}{x_wr_timezone};

  for my $k (keys %properties) {
    my $p = _vkey($k);
    $ical->{properties}{$p} = [{value => $properties{$k}}];
  }

  $defaults{hostname} = Sys::Hostname::hostname;
  $defaults{now}      = Mojo::Date->new->to_datetime;
  $defaults{now} =~ s![:-]!!g;    # 1994-11-06T08:49:37Z => 19941106T084937Z

  for my $event (@{$data->{events} || []}) {
    push @{$ical->{objects}}, {properties => _event_to_properties($event, \%defaults), type => 'VEVENT'};
  }

  return $self->{vfile}->generate_lines($ical);
}

sub _reply_ical {
  my ($self, $c, $data) = @_;
  $c->res->headers->content_type('text/calendar');
  $c->render(text => join '', map {"$_\n"} $self->_render_ical($c, $data));
}

sub _md5 {
  my $data = $_[0];
  Mojo::Util::md5_sum(join ':', map {"$_=$data->{$_}"} grep { $_ ne 'dtstamp' } sort keys %$data);
}

sub _vkey {
  return $_[0] if $_[0] =~ /^[A-Z]/;
  local $_ = uc $_[0];
  s!_!-!g;
  $_;
}

1;

=encoding utf8

=head1 NAME

Mojolicious::Plugin::Ical - Generate .ical documents

=head1 VERSION

0.05

=head1 SYNOPSIS

=head2 Application

  use Mojolicious::Lite;
  plugin ical => {
    properties => {
      calscale      => "GREGORIAN"         # default GREGORIAN
      method        => "REQUEST",          # default PUBLISH
      prodid        => "-//ABC Corporation//NONSGML My Product//EN",
      version       => "1.0",              # default to 2.0
      x_wr_caldesc  => "Some description",
      x_wr_calname  => "My calender",
      x_wr_timezone => "EDT",              # default to timezone for localhost
    }
  };

  get '/calendar' => sub {
    my $c = shift;
    $c->reply->ical({
      events => [
        {
          created       => $date,
          description   => $str,   # http://www.kanzaki.com/docs/ical/description.html
          dtend         => $date,
          dtstamp       => $date,  # UTC time format, defaults to "now"
          dtstart       => $date,
          last_modified => $date,  # defaults to "now"
          location      => $str,   # http://www.kanzaki.com/docs/ical/location.html
          sequence      => $int,   # default 0
          status        => $str,   # default CONFIRMED
          summary       => $str,   # http://www.kanzaki.com/docs/ical/summary.html
          transp        => $str,   # default OPAQUE
          uid           => $str,   # default to md5 of the values @hostname
        },
        ...
      ],
    });
  };

  # or using respond_to()
  get '/events' => sub {
    my $c = shift;
    my $ical = { events => [...] };
    $c->respond_to(
      ical => {handler => 'ical', ical => $ical},
      json => {json => $ical}
    );
  };

=head1 DESCRIPTION

L<Mojolicious::Plugin::Ical> is a L<Mojolicious> plugin for generating
L<iCalendar|http://www.kanzaki.com/docs/ical/> documents.

This plugin will...

=over 4

=item *

Add the helper L</reply.ical>.

=item *

Add ".ical" type to L<Mojolicious/types>.

=item *

Add a handler "ical" to L<Mojolicious/renderer>.

=back

=head1 HELPERS

=head2 reply.ical

  $c = $c->reply->ical({ events => [...], properties => {...} });

Will render a iCal document with the Content-Type "text/calender".

C<events> is an array ref of calendar events.
C<properties> will override the defaults given to L</register>.

See L</SYNOPSIS> for more details.

=head1 METHODS

=head2 register

  plugin ical => {properties => {...}};

Register L</reply.ical> helper.

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2014, Jan Henning Thorsen

This program is free software, you can redistribute it and/or modify it under
the terms of the Artistic License version 2.0.

=head1 AUTHOR

Jan Henning Thorsen - C<jhthorsen@cpan.org>

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.