In the past Google Calendar exports were done by first removing all events of a day and then create new ones. This takes a lot of time for export and runs into Google Calendar usage limits after some time. By now content will be compared before removing/creating a single event one. To be able to do so, all other sync sources and targets have been removed, so its only possible to export from database to Google Calendar by this change. To trigger an export you need to create a trigger file. run_jobs.pl runs periodically e.g. started by cron and checks if a trigger file exists and start sync_cms.pl to export the selected events to the Google Calendar. Trigger files and jobs are configured at jobs.config. Each job has a source and target file containing the access data for calcms and the calendar. Configuration files have been cleaned up. Old Accounts and passwords have been removed. They hopefully should have been not active for a long time ;-)
251 lines
7.2 KiB
Perl
251 lines
7.2 KiB
Perl
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;
|