Hi,

On Fri, Mar 4, 2011 at 11:54 PM, Ben <[email protected]>wrote:

> I was wondering how your perl interface for resource management was working
> and if you'd be willing to share the code? I'd be interested in testing  /
> debuging / using it.
>

Here's how it works.

For the particular organization for which the solution was devised - the
criteria for resource booking is pretty simple - first come, first serve.

So, keeping that in mind, the process involves calculating the appropriate
recurrence ( in case of recurring events ), breaking 'em down and comparing
for conflicts. Otherwise, a straightforward affair. If there's a conflict,
that particular user's event will be rejected. Infinite events are rejected
as well.

There are some great modules that perl provides which we use -
http://search.cpan.org/~alexmv/Data-ICal-0.16/ and
http://search.cpan.org/~simonw/Data-ICal-DateTime-0.7/

Currently, the code is integrated with the email sub-system ( based on qmail
). Also, the code can be made a lot efficient if I could use object-oriented
paradigms. But anyway I am attaching the particular script.

You would probably want to avoid the email parts.

So, mainly there are two queues - the first reads off the iCal objects and
the other one puts a paticular mail in a separate queue ( $m_dirq ) ..
.which you would avoid, unless in a similar situation.

Let me know of any problems - the script is quite specific - and hasn't been
generalized, so there might be a few things to watch out for.
#!/usr/bin/perl

use strict;
use warnings;

use POSIX;
use HTTP::DAV;
use File::Copy;
use File::Path;
use MIME::Lite::TT;
use Linux::Inotify2;
use Directory::Queue;
use Data::ICal::DateTime;

my $resource    = $ARGV[0];
my $resource_cn = $ARGV[1];
my $tmp_dir     = "/tmp/cal_$resource";
my $queue_dir   = "/tmp/queue_$resource";
my $m_queue_dir = "/tmp/m_queue_$resource";
my $m_schema    = {"body" => "table"};
my $mq_name;
my $schema = {"body" => "string"};
my $alias = "/home/caladmin";
my $d;
my ($ne_path, $nc_path);
my $cal;
my $nc_cal;
my $nc_events;
my $e;
my $oldmod;
my $conflicts;
my $dirq;
my $name;
my %data;
my $ce;

$| = 1;

sub catch_sig {
        my $signame = shift;
        die " Caught TERM \n\n\n";
}

$SIG{TERM} = \&catch_sig;

sub auth
{
    my $url = shift;
    $d->credentials(
                    -user  => "cal.admin",
                    -pass  => "cal246",
                    -url   => $url,
                    -realm => "SOGo"
                   );
    $d->open(-url => $url);
}

sub end
{
    shift->properties()->{dtend}->[0]->decoded_value();
}

sub cancel_val
{
    shift->properties()->{method}->[0]->decoded_value();
}

sub sequence
{
    shift->properties()->{sequence}->[0]->decoded_value();
}

sub organizer
{
    shift->properties()->{organizer}->[0]->parameters->{CN};
}

sub lastmod
{
    shift->properties()->{'last-modified'}->[0]->decoded_value();
}

sub dstart
{
    shift->properties()->{dtstart}->[0]->decoded_value();
}

sub dend
{
    shift->properties()->{dtend}->[0]->decoded_value();
}

sub pstat
{
    my $ev  = shift;
    my $att = scalar @{$ev->properties()->{attendee}} - 1;
    my $cn;
    my $loc;
    foreach my $i (0 .. $att)
    {
        $cn = $ev->properties()->{attendee}->[$i]->parameters->{CN};
        if ($cn eq "$resource_cn")
        {
            $loc = $i;
        }
    }
    return $ev->properties()->{'attendee'}->[$loc]->parameters()->{'PARTSTAT'};

}

sub set_pstat
{
    my ($ev, $val) = @_;

    my $att = scalar @{$ev->properties()->{attendee}} - 1;
    my $cn;
    my $loc;
    foreach my $i (0 .. $att)
    {
        $cn = $ev->properties()->{attendee}->[$i]->parameters->{CN};
        if ($cn eq "$resource_cn")
        {
            $loc = $i;
        }
    }
    $ev->properties()->{'attendee'}->[$loc]->parameters()->{'PARTSTAT'} = $val;

}

sub rec
{
    my $ev        = shift;
    my $ev_span   = $ev->recurrence;
    my $iter      = $ev_span->iterator;
    my $ev_cal    = Data::ICal->new(filename => $nc_path);
    my @ev_events = $ev_cal->events();
    foreach my $i (@ev_events)
    {
        if ($i->uid eq $ev->uid and !$i->recurrence)
        {
            while (my $dt = $iter->next)
            {
                if ($dt->ymd eq $i->start->ymd)
                {
                    $ev_span = $ev_span->complement($dt);
                }
            }
        }
    }

    return $ev_span;
}

sub events_conflict
{
    my ($e1, $e2) = @_;
    my $e1_span;
    my $e2_span;
    if (!$e1->recurrence and !$e2->recurrence)
    {
        $e1_span =
          DateTime::Span->from_datetimes(start => $e1->start, end => $e1->end);
        $e2_span =
          DateTime::Span->from_datetimes(start => $e2->start, end => $e2->end);

        return 1
          if (    $e1_span->intersects($e2_span)
              and $e1->end   ne $e2->start
              and $e1->start ne $e2->end);
    } elsif ($e1->recurrence)
    {
        my $recur = rec($e1);
        my @evts  = $e1->explode($recur);
        foreach my $e (@evts)
        {
            return 1 if events_conflict($e2, $e);
        }
    } elsif ($e2->recurrence)
    {

        return events_conflict($e2, $e1);
    }
}

sub r_end
{
    my $ev              = shift;
    my $recur           = $ev->recurrence;
    my @evts            = $ev->explode($recur);
    my $last_recur_e    = pop(@evts);
    my $d_cal           = Data::ICal->new(filename => $nc_path);
    my @d_events        = $d_cal->events();
    my $check           = 0;
    my $last_rrule_date = $ev->recurrence->max;
    my $exdate_span;
    my $ev_end;
    my $iter_exdate;

    if ($ev->exdate)
    {
        $exdate_span = $ev->exdate;
        $iter_exdate = $exdate_span->iterator;
        while (my $dt_exdate = $iter_exdate->next)
        {
            if ($dt_exdate eq $last_rrule_date)
            {
                $check = 1;
            }
        }
    } else
    {
        foreach my $i (@d_events)
        {
            if (    $i->uid eq $ev->uid
                and $i->recurrence_id
                and $i->recurrence_id eq $last_rrule_date)
            {
                $ev_end = end($i);
            } elsif (    $i->uid eq $ev->uid
                     and $i->recurrence)
            {
                $ev_end = end($last_recur_e);
            }
        }
    }

    if ($check)
    {
        foreach my $i (@d_events)
        {
            if (    $i->uid eq $ev->uid
                and $i->recurrence_id
                and $i->recurrence_id eq $last_recur_e->start)
            {
                $ev_end = end($i);
            }
        }
    }

    return $ev_end;
}

sub nc_cal_check
{
    my $ev = shift;
    my $ev_end;
    if (!$ev->recurrence and !$ev->recurrence_id)
    {
        $ev_end = end($ev);
    } elsif ($ev->recurrence and !$ev->recurrence_id)
    {
        $ev_end = r_end($ev);
    } elsif ($ev->recurrence_id)
    {
        my $rcal = Data::ICal->new(filename => $nc_path);
        my @revents = $rcal->events();
        foreach my $i (@revents)
        {
            if (    $i->uid eq $ev->uid
                and $i->recurrence)
            {
                $ev_end = r_end($i);
            }
        }
    }
    if ($ev_end lt strftime("%Y%m%dT%H%M%SZ", gmtime(time)))
    {
        return 1;
    }

}

sub maildir_name
{
    my $ev = shift;
    if ($ev->recurrence_id)
    {
        return $ev->uid . "--" . $ev->recurrence_id . "--" . sequence($ev);
    } else
    {
        if ($ev->properties()->{sequence})
        {
            return $ev->uid . "--" . sequence($ev);
        } else
        {
            return $ev->uid;
        }
    }
}

my $m_dirq = Directory::Queue->new(path => $m_queue_dir, schema => $m_schema);

sub maildir_flush
{
    my ($maildir, $num_of_att) = @_;
    my $m_ref = {
                 "$maildir"  => "$num_of_att",
                 "operation" => "send"
                };
    $mq_name = $m_dirq->add(body => $m_ref);
}

sub maildir_delete
{
    my ($maildir, $num_of_att) = @_;
    my $m_ref = {
                 "$maildir"  => "$num_of_att",
                 "operation" => "delete"
                };
    $mq_name = $m_dirq->add(body => $m_ref);
}

sub num_of_attendees
{
    my $ev        = shift;
    my $num       = scalar @{$ev->properties()->{attendee}};
    my $att_count = scalar @{$ev->properties()->{attendee}} - 1;
    foreach my $i (0 .. $att_count)
    {
        if ($ev->properties()->{attendee}->[$i]->parameters()->{PARTSTAT} eq
            'DECLINED')
        {
            $num -= 1;
        }
    }
    return $num;
}

sub new_accept_event
{
    my $ev   = shift;
    my $euid = $ev->uid;
    my $ev_cal;
    my $num_of_att = num_of_attendees($ev);
    my $mdir       = maildir_name($ev);
    my $url        = "http://calendar.foo.com/SOGo/dav/$resource/Calendar/personal";;

    if (-f "$tmp_dir/ev.ics")
    {
        unlink "$tmp_dir/ev.ics";
    }

    maildir_flush("$alias/$mdir/", $num_of_att);

    auth($url);
    $d->get(-url => "$url/$euid.ics", to => "$tmp_dir/ev.ics");
    $ev_cal = Data::ICal->new(filename => "$tmp_dir/ev.ics");
    my @evs = $ev_cal->events();
    foreach my $i (@evs)
    {
        if (pstat($i) eq 'NEEDS-ACTION')
        {
            set_pstat($i, 'ACCEPTED');
        }
    }
    open(my $afh, '>', "$tmp_dir/ev.ics");
    print $afh $ev_cal->as_string;
    close($afh);
    $d->put(-local => "$tmp_dir/ev.ics", -url => "$url/$euid.ics");
}

sub new_reject_event
{

    my $ev   = shift;
    my $euid = $ev->uid;
    my $ev_cal;
    my $organizer = $ev->properties()->{organizer}->[0]->parameters->{CN};
    my $url       = "http://calendar.foo.com/SOGo/dav/$organizer/Calendar/personal/";;
    my $multiple  = 0;

    if (-f "$tmp_dir/ev.ics")
    {
        unlink "$tmp_dir/ev.ics";
    }

    auth($url);
    $d->get(-url => "$url/$euid.ics", to => "$tmp_dir/ev.ics");
    $ev_cal = Data::ICal->new(filename => "$tmp_dir/ev.ics");
    my $evs = [];
    @$evs = $ev_cal->events();
    if (scalar @{$ev_cal->entries} > 1)
    {
        $multiple = 1;
        foreach my $i (0 .. $#$evs)
        {
            my $ae = $evs->[$i];

            if (pstat($ae) eq 'NEEDS-ACTION')
            {
                splice(@{$ev_cal->entries}, $i, 1);
            }
        }
    }
    open(my $afh, '>', "$tmp_dir/ev.ics");
    print $afh $ev_cal->as_string;
    close($afh);
    if ($multiple)
    {
        $d->put(-local => "$tmp_dir/ev.ics",
                -url   => "$url/$euid.ics");
    } else
    {
        $d->delete(-url => "$url/$euid.ics");
    }
    my $mdir_invitations = maildir_name($ev);
    my $num_of_att       = num_of_attendees($ev);
    maildir_delete("$alias/$mdir_invitations/", $num_of_att);
}

sub date_time
{
    my ($ev, $t) = @_;
    my ($yr, $mt, $d, $hr, $min, $s) =
      $t =~ m/(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)Z/;
    my $dt = DateTime->new(
                           year   => $yr,
                           month  => $mt,
                           day    => $d,
                           hour   => $hr,
                           minute => $min,
                           second => $s
                          );

    $dt->add(hours   => 5,
             minutes => 30);
    return $dt;
}

sub reject_mail
{
    my ($c_e, $n_c_e) = @_;
    my $org = organizer($c_e);
    my %params;
    my %options;
    $params{resource}           = $resource_cn;
    $params{c_organizer}        = $org;
    $params{summary}            = $n_c_e->summary;
    $params{nc_organizer}       = organizer($n_c_e);
    $params{last_modified_time} = date_time($n_c_e, lastmod($n_c_e))->hms;
    $params{last_modified_date} = date_time($n_c_e, lastmod($n_c_e))->dmy;
    $params{start_time}         = date_time($n_c_e, dstart($n_c_e))->hms;
    $params{end_time}           = date_time($n_c_e, dend($n_c_e))->hms;
    $params{start_date}         = date_time($n_c_e, dstart($n_c_e))->dmy;
    $params{end_date}           = date_time($n_c_e, dend($n_c_e))->dmy;
    $options{INCLUDE_PATH}      = '/home/caladmin/templates';
    my $msg = MIME::Lite::TT->new(
                                  From        => '[email protected]',
                                  To          => "$org\@foo.com",
                                  Subject     => "Resource booking rejected",
                                  Template    => 'ev.txt.tt',
                                  TmplOptions => \%options,
                                  TmplParams  => \%params
                                 );
    $msg->send;

}

sub cutjob
{
    my $cut_e          = shift;
    my $euid           = $cut_e->uid;
    my $to_be_cut      = undef;
    my $cut_flag       = 0;
    my $uid_flag       = 0;
    my $caladmin_index = undef;
    my @array_cut      = ();
    my $cut_cal        = Data::ICal->new(filename => $nc_path);
    my $cut_cal_events = [];
    @$cut_cal_events = $cut_cal->events();
    my $url = "http://calendar.foo.com/SOGo/dav/$resource/Calendar/personal";;

    foreach my $i (0 .. $#$cut_cal_events)
    {
        my $cce = $cut_cal_events->[$i];

        if ($cut_e->uid eq $cce->uid)
        {
            $uid_flag = 1;
            if (!$cut_e->recurrence_id and !$cut_e->recurrence)
            {
                $to_be_cut = $i;
            } elsif (    $cut_e->recurrence_id
                     and $cce->recurrence_id
                     and $cut_e->recurrence_id eq $cce->recurrence_id)
            {
                $to_be_cut = $i;
                $cut_flag  = 1;
            } elsif ($cut_e->recurrence)
            {
                unshift(@array_cut, $i);
            } elsif ($cut_e->recurrence_id and $cce->recurrence)
            {
                $caladmin_index = $i;
            }
        }
    }

    if (defined $to_be_cut)
    {
        if (!$cut_flag)
        {
            splice(@{$cut_cal->entries}, $to_be_cut, 1);
            save_cal($cut_cal);
        } elsif ($cut_flag)
        {
            if ($to_be_cut lt $caladmin_index)
            {
                splice(@{$cut_cal->entries}, $caladmin_index, 1);
                splice(@{$cut_cal->entries}, $to_be_cut,      1);
            } else
            {
                splice(@{$cut_cal->entries}, $to_be_cut,      1);
                splice(@{$cut_cal->entries}, $caladmin_index, 1);
            }
            if (-f "$tmp_dir/splice.ics")
            {
                unlink "$tmp_dir/splice.ics";
            }
            auth($url);
            $d->get(-url => "$url/$euid.ics",
                    -to  => "$tmp_dir/splice.ics");
            my $scal    = Data::ICal->new(filename => "$tmp_dir/splice.ics");
            my @sevents = $scal->events();
            my $se      = $sevents[0];
            $cut_cal->add_entry($se);
            save_cal($cut_cal);
        }
    } elsif (defined $caladmin_index and !$cut_flag)
    {
        print "called procesing for caladmin index";
        splice(@{$cut_cal->entries}, $caladmin_index, 1);
        if (-f "$tmp_dir/splice.ics")
        {
            unlink "$tmp_dir/splice.ics";
        }
        auth($url);
        $d->get(-url => "$url/$euid.ics", -to => "$tmp_dir/splice.ics");
        my $scal    = Data::ICal->new(filename => "$tmp_dir/splice.ics");
        my @sevents = $scal->events();
        my $se      = $sevents[0];
        $cut_cal->add_entry($se);
        save_cal($cut_cal);
    } elsif ($cut_e->recurrence)
    {
        foreach my $i (@array_cut)
        {
            splice(@{$cut_cal->entries}, $i, 1);
        }
        save_cal($cut_cal);
    }

    my $mdir       = maildir_name($cut_e) . "--cancel";
    my $num_of_att = num_of_attendees($cut_e);

    if ($uid_flag)
    {
        maildir_flush("$alias/$mdir/", $num_of_att);
    } else
    {
        maildir_delete("$alias/$mdir/", $num_of_att);
    }

}

sub is_infinite
{
    my $ev = shift;
    if (my $ev_r = $ev->recurrence)
    {
        if (!defined $ev_r->count)
        {
            return 1;
        }
    }
}

sub reject_mail_infinite
{
    my $ev  = shift;
    my $org = organizer($ev);
    my %params;
    my %options;
    $params{resource}      = $resource_cn;
    $params{organizer}     = $org;
    $params{summary}       = $ev->summary;
    $options{INCLUDE_PATH} = '/home/caladmin/templates';
    my $msg = MIME::Lite::TT->new(
                                  From        => '[email protected]',
                                  To          => "$org\@foo.com",
                                  Subject     => " Resource booking rejected ",
                                  Template    => 'ev.infinite.txt.tt',
                                  TmplOptions => \%options,
                                  TmplParams  => \%params
                                 );
    $msg->send;
}

sub save_cal
{
    my $nc_cal = shift;
    open(my $fh, ">", $nc_path);
    print $fh $nc_cal->as_string;
    close($fh);
}

$nc_path = "$alias/nc_cal_$resource.ics";
$d       = HTTP::DAV->new();

if (!-d $tmp_dir)
{
    mkdir $tmp_dir;
}

if (!-d $queue_dir)
{
    mkdir $queue_dir;
}

$dirq = Directory::Queue->new(path => $queue_dir, schema => $schema);
my $inotify = Linux::Inotify2->new();
$inotify->watch("$queue_dir/temporary", IN_MOVED_FROM, \&queue_process);
1 while $inotify->poll;

sub queue_process
{

    for ($name = $dirq->first() ; $name ; $name = $dirq->next())
    {
        next unless $dirq->lock($name);
        %data = $dirq->get($name);

        $ne_path = "$data{body}";

        my $cancel_flag = 0;
        $cal = Data::ICal->new(filename => $ne_path);

        my @events = $cal->events();
        $e = $events[0];
        print " Processing " . $e->uid . "\n";
        if (cancel_val($cal) eq 'CANCEL')
        {
            print " Calendar method is CANCEL, event will be removed \n";
            cutjob($e);
            $dirq->remove($name);
	    print " Processing complete for " . $e->uid . "\n\n\n";
            next;
        }

        if (is_infinite($e))
        {
            print " Infinite Event, will reject \n";
            new_reject_event($e);
            reject_mail_infinite($e);
            $dirq->remove($name);
	    print " Processing complete for " . $e->uid . "\n\n\n";
            next;
        }

        if (!-f $nc_path)
        {
            copy($ne_path, $nc_path);
            print
              "First event in the queue, will be accepted and nc_cal.ics initialized \n";
            new_accept_event($e);
            $dirq->remove($name);
	    print " Processing complete for " . $e->uid . "\n\n\n";
            next;
        }

        $nc_cal     = Data::ICal->new(filename => $nc_path);
        $nc_events  = [];
        @$nc_events = $nc_cal->events();

        $oldmod    = undef;
        $conflicts = 0;

        print " Total non-conflicting events: " . (scalar @$nc_events) . "\n";

        foreach my $i (0 .. $#$nc_events)
        {
            my $nce = $nc_events->[$i];

            if (nc_cal_check($nce))
            {
                if ($nce->recurrence_id or $nce->recurrence)
                {
                    my $nce_uid = $nce->uid;
                    foreach my $j (0 .. $#$nc_events)
                    {
                        my $ncve = $nc_events->[$j];
                        if ($ncve->uid eq $nce_uid)
                        {
                            splice(@{$nc_cal->entries}, $j, 1);
                        }
                    }
                } else
                {
                    splice(@{$nc_cal->entries}, $i, 1);
                }
                next;
            }

            if ($nce->uid eq $e->uid)
            {

                if (    $e->recurrence_id
                    and $nce->recurrence_id
                    and $e->recurrence_id eq $nce->recurrence_id)
                {
                    print " Modified occurence, will remove if conflicts \n";
                    $oldmod = $i;
                } elsif (    $e->recurrence_id
                         and $nce->recurrence_id
                         and $e->recurrence_id ne $nce->recurrence_id)
                {
                    print " New occurence, both events have a recurrence-id \n";
                } elsif ($e->recurrence_id and !$nce->recurrence_id)
                {
                    print
                      " New occurence, incoming event has a recurrence-id \n";
                } elsif (!$e->recurrence_id and !$nce->recurrence_id)
                {
                    if ($e->recurrence)
                    {
                        print
                          " Modified recurring incoming event with recurrence, nce doesn't have rec-id \n";
                        $oldmod = $i;
                    } else
                    {
                        print " Modified non recurring incoming event \n";
                        $oldmod = $i;
                    }
                } elsif (!$e->recurrence_id and $nce->recurrence_id)
                {
                    print
                      " Modified recurring incoming event, nce has rec-id \n";
                }

            } elsif (!$conflicts
                     and events_conflict($e, $nce))
            {
                print " Event conflicts\n ";
                $conflicts = 1;
                $ce        = $nce;
            }
        }

        if (defined $oldmod)
        {
            print " Removing modified event from nc_cal \n";
            splice(@{$nc_cal->entries}, $oldmod, 1);
        }

        if ($conflicts)
        {
            print " Will reject \n";
            new_reject_event($e);
            reject_mail($e, $ce);
        } else
        {
            print " Will accept \n";
            new_accept_event($e);
            $nc_cal->add_entry($e);
        }

      SAVECAL:
        print " Will now save nc_cal \n";
        save_cal($nc_cal);

        $dirq->remove($name);

        print " Processing complete for " . $e->uid . "\n\n\n";
    }
}

Reply via email to