263 lines
7.5 KiB
Perl
Executable File
263 lines
7.5 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
use utf8;
|
|
use OpenERP::XMLRPC::Client;
|
|
use Data::Dumper;
|
|
use Data::UUID;
|
|
use Encode qw(encode decode);
|
|
use HTML::Parse;
|
|
use HTML::FormatText;
|
|
use URI::Simple;
|
|
use Term::ReadKey;
|
|
use HTTP::DAV;
|
|
use Getopt::Long;
|
|
use YAML::XS qw(LoadFile);
|
|
use File::Basename;
|
|
use File::Path qw(rmtree);
|
|
use File::Temp;
|
|
use Hash::Merge::Simple qw(merge);
|
|
|
|
my $conf_file = './odoo2carddav.yml';
|
|
my $conf = {
|
|
odoo => {
|
|
url => undef,
|
|
user => undef,
|
|
password => undef,
|
|
database => undef,
|
|
filters => [
|
|
["is_company", "=", 0]
|
|
]
|
|
},
|
|
dav => {
|
|
url => undef,
|
|
user => undef,
|
|
password => undef,
|
|
delete => 0
|
|
},
|
|
path => {
|
|
workdir => mkdtemp('/tmp/odoo2carddav.XXXXXXXXX'),
|
|
keep_vcards => 0
|
|
}
|
|
};
|
|
|
|
my $env_conf = {
|
|
odoo => {
|
|
url => $ENV{ODOO_URL},
|
|
user => $ENV{ODOO_USER},
|
|
password => $ENV{ODOO_PASSWORD},
|
|
database => $ENV{ODOO_DATABASE}
|
|
},
|
|
dav => {
|
|
url => $ENV{DAV_URL},
|
|
user => $ENV{DAV_USER},
|
|
password => $ENV{DAV_PASSWORD},
|
|
delete => $ENV{DAV_DELETE}
|
|
},
|
|
path => {
|
|
workdir => $ENV{PATH_WORKDIR},
|
|
keep_vcards => $ENV{PATH_KEEP_VCARDS}
|
|
}
|
|
};
|
|
|
|
my $cli_conf = {};
|
|
GetOptions(
|
|
"config=s" => \$conf_file,
|
|
"odoo-url=s" => \$cli_conf->{odoo}->{url},
|
|
"odoo-database=s" => \$cli_conf->{odoo}->{database},
|
|
"odoo-password=s" => \$cli_conf->{odoo}->{password},
|
|
"path-workdir=s" => \$cli_conf->{path}->{workdir},
|
|
"path-keep-vcards" => \$cli_conf->{path}->{keep_vcards},
|
|
"dav-url=s" => \$cli_conf->{dav}->{url},
|
|
"dav-user=s" => \$cli_conf->{dav}->{user},
|
|
"dav-password=s" => \$cli_conf->{dav}->{password},
|
|
"dav-delete" => \$cli_conf->{dav}->{delete}
|
|
);
|
|
|
|
# Remove undefined values from cli and env config, so we can merge conf hashref easily
|
|
foreach my $hash ($cli_conf, $env_conf){
|
|
foreach my $k1 (keys %{$hash}){
|
|
foreach my $k2 (keys %{$hash->{$k1}}){
|
|
delete $hash->{$k1}->{$k2} unless defined $hash->{$k1}->{$k2};
|
|
}
|
|
delete $hash->{$k1} unless defined $hash->{$k1};
|
|
}
|
|
}
|
|
|
|
# Merge config from various sources. In order :
|
|
# - default conf inlined in the script
|
|
# - config file (if it exists)
|
|
# - cli arguments
|
|
if (-e $conf_file){
|
|
print "Reading $conf_file\n";
|
|
$conf = merge($conf, $env_conf, LoadFile($conf_file), $cli_conf);
|
|
} else {
|
|
print "Config file $conf_file not found\n";
|
|
$conf = merge($conf, $env_conf, $cli_conf);
|
|
}
|
|
|
|
# Prompt for password if needed
|
|
if (not defined $conf->{odoo}->{password}) {
|
|
$conf->{odoo}->{password} = prompt_pass("Enter Odoo password: ");
|
|
}
|
|
if (defined $conf->{dav}->{url} and not defined $conf->{dav}->{password}) {
|
|
$conf->{dav}->{password} = prompt_pass("Enter webdav password for $conf->{dav}->{url}: ");
|
|
}
|
|
|
|
# Sanitize some input
|
|
if (not defined $conf->{odoo}->{url}){
|
|
die "odoo.url is required\n";
|
|
}
|
|
|
|
if (not defined $conf->{odoo}->{user}){
|
|
die "odoo.user is required\n";
|
|
}
|
|
|
|
if (not defined $conf->{odoo}->{password}){
|
|
die "odoo.password is required\n";
|
|
}
|
|
|
|
if (defined $conf->{dav}->{url}){
|
|
if (not defined $conf->{dav}->{user}){
|
|
die "dav.user is needed as you have defined dav.url\n";
|
|
}
|
|
if (not defined $conf->{dav}->{password}){
|
|
die "dav.password is needed as you have defined dav.url\n";
|
|
}
|
|
}
|
|
|
|
my $odoo_uri = URI::Simple->new($conf->{odoo}->{url});
|
|
my $uuidgen = Data::UUID->new;
|
|
|
|
# Create an API client to query Odoo
|
|
my $odoo = OpenERP::XMLRPC::Client->new(
|
|
host => $odoo_uri->host,
|
|
port => $odoo_uri->port || '443',
|
|
proto => $odoo_uri->protocol,
|
|
dbname => $conf->{odoo}->{database},
|
|
username => $conf->{odoo}->{user},
|
|
password => $conf->{odoo}->{password}
|
|
);
|
|
|
|
# Odoo field -> vCard field mapping
|
|
my $odoo2vcf = {
|
|
name => 'N',
|
|
email => 'EMAIL;TYPE=PREF,INTERNET',
|
|
mobile => 'TEL;TYPE=cell,voice',
|
|
phone => 'TEL;TYPE=work,voice',
|
|
function => 'TITLE',
|
|
commercial_company_name => 'ORG',
|
|
website => 'URL;TYPE=work',
|
|
comment => 'NOTE'
|
|
};
|
|
|
|
# Prompt for a password in the terminal
|
|
sub prompt_pass {
|
|
my $desc = shift;
|
|
Term::ReadKey::ReadMode('noecho');
|
|
print STDERR "$desc";
|
|
my $password = Term::ReadKey::ReadLine(0);
|
|
# Rest the terminal to what it was previously doing
|
|
Term::ReadKey::ReadMode('restore');
|
|
print STDERR "\n";
|
|
$password =~ s/\R\z//;
|
|
return $password;
|
|
}
|
|
|
|
my %odoo_uuid = ();
|
|
my $contact_count = 0;
|
|
print "Querying Odoo API for contacts\n";
|
|
foreach my $contact_id (@{$odoo->search('res.partner', $conf->{odoo}->{filters})}){
|
|
my $contact = $odoo->read('res.partner', [ $contact_id ])->[0];
|
|
|
|
my $vcard =<<_EOV;
|
|
BEGIN:VCARD
|
|
VERSION:3.0
|
|
_EOV
|
|
|
|
|
|
foreach (keys %{$odoo2vcf}){
|
|
next unless defined $contact->{$_};
|
|
if ($_ eq 'comment'){
|
|
# Comment is HTML formated, so extract the text only
|
|
# And replace newline with literal \n
|
|
$contact->{$_} = HTML::FormatText->new->format(HTML::Parse::parse_html($contact->{$_}));
|
|
$contact->{$_} =~ s/\r?\n/\\n/g;
|
|
}
|
|
# Check if the field contains non ASCII characters. If it does, it's UTF-8, else, it's Latin1
|
|
if ($contact->{$_} =~ /([^\x{00}-\x{ff}])/){
|
|
$vcard .= "$odoo2vcf->{$_}:" . encode("UTF-8", $contact->{$_}) . "\n";
|
|
} else {
|
|
$vcard .= "$odoo2vcf->{$_}:" . encode("UTF-8", decode("Latin1", $contact->{$_})) . "\n";
|
|
}
|
|
}
|
|
|
|
my $uuid = lc $uuidgen->to_string(
|
|
$uuidgen->create_from_name(NameSpace_URL, "$conf->{odoo}->{url}/contacts/$contact->{id}")
|
|
);
|
|
$vcard .= "UID:$uuid\n";
|
|
$vcard .= "END:VCARD\n";
|
|
# Track UUID present in Odoo, so we can delete contacts from carddav server later
|
|
$odoo_uuid{$uuid} = 1;
|
|
open VCARD, ">$conf->{path}->{workdir}/$uuid.vcf";
|
|
print VCARD $vcard;
|
|
close VCARD;
|
|
$contact_count++;
|
|
}
|
|
|
|
my $dav_success = 0;
|
|
my $dav_error = 0;
|
|
if (defined $conf->{dav}->{url}){
|
|
my $d = HTTP::DAV->new();
|
|
$d->credentials(
|
|
-user => $conf->{dav}->{user},
|
|
-pass => $conf->{dav}->{password},
|
|
-url => $conf->{dav}->{url}
|
|
);
|
|
|
|
my $dav_uri = URI::Simple->new($conf->{dav}->{url});
|
|
print("Opening webdav collection $conf->{dav}->{url}\n");
|
|
$d->open( -url => $conf->{dav}->{url} )
|
|
or die("Couldn't open $conf->{dav}->{url}: " . $d->message . "\n");
|
|
|
|
my $dav_cards = $d->propfind( -url => $dav_uri->path, -depth => 1);
|
|
|
|
if ($conf->{dav}->{delete}) {
|
|
print "Checking vcards to delete on webdav server\n";
|
|
# Iterate over resources in the dav server, and remove it if it doesn't correspond to an odoo contact
|
|
foreach my $card (@{$dav_cards->get_resourcelist->{_resources}}){
|
|
my $card_uri = $card->get_uri->as_string;
|
|
if ($card->get_property('getcontenttype') ne "text/vcard"){
|
|
print "Skipping $card_uri as it's not a .vcf\n";
|
|
next;
|
|
}
|
|
if (not defined $odoo_uuid{basename($card_uri, ".vcf")}){
|
|
print "Card $card_uri exists in dav server, but not in Odoo, removing from dav\n";
|
|
$card->delete;
|
|
}
|
|
}
|
|
}
|
|
|
|
print "Uploading vcards to webdav server $conf->{dav}->{url}\n";
|
|
foreach my $vcf (glob("$conf->{path}->{workdir}/*.vcf")){
|
|
if ($d->put( -local => "$vcf" ) == 1){
|
|
$dav_success++;
|
|
} else {
|
|
print "An error occured while uploading $vcf : " . $d->message . "\n";
|
|
$dav_error++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (not $conf->{path}->{keep_vcards}){
|
|
print "Deleting vcards from $conf->{path}->{workdir}\n";
|
|
rmtree($conf->{path}->{workdir});
|
|
}
|
|
|
|
print "$contact_count contacts found in Odoo\n";
|
|
if (defined $conf->{dav}->{url}){
|
|
print "$dav_success contacts successfuly uploaded to $conf->{dav}->{url}\n";
|
|
print "$dav_error contacts failed while uploading to $conf->{dav}->{url}\n";
|
|
}
|