PageRenderTime 60ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/atlassian-avatar-sync.pl

https://bitbucket.org/taqueci/atlassian-avatar-sync
Perl | 770 lines | 523 code | 240 blank | 7 comment | 28 complexity | 6545627f978f3a84d8c44c1761de0657 MD5 | raw file
Possible License(s): GPL-2.0
  1. =head1 NAME
  2. atlassian-avatar-sync.pl - copies avatar pictures from JIRA to Confluence or
  3. Bitbucket
  4. =head1 SYSNOPSIS
  5. perl atlassian-avatar-sync.pl [OPTION] ... URL_CONFLUENCE URL_JIRA [TARGET_USER] ...
  6. perl atlassian-avatar-sync.pl --bitbucket [OPTION] ... URL_BITBUCKET URL_JIRA [TARGET_USER] ...
  7. =head1 DESCRIPTION
  8. This script copies avatar pictures of user TARGET_USER from URL_JRIA to
  9. URL_CONFLUENCE or URL_BITBUCKET.
  10. If TARGET_USER is not specified, all avatar pictures are copied.
  11. =head1 OPTIONS
  12. =over 4
  13. =item -u USER, --user=UESR
  14. Use USER for authentication of Confluence or Bitbucket.
  15. =item -p PASSWORD, --passowrd=PASSWORD
  16. Use PASSWORD for authentication of Confluence or Bitbucket.
  17. =item -U USER, --jira-user=UESR
  18. Use USER for authentication of JIRA.
  19. =item -P PASSWORD, --jira-passowrd=PASSWORD
  20. Use PASSWORD for authentication of JIRA.
  21. =item -f, --force
  22. Overwrite avatar.
  23. =item -l FILE, --log=FILE
  24. Write log to FILE.
  25. =item --verbose
  26. Print verbosely.
  27. =item --help
  28. Print this help.
  29. =back
  30. =head1 AUTHOR
  31. Takeshi Nakamura <taqueci.n@gmail.com>
  32. =head1 COPYRIGHT
  33. Copyright (C) 2018 Takeshi Nakamura. All Rights Reserved.
  34. =cut
  35. use strict;
  36. use warnings;
  37. use utf8;
  38. use Digest::MD5 qw(md5_hex);
  39. use Getopt::Long qw(:config no_ignore_case no_auto_abbrev gnu_compat);
  40. use HTTP::Request;
  41. use LWP::UserAgent;
  42. use Pod::Usage;
  43. use Term::ReadKey;
  44. PLib::import();
  45. _main(@ARGV) or exit 1;
  46. exit 0;
  47. sub _main {
  48. local @ARGV = @_;
  49. my %opt;
  50. GetOptions(\%opt, '--bitbucket|b', 'force|f', 'user|u=s', 'password|p=s',
  51. 'jira-user|U=s', 'jira-password|P=s',
  52. 'log|l=s', 'verbose', 'help') or return 0;
  53. p_set_log($opt{log}) if defined $opt{log};
  54. p_set_verbose(1) if $opt{verbose};
  55. pod2usage(-exitval => 0, -verbose => 2, -noperldoc => 1) if $opt{help};
  56. unless (@ARGV > 1) {
  57. p_error("Too few arguments");
  58. return 0;
  59. }
  60. my ($url_to, $url_from, @target) = @ARGV;
  61. my $to = $opt{bitbucket} ? 'Bitbucket' : 'Confluence';
  62. print "Authentication for $to $url_to\n" unless $opt{user} &&
  63. $opt{password};
  64. my $user = $opt{user} // _read_key("User: ");
  65. my $passwd = $opt{password} // _read_key("Password: ", 1);
  66. print "Authentication for JIRA $url_from\n" unless $opt{'jira-user'} &&
  67. $opt{'jira-password'};
  68. my $user_from = $opt{'jira-user'} // _read_key("User: ");
  69. my $passwd_from = $opt{'jira-password'} // _read_key("Password: ", 1);
  70. my $appl_from = Jira->new($url_from, $user_from, $passwd_from);
  71. my $appl_to = $opt{bitbucket} ?
  72. Bitbucket->new($url_to, $user, $passwd) :
  73. Confluence->new($url_to, $user, $passwd);
  74. unless (@target > 0) {
  75. p_verbose("Reading user information from $url_to");
  76. my $users = $appl_to->all_users or return 0;
  77. @target = @$users;
  78. }
  79. p_verbose("Synchronizing avatars");
  80. _sync_avatars(\@target, $appl_to, $appl_from, $opt{force}) or return 0;
  81. p_verbose("Completed!\n");
  82. return 1;
  83. }
  84. sub _read_key {
  85. my ($msg, $noecho) = @_;
  86. print $msg;
  87. ReadMode 'noecho' if $noecho;
  88. my $val = ReadLine 0;
  89. ReadMode 'restore' if $noecho;
  90. print "\n" if $noecho;
  91. chomp $val;
  92. return $val;
  93. }
  94. sub _sync_avatars {
  95. my ($target, $appl_to, $appl_from, $force) = @_;
  96. my $name_to = ref $appl_to;
  97. my $nerr = 0;
  98. foreach my $x (@$target) {
  99. p_verbose("Synchronizing avatar picture of user '$x'");
  100. p_verbose("Getting avatar from JIRA");
  101. my $avtr_from = $appl_from->avatar($x);
  102. unless ($avtr_from) {
  103. $nerr++;
  104. next;
  105. }
  106. if ($avtr_from->is_default) {
  107. p_warning("Avatar for user '$x' is not updated because no new one has been uploaded");
  108. next;
  109. }
  110. my $t = $avtr_from->type;
  111. unless (($t eq 'image/png') || ($t eq 'image/jpeg') ||
  112. ($t eq 'image/gif')) {
  113. p_warning("Avatar for user '$x' is not updated because image type '$t' is not supported");
  114. next;
  115. }
  116. p_verbose("Getting existing avatar from $name_to");
  117. my $avtr_to = $appl_to->avatar($x);
  118. unless ($avtr_to) {
  119. $nerr++;
  120. next;
  121. }
  122. if ($avtr_from->is_equal($avtr_to) ||
  123. (!$force && !$avtr_to->is_default)) {
  124. p_warning("Avatar for user '$x' has already been updated");
  125. next;
  126. }
  127. p_verbose("Setting avatar of $name_to");
  128. $avtr_to->set_data($avtr_from->type, $avtr_from->data,
  129. $avtr_from->path);
  130. $avtr_to->push or $nerr++;
  131. }
  132. return $nerr == 0;
  133. }
  134. # JIRA
  135. package Jira;
  136. INIT { PLib::import() }
  137. sub new {
  138. my ($class, $url, $user, $passwd) = @_;
  139. my $self = {url => $url, user => $user, password => $passwd};
  140. return bless $self, $class;
  141. }
  142. sub avatar {
  143. my ($self, $id) = @_;
  144. my $avatar = Avatar::Jira->new($self->{url}, $self->{user},
  145. $self->{password}, $id);
  146. return $avatar->pull ? $avatar : undef;
  147. }
  148. # Confluence
  149. package Confluence;
  150. use JSON;
  151. INIT { PLib::import() }
  152. sub new {
  153. my ($class, $url, $user, $passwd) = @_;
  154. my $self = {url => $url, user => $user, password => $passwd};
  155. return bless $self, $class;
  156. }
  157. sub all_users {
  158. my $self = shift;
  159. my $url = $self->{url};
  160. my $u = "$url/rpc/json-rpc/confluenceservice-v2/getActiveUsers";
  161. my $req = HTTP::Request->new(POST => $u);
  162. $req->authorization_basic($self->{user}, $self->{password});
  163. $req->content_type('application/json');
  164. $req->content(encode_json([JSON::true]));
  165. my $ua = LWP::UserAgent->new;
  166. my $r = $ua->request($req);
  167. unless ($r->is_success) {
  168. p_error("Failed to get all users from $url");
  169. p_log($r->status_line);
  170. return undef;
  171. }
  172. my $c = decode_json($r->content);
  173. unless (ref($c) eq 'ARRAY') {
  174. p_error("Failed to get all users from $url");
  175. p_log($c->{error}->{message});
  176. return undef;
  177. }
  178. return $c;
  179. }
  180. sub avatar {
  181. my ($self, $id) = @_;
  182. my $avatar = Avatar::Confluence->new($self->{url}, $self->{user},
  183. $self->{password}, $id);
  184. return $avatar->pull ? $avatar : undef;
  185. }
  186. # Bitbucket
  187. package Bitbucket;
  188. use JSON;
  189. INIT { PLib::import() }
  190. sub new {
  191. my ($class, $url, $user, $passwd) = @_;
  192. my $self = {url => $url, user => $user, password => $passwd};
  193. return bless $self, $class;
  194. }
  195. sub all_users {
  196. my $self = shift;
  197. my $url = $self->{url};
  198. my $start = 0;
  199. my @users;
  200. my $ua = LWP::UserAgent->new;
  201. while (1) {
  202. my $u = "$url/rest/api/1.0/users?start=$start";
  203. my $req = HTTP::Request->new(GET => $u);
  204. $req->authorization_basic($self->{user}, $self->{password});
  205. $req->content_type('application/json');
  206. my $r = $ua->request($req);
  207. unless ($r->is_success) {
  208. p_error("Failed to get all users from $url");
  209. p_log($r->status_line);
  210. return undef;
  211. }
  212. my $c = decode_json($r->content);
  213. push @users, map {$_->{name}} @{$c->{values}};
  214. last if $c->{isLastPage};
  215. $start = $c->{nextPageStart};
  216. }
  217. return \@users;
  218. }
  219. sub avatar {
  220. my ($self, $id) = @_;
  221. my $avatar = Avatar::Bitbucket->new($self->{url}, $self->{user},
  222. $self->{password}, $id);
  223. return $avatar->pull ? $avatar : undef;
  224. }
  225. # JIRA avatar
  226. package Avatar::Jira;
  227. use Digest::MD5 qw(md5_hex);
  228. use File::Basename;
  229. use JSON;
  230. INIT { PLib::import() }
  231. sub new {
  232. my ($class, $url, $user, $passwd, $id) = @_;
  233. my $self = {url => $url, user => $user, password => $passwd, id => $id};
  234. return bless $self, $class;
  235. }
  236. sub pull {
  237. my $self = shift;
  238. my $path = $self->_path($self->{id}) or return 0;
  239. return $self->_get($path);
  240. }
  241. sub _path {
  242. my ($self, $id) = @_;
  243. my $url = $self->{url};
  244. my $u = "$url/rest/api/2/user?username=$id";
  245. my $req = HTTP::Request->new(GET => $u);
  246. $req->authorization_basic($self->{user}, $self->{password});
  247. my $ua = LWP::UserAgent->new;
  248. my $r = $ua->request($req);
  249. unless ($r->is_success) {
  250. p_error("Failed to get avatar URL for user '$id'");
  251. p_log($r->status_line);
  252. return undef;
  253. }
  254. return decode_json($r->content)->{avatarUrls}->{'48x48'};
  255. }
  256. sub _get {
  257. my ($self, $path) = @_;
  258. my $req = HTTP::Request->new(GET => $path);
  259. $req->authorization_basic($self->{user}, $self->{password});
  260. my $ua = LWP::UserAgent->new;
  261. my $r = $ua->request($req);
  262. unless ($r->is_success) {
  263. p_error("Failed to download avatar picture from $path");
  264. p_log($r->status_line);
  265. return 0;
  266. }
  267. my $t = $r->content_type;
  268. my $c = $r->content;
  269. $self->{type} = $t;
  270. $self->{data} = $c;
  271. $self->{path} = $path;
  272. return 1;
  273. }
  274. sub type {
  275. return shift->{type};
  276. }
  277. sub data {
  278. return shift->{data};
  279. }
  280. sub path {
  281. return shift->{path};
  282. }
  283. sub is_default {
  284. my $DEFAULT_AVATAR_ID = 10122;
  285. return shift->{path} =~ /avatarId=$DEFAULT_AVATAR_ID/;
  286. }
  287. sub is_equal {
  288. my ($self, $avatar) = @_;
  289. my $t = ref $avatar;
  290. if ($t eq 'Avatar::Confluence') {
  291. return md5_hex($self->{path}) eq basename($avatar->path);
  292. } elsif ($t eq 'Avatar::Bitbucket') {
  293. return md5_hex($self->{data}) eq md5_hex($avatar->data);
  294. } else {
  295. return 0;
  296. }
  297. }
  298. # Confluence avatar
  299. package Avatar::Confluence;
  300. use Digest::MD5 qw(md5_hex);
  301. use File::Basename;
  302. use JSON;
  303. INIT { PLib::import() }
  304. sub new {
  305. my ($class, $url, $user, $passwd, $id) = @_;
  306. my $self = {url => $url, user => $user, password => $passwd, id => $id};
  307. return bless $self, $class;
  308. }
  309. sub pull {
  310. my $self = shift;
  311. my $id = $self->{id};
  312. my $url = $self->{url};
  313. my $u = "$url/rest/api/user?username=$id";
  314. my $req = HTTP::Request->new(GET => $u);
  315. $req->authorization_basic($self->{user}, $self->{password});
  316. my $ua = LWP::UserAgent->new;
  317. my $r = $ua->request($req);
  318. unless ($r->is_success) {
  319. p_error("Failed to get information of user '$id'");
  320. p_log($r->status_line);
  321. return 0;
  322. }
  323. my $t = $r->content_type;
  324. my $c = $r->content;
  325. $self->{type} = $t;
  326. $self->{data} = $c;
  327. $self->{path} = decode_json($r->content)->{profilePicture}->{path};
  328. return 1;
  329. }
  330. sub push {
  331. my $self = shift;
  332. my $url = $self->{url};
  333. my $id = $self->{id};
  334. my $u = "$url/rpc/json-rpc/confluenceservice-v2/addProfilePicture";
  335. my $req = HTTP::Request->new(POST => $u);
  336. my $name = defined($self->{path}) ?
  337. basename($self->{path}) : md5_hex($self->{origin});
  338. my @d = unpack 'C*', $self->{data};
  339. $req->authorization_basic($self->{user}, $self->{password});
  340. $req->content_type('application/json');
  341. $req->content(encode_json([$id, $name, $self->{type}, \@d]));
  342. my $ua = LWP::UserAgent->new;
  343. my $r = $ua->request($req);
  344. unless ($r->is_success) {
  345. p_error("Failed to update avatar of user '$id'");
  346. p_log($r->status_line);
  347. return 0;
  348. }
  349. return 1;
  350. }
  351. sub data {
  352. return shift->{data};
  353. }
  354. sub set_data {
  355. my ($self, $type, $data, $origin) = @_;
  356. $self->{type} = $type;
  357. $self->{data} = $data;
  358. $self->{path} = undef;
  359. $self->{origin} = $origin;
  360. }
  361. sub path {
  362. return shift->{path};
  363. }
  364. sub is_default {
  365. my $path = shift->{path};
  366. my $DEFAULT_AVATAR_FILE = 'default.png';
  367. return defined($path) ? (basename($path) eq $DEFAULT_AVATAR_FILE) : 0;
  368. }
  369. # Bitbucket avatar
  370. package Avatar::Bitbucket;
  371. use Digest::MD5 qw(md5_hex);
  372. use HTTP::Request::Common;
  373. use JSON;
  374. INIT { PLib::import() }
  375. sub new {
  376. my ($class, $url, $user, $passwd, $id) = @_;
  377. my $self = {url => $url, user => $user, password => $passwd, id => $id};
  378. return bless $self, $class;
  379. }
  380. sub pull {
  381. my $self = shift;
  382. my $id = $self->{id};
  383. my $url = $self->{url};
  384. my $u = "$url/users/$id/avatar.png";
  385. my $req = HTTP::Request->new(GET => $u);
  386. $req->authorization_basic($self->{user}, $self->{password});
  387. my $ua = LWP::UserAgent->new;
  388. my $r = $ua->request($req);
  389. unless ($r->is_success) {
  390. p_error("Failed to download avatar picture from $url");
  391. p_log($r->status_line);
  392. return 0;
  393. }
  394. my $t = $r->content_type;
  395. my $c = $r->content;
  396. $self->{type} = $t;
  397. $self->{data} = $c;
  398. return 1;
  399. }
  400. sub push {
  401. my $self = shift;
  402. my $url = $self->{url};
  403. my $id = $self->{id};
  404. my $u = "$url/rest/api/1.0/users/$id/avatar.png";
  405. my $req = POST $u, Content_Type => 'multipart/form-data',
  406. Content => [avatar => $self->{data}];
  407. $req->authorization_basic($self->{user}, $self->{password});
  408. $req->header('X-Atlassian-Token' => 'no-check');
  409. my $ua = LWP::UserAgent->new;
  410. my $r = $ua->request($req);
  411. unless ($r->is_success) {
  412. p_error("Failed to update avatar of user '$id'");
  413. p_log($r->status_line);
  414. return 0;
  415. }
  416. return 1;
  417. }
  418. sub data {
  419. return shift->{data};
  420. }
  421. sub set_data {
  422. my ($self, $type, $data, $origin) = @_;
  423. $self->{type} = $type;
  424. $self->{data} = $data;
  425. $self->{path} = undef;
  426. $self->{origin} = $origin;
  427. }
  428. sub path {
  429. return undef;
  430. }
  431. sub is_default {
  432. my $DEFAULT_AVATAR_MD5 = 'b1c94647deb67e378c7d72e6a467c2b5';
  433. return md5_hex(shift->{data}) eq $DEFAULT_AVATAR_MD5;
  434. }
  435. # Library
  436. package PLib;
  437. use Carp;
  438. use Encode;
  439. my $p_message_prefix;
  440. my $p_log_file;
  441. my $p_is_verbose;
  442. my $p_encoding;
  443. INIT {
  444. $p_message_prefix = "";
  445. $p_is_verbose = 0;
  446. $p_encoding = 'utf-8';
  447. }
  448. sub import {
  449. my @EXPORT = qw(p_message p_warning p_error p_verbose p_log
  450. p_set_encoding p_set_message_prefix p_set_log
  451. p_set_verbose p_exit p_error_exit p_slurp);
  452. my $caller = caller;
  453. no strict 'refs';
  454. foreach my $func (@EXPORT) {
  455. *{"${caller}::$func"} = \&{"PLib::$func"};
  456. }
  457. }
  458. sub p_decode {
  459. return decode($p_encoding, shift);
  460. }
  461. sub p_encode {
  462. return encode($p_encoding, shift);
  463. }
  464. sub p_message {
  465. my @msg = ($p_message_prefix, @_);
  466. print STDERR map {p_encode($_)} @msg, "\n";
  467. p_log(@msg);
  468. }
  469. sub p_warning {
  470. my @msg = ("*** WARNING ***: ", $p_message_prefix, @_);
  471. print STDERR map {p_encode($_)} @msg, "\n";
  472. p_log(@msg);
  473. }
  474. sub p_error {
  475. my @msg = ("*** ERROR ***: ", $p_message_prefix, @_);
  476. print STDERR map {p_encode($_)} @msg, "\n";
  477. p_log(@msg);
  478. }
  479. sub p_verbose {
  480. my @msg = @_;
  481. print STDERR map {p_encode($_)} @msg, "\n" if $p_is_verbose;
  482. p_log(@msg);
  483. }
  484. sub p_log {
  485. my @msg = @_;
  486. return unless defined $p_log_file;
  487. open my $fh, '>>', $p_log_file or die "$p_log_file: $!\n";
  488. print $fh map {p_encode($_)} @msg, "\n";
  489. close $fh;
  490. }
  491. sub p_set_encoding {
  492. $p_encoding = shift;
  493. }
  494. sub p_set_message_prefix {
  495. my $prefix = shift;
  496. defined $prefix or croak 'Invalid argument';
  497. $p_message_prefix = $prefix;
  498. }
  499. sub p_set_log {
  500. my $file = shift;
  501. defined $file or croak 'Invalid argument';
  502. $p_log_file = $file;
  503. }
  504. sub p_set_verbose {
  505. $p_is_verbose = (!defined($_[0]) || ($_[0] != 0));
  506. }
  507. sub p_exit {
  508. my ($val, @msg) = @_;
  509. print STDERR map {p_encode($_)} @msg, "\n";
  510. p_log(@msg);
  511. exit $val;
  512. }
  513. sub p_error_exit {
  514. my ($val, @msg) = @_;
  515. p_error(@msg);
  516. exit $val;
  517. }
  518. sub p_slurp {
  519. my ($file, $encoding) = @_;
  520. my $fh;
  521. $encoding //= $p_encoding;
  522. unless (open $fh, $file) {
  523. p_error("$file: $!");
  524. return undef;
  525. }
  526. local $/ = undef;
  527. my $content = <$fh>;
  528. close $fh;
  529. return decode $encoding, $content;
  530. }