package GoogleCalendarApi; use strict; use warnings; use JSON; use JSON::WebToken; use LWP::UserAgent; use HTML::Entities; use URI::Escape; use Data::Dumper; use DateTime; use Time::HiRes qw(sleep); use Common ( 'info', 'error' ); sub new { my $class = shift; my $params = shift; #print Dumper($class); my $self = {}; for my $attr ( 'calendarId', 'debug' ) { $self->{$attr} = $params->{$attr} if defined $params->{$attr}; } $self->{debug} = 1; my $instance = bless $self, $class; if ( ( defined $params->{serviceAccount} ) && ( defined $params->{privateKey} ) ) { $instance->login( $params->{serviceAccount}, $params->{privateKey} ); } return $instance; } sub setCalendar { my $self = shift; my $calendarId = shift; $self->{calendarId} = $calendarId; } sub getBasicUrl { my $self = shift; return 'https://www.googleapis.com/calendar/v3/calendars/' . encode_entities( $self->{calendarId} ); } #https://developers.google.com/google-apps/calendar/v3/reference/events/list #returns { # 'timeZone' => 'Europe/Berlin', # 'description' => "Radioprogramm von Pi Radio f\x{fc}r 88vier.de", # 'defaultReminders' => [], # 'accessRole' => 'owner', # 'etag' => '"1415821582086000"', # 'kind' => 'calendar#events', # 'summary' => '88vier.de Pi Radio (Programm)', # 'updated' => '2014-11-12T19:46:22.086Z', # 'items' => [...] # } sub getEvents { my $self = shift; my $params = shift; my $url = '/events?'; for my $param ( 'iCalUID', 'alwaysIncludeEmail', 'maxAttendees', 'maxResults', 'orderBy', 'pageToken', 'privateExtendedProperty', 'q', 'sharedExtendedProperty', 'showDeleted', 'showHiddenInvitations', 'singleEvents', 'syncToken', 'timeZone' ) { $url .= '&' . $param . '=' . uri_escape( $params->{$param} ) if defined $params->{$param}; } for my $param ( 'timeMin', 'timeMax', 'updatedMin' ) { $url .= '&' . $param . '=' . uri_escape( $self->formatDateTime( $params->{$param} ) ) if defined $params->{$param}; } my $result = $self->httpRequest( 'GET', $url ); return $result; } # sleep 0.25 seconds to prevent hitting the 5.0 requests/second/user rate #sub sleep{ # my $this=shift; # my $duration=shift; # $duration=1 unless defined $duration; # select(undef, undef, undef, $duration); #} #https://developers.google.com/google-apps/calendar/v3/reference/events/delete sub deleteEvent { my $self = shift; my $eventId = shift; my $url = '/events/' . $eventId; #DELETE https://www.googleapis.com/calendar/v3/calendars/calendarId/events/eventId my $result = $self->httpRequest( 'DELETE', $url ); #$self->sleep(); return $result; } #https://developers.google.com/google-apps/calendar/v3/reference/events/insert sub insertEvent { my $self = shift; my $params = shift; my $event = { start => { dateTime => $self->formatDateTime( $params->{start} ) }, end => { dateTime => $self->formatDateTime( $params->{end} ) }, summary => $params->{summary} || '', description => $params->{description} || '', location => $params->{location} || '', status => $params->{confirmed} || 'confirmed' }; $event = encode_json $event; #POST https://www.googleapis.com/calendar/v3/calendars/calendarId/events my $url = '/events'; my $result = $self->httpRequest( 'POST', $url, $event ); #$self->sleep(); return $result; } # send a HTTP request sub httpRequest { my $self = shift; my $method = shift; my $url = shift; my $content = shift || ''; sleep 0.3; print STDERR "$method " . $url . "\n" if $self->{debug}; die("missing url") unless defined $url; die("calendarId not set") unless defined $self->{calendarId}; die("not logged in ") unless defined $self->{api}; #prepend basic url including calendar id $url = $self->getBasicUrl() . $url; print STDERR "$method " . $url . "\n" if $self->{debug}; my $response = undef; if ( $method eq 'GET' ) { $response = $self->{api}->get($url); } elsif ( ( $method eq 'POST' ) || ( $method eq 'PUT' ) ) { #return; print STDERR $content . "\n" if $self->{debug}; my $request = HTTP::Request->new( $method, $url ); $request->header( 'Content-Type' => 'application/json' ); $request->content($content); $response = $self->{api}->request($request); } elsif ( $method eq 'DELETE' ) { #return; $response = $self->{api}->delete($url); } if ( $response->is_success ) { my $content = $response->content; return {} if $content eq ''; return decode_json($content); } else { print "ERROR:\n"; print "Code: " . $response->code . "\n"; print "Message: " . $response->message . "\n"; print $response->content . "\n"; die; } } # write datetime object to string sub formatDateTime { my $self = shift; my $dt = shift; my $datetime = $dt->format_cldr("yyyy-MM-ddTHH:mm:ssZZZZZ"); print STDERR "$dt -> $datetime\n" if $self->{debug}; return $datetime; } # parse datetime from string to object sub getDateTime { my $self = shift; my $datetime = shift; my $timezone = shift; return if ( !defined $datetime ) or ( $datetime eq '' ); my @l = split /[\-\;T\s\:\+\.]/, $datetime; $datetime = DateTime->new( year => $l[0], month => $l[1], day => $l[2], hour => $l[3], minute => $l[4], second => $l[5], time_zone => $timezone ); return $datetime; } # login with serviceAccount and webToken (from privateKey) sub login { my $self = shift; my $serviceAccount = shift; my $privateKey = shift; # https://developers.google.com/accounts/docs/OAuth2ServiceAccount my $time = time; #create JSON Web Token my $jwt = JSON::WebToken->encode( { iss => $serviceAccount, scope => 'https://www.googleapis.com/auth/calendar', aud => 'https://accounts.google.com/o/oauth2/token', exp => $time + 3600, iat => $time, }, $privateKey, 'RS256', { typ => 'JWT' } ); #send JSON web token to authentication service $self->{auth} = LWP::UserAgent->new(); my $response = $self->{auth}->post( 'https://accounts.google.com/o/oauth2/token', { grant_type => encode_entities('urn:ietf:params:oauth:grant-type:jwt-bearer'), assertion => $jwt } ); die( $response->code, "\n", $response->content, "\n" ) unless $response->is_success(); my $data = decode_json( $response->content ); #create a new user agent and set token to bearer $self->{api} = LWP::UserAgent->new(); $self->{api}->default_header( Authorization => 'Bearer ' . $data->{access_token} ); print STDERR "login successful\n" if $self->{debug}; return $data; } 1;