| blib/lib/WWW/Gittip.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 90 | 105 | 85.7 |
| branch | 16 | 22 | 72.7 |
| condition | 5 | 9 | 55.5 |
| subroutine | 16 | 17 | 94.1 |
| pod | 10 | 10 | 100.0 |
| total | 137 | 163 | 84.0 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package WWW::Gittip; | ||||||
| 2 | 1 | 1 | 49013 | use strict; | |||
| 1 | 2 | ||||||
| 1 | 43 | ||||||
| 3 | 1 | 1 | 7 | use warnings; | |||
| 1 | 2 | ||||||
| 1 | 33 | ||||||
| 4 | |||||||
| 5 | 1 | 1 | 1103 | use LWP::UserAgent; | |||
| 1 | 89944 | ||||||
| 1 | 49 | ||||||
| 6 | 1 | 1 | 1457 | use JSON qw(from_json); | |||
| 1 | 36667 | ||||||
| 1 | 8 | ||||||
| 7 | 1 | 1 | 1837 | use HTML::TreeBuilder 5 -weak; | |||
| 1 | 40315 | ||||||
| 1 | 17 | ||||||
| 8 | |||||||
| 9 | our $VERSION = '0.07'; | ||||||
| 10 | my $BASE_URL = 'https://www.gratipay.com'; | ||||||
| 11 | |||||||
| 12 | =head1 NAME | ||||||
| 13 | |||||||
| 14 | WWW::Gittip - Implementing the Gittip (now Gratipay) API. More or less. | ||||||
| 15 | |||||||
| 16 | =head1 SYNOPSIS | ||||||
| 17 | |||||||
| 18 | use WWW::Gittip; | ||||||
| 19 | my $gt = WWW::Gittip->new; | ||||||
| 20 | my $charts = $gt->charts; | ||||||
| 21 | |||||||
| 22 | my $user_charts = $gt->user_charts('szabgab'); | ||||||
| 23 | |||||||
| 24 | =head1 DESCRIPTION | ||||||
| 25 | |||||||
| 26 | This module provides a Perl interface to the L |
||||||
| 27 | Gittip describes itself as "a way to give small weekly cash gifts to people you | ||||||
| 28 | love and are inspired by". It is one way you can give small recurring amounts to | ||||||
| 29 | people who've written open source software that you regularly use. | ||||||
| 30 | |||||||
| 31 | The API docs of Gittp: L |
||||||
| 32 | |||||||
| 33 | When necessary, you can get an API key from your account on Gittip at L |
||||||
| 34 | |||||||
| 35 | =cut | ||||||
| 36 | |||||||
| 37 | |||||||
| 38 | =head2 new | ||||||
| 39 | |||||||
| 40 | my $gt = WWW::Gittip->new; | ||||||
| 41 | my $gt = WWW::Gittip->new( api_key => '123-456' ); | ||||||
| 42 | |||||||
| 43 | |||||||
| 44 | =cut | ||||||
| 45 | |||||||
| 46 | sub new { | ||||||
| 47 | 1 | 1 | 1 | 9792 | my ($class, %params) = @_; | ||
| 48 | 1 | 7 | bless \%params, $class; | ||||
| 49 | } | ||||||
| 50 | |||||||
| 51 | =head2 api_key | ||||||
| 52 | |||||||
| 53 | Set/Get the API_KEY | ||||||
| 54 | |||||||
| 55 | $gt->api_key('123-456'); | ||||||
| 56 | |||||||
| 57 | my $api_key = $gt->api_key; | ||||||
| 58 | |||||||
| 59 | =cut | ||||||
| 60 | |||||||
| 61 | |||||||
| 62 | sub api_key { | ||||||
| 63 | 13 | 13 | 1 | 27 | my ($self, $value) = @_; | ||
| 64 | 13 | 50 | 58 | if (defined $value) { | |||
| 65 | 0 | 0 | $self->{api_key} = $value; | ||||
| 66 | } | ||||||
| 67 | 13 | 43 | return $self->{api_key}; | ||||
| 68 | } | ||||||
| 69 | |||||||
| 70 | |||||||
| 71 | =head2 charts | ||||||
| 72 | |||||||
| 73 | Returns an array reference from /about/charts.json | ||||||
| 74 | Each element in the array has the following fields: | ||||||
| 75 | |||||||
| 76 | { | ||||||
| 77 | "active_users" => 50, | ||||||
| 78 | "charges" => 25.29, | ||||||
| 79 | "date" => "2012-06-22", | ||||||
| 80 | "total_gifts" => 62.08, | ||||||
| 81 | "total_users" => 621, | ||||||
| 82 | "weekly_gifts" => 30.08, | ||||||
| 83 | "withdrawals" => 0.00 | ||||||
| 84 | }, | ||||||
| 85 | |||||||
| 86 | =cut | ||||||
| 87 | |||||||
| 88 | |||||||
| 89 | sub charts { | ||||||
| 90 | 1 | 1 | 1 | 2216 | my ($self) = @_; | ||
| 91 | |||||||
| 92 | 1 | 5 | my $url = "$BASE_URL/about/charts.json"; | ||||
| 93 | 1 | 6 | return $self->_get($url); | ||||
| 94 | } | ||||||
| 95 | |||||||
| 96 | =head2 user_charts | ||||||
| 97 | |||||||
| 98 | $gt->user_charts(USERNAME); | ||||||
| 99 | |||||||
| 100 | Returns an array referene from /%username/charts.json | ||||||
| 101 | Each element in the array has the following fields: | ||||||
| 102 | |||||||
| 103 | { | ||||||
| 104 | 'date' => '2012-06-08', | ||||||
| 105 | 'npatrons' => 0, | ||||||
| 106 | 'receipts' => '0', | ||||||
| 107 | 'ts_start' => '2012-06-08T12:02:45.182409+00:00' | ||||||
| 108 | } | ||||||
| 109 | |||||||
| 110 | |||||||
| 111 | =cut | ||||||
| 112 | |||||||
| 113 | |||||||
| 114 | sub user_charts { | ||||||
| 115 | 2 | 2 | 1 | 372013 | my ($self, $username) = @_; | ||
| 116 | |||||||
| 117 | #croak "Invalid username '$username'" if $username eq 'about'; | ||||||
| 118 | |||||||
| 119 | 2 | 9 | my $url = "$BASE_URL/$username/charts.json"; | ||||
| 120 | 2 | 11 | return $self->_get($url); | ||||
| 121 | } | ||||||
| 122 | |||||||
| 123 | |||||||
| 124 | =head2 paydays | ||||||
| 125 | |||||||
| 126 | Returns an array reference from /about/paydays.json | ||||||
| 127 | Each element in the array has the following fields: | ||||||
| 128 | |||||||
| 129 | { | ||||||
| 130 | 'ach_fees_volume' => '0', | ||||||
| 131 | 'ach_volume' => '0', | ||||||
| 132 | 'charge_fees_volume' => '2.11', | ||||||
| 133 | 'charge_volume' => '25.28', | ||||||
| 134 | 'nachs' => 0, | ||||||
| 135 | 'nactive' => 25 | ||||||
| 136 | 'ncc_failing' => 1, | ||||||
| 137 | 'ncc_missing' => 18, | ||||||
| 138 | 'ncharges' => 11, | ||||||
| 139 | 'nparticipants' => 175, | ||||||
| 140 | 'ntransfers' => 49, | ||||||
| 141 | 'ntippers' => 12, | ||||||
| 142 | 'transfer_volume' => '24.8', | ||||||
| 143 | 'ts_end' => '2012-06-08T12:03:19.889215+00:00', | ||||||
| 144 | 'ts_start' => '2012-06-08T12:02:45.182409+00:00', | ||||||
| 145 | }, | ||||||
| 146 | |||||||
| 147 | =cut | ||||||
| 148 | |||||||
| 149 | sub paydays { | ||||||
| 150 | 1 | 1 | 1 | 24742 | my ($self) = @_; | ||
| 151 | |||||||
| 152 | 1 | 7 | my $url = "$BASE_URL/about/paydays.json"; | ||||
| 153 | 1 | 6 | return $self->_get($url); | ||||
| 154 | } | ||||||
| 155 | |||||||
| 156 | =head2 stats | ||||||
| 157 | |||||||
| 158 | Returns a reference to a hash from /about/stats.json | ||||||
| 159 | with lots of keys... | ||||||
| 160 | |||||||
| 161 | =cut | ||||||
| 162 | |||||||
| 163 | |||||||
| 164 | |||||||
| 165 | sub stats { | ||||||
| 166 | 1 | 1 | 1 | 302802 | my ($self) = @_; | ||
| 167 | |||||||
| 168 | 1 | 6 | my $url = "$BASE_URL/about/stats.json"; | ||||
| 169 | 1 | 8 | return $self->_get($url); | ||||
| 170 | } | ||||||
| 171 | |||||||
| 172 | =head2 communities | ||||||
| 173 | |||||||
| 174 | See L |
||||||
| 175 | |||||||
| 176 | L |
||||||
| 177 | |||||||
| 178 | L |
||||||
| 179 | |||||||
| 180 | L |
||||||
| 181 | |||||||
| 182 | Currently only returns an empty list. | ||||||
| 183 | |||||||
| 184 | =cut | ||||||
| 185 | |||||||
| 186 | sub communities { | ||||||
| 187 | 1 | 1 | 1 | 6508 | my ($self) = @_; | ||
| 188 | |||||||
| 189 | 1 | 5 | my $url = "$BASE_URL/for/communities.json"; | ||||
| 190 | 1 | 8 | return $self->_get($url); | ||||
| 191 | } | ||||||
| 192 | |||||||
| 193 | =head2 user_public | ||||||
| 194 | |||||||
| 195 | $gt->user_public(USERNAME); | ||||||
| 196 | |||||||
| 197 | Returns an hash referene from /%username/public.json | ||||||
| 198 | Some of the fields look like these: | ||||||
| 199 | |||||||
| 200 | |||||||
| 201 | { | ||||||
| 202 | 'id' => 25031, | ||||||
| 203 | 'username' => 'szabgab', | ||||||
| 204 | 'number' => 'singular', | ||||||
| 205 | 'on' => 'gittip', | ||||||
| 206 | 'giving' => undef, | ||||||
| 207 | 'npatrons' => 7, | ||||||
| 208 | 'receiving' => '5.01', | ||||||
| 209 | 'goal' => undef, | ||||||
| 210 | 'avatar' => 'https://avatars.githubusercontent.com/u/48833?s=128', | ||||||
| 211 | 'bitcoin' => 'https://blockchain.info/address/1riba1Z6o3man18rASVyiG6NeFAhvf7rU', | ||||||
| 212 | 'elsewhere' => { | ||||||
| 213 | 'github' => { | ||||||
| 214 | 'user_id' => '48833', | ||||||
| 215 | 'id' => 85177, | ||||||
| 216 | 'user_name' => 'szabgab' | ||||||
| 217 | }, | ||||||
| 218 | 'twitter' => { | ||||||
| 219 | 'user_id' => '21182516', | ||||||
| 220 | 'user_name' => 'szabgab', | ||||||
| 221 | 'id' => 424525 | ||||||
| 222 | } | ||||||
| 223 | }, | ||||||
| 224 | }; | ||||||
| 225 | |||||||
| 226 | =cut | ||||||
| 227 | |||||||
| 228 | sub user_public { | ||||||
| 229 | 1 | 1 | 1 | 7871 | my ($self, $username) = @_; | ||
| 230 | |||||||
| 231 | 1 | 306 | my $url = "$BASE_URL/$username/public.json"; | ||||
| 232 | 1 | 23 | return $self->_get($url); | ||||
| 233 | } | ||||||
| 234 | |||||||
| 235 | # https://www.gratipay.com/about/tip-distribution.json | ||||||
| 236 | # returns an array of numbers \d+\.\d\d (over 8000 entries), probably the full list of tips. | ||||||
| 237 | |||||||
| 238 | =head2 user_tips | ||||||
| 239 | |||||||
| 240 | Requires API_KEY. | ||||||
| 241 | |||||||
| 242 | GET /%username/tips.json and returns an array reference of hashes. | ||||||
| 243 | Each hash is looks like this | ||||||
| 244 | |||||||
| 245 | { | ||||||
| 246 | 'username' => 'perlweekly', | ||||||
| 247 | 'platform' => 'gittip', | ||||||
| 248 | 'amount' => '1.01' | ||||||
| 249 | } | ||||||
| 250 | |||||||
| 251 | $gt->user_tips($username); | ||||||
| 252 | |||||||
| 253 | =cut | ||||||
| 254 | |||||||
| 255 | sub user_tips { | ||||||
| 256 | 0 | 0 | 1 | 0 | my ($self, $username) = @_; | ||
| 257 | |||||||
| 258 | 0 | 0 | my $url = "$BASE_URL/$username/tips.json"; | ||||
| 259 | 0 | 0 | return $self->_get($url); | ||||
| 260 | } | ||||||
| 261 | |||||||
| 262 | =head2 community_members | ||||||
| 263 | |||||||
| 264 | $gt->community_members('perl'); | ||||||
| 265 | |||||||
| 266 | Given the name of a community, returns a hash with 3 keys: | ||||||
| 267 | new, give, and receive corresponding to the 3 columns of the | ||||||
| 268 | https://www.gratipay.com/for/perl page. | ||||||
| 269 | |||||||
| 270 | Each key has an array reference as the value. Each arr has several elements: | ||||||
| 271 | |||||||
| 272 | { | ||||||
| 273 | new => [ | ||||||
| 274 | { | ||||||
| 275 | name => 'szabgab', | ||||||
| 276 | }, | ||||||
| 277 | { | ||||||
| 278 | name => 'rjbs', | ||||||
| 279 | }, | ||||||
| 280 | ... | ||||||
| 281 | ], | ||||||
| 282 | give => [ | ||||||
| 283 | ... | ||||||
| 284 | ], | ||||||
| 285 | receive => [ | ||||||
| 286 | ... | ||||||
| 287 | ], | ||||||
| 288 | } | ||||||
| 289 | |||||||
| 290 | There is no official API, so this call is scraping the HTML page. | ||||||
| 291 | Currently Gittip limits the number of people shown in each column to 100. | ||||||
| 292 | |||||||
| 293 | The user could set the limt at a lower number using limit=... in the URL. | ||||||
| 294 | The user can also set the starting user using offset=... | ||||||
| 295 | |||||||
| 296 | WWW::Gittip sends multiple requests as necessary to fetch all the users. | ||||||
| 297 | It uses limit=100 and the appropriate offset= for each request. | ||||||
| 298 | |||||||
| 299 | =cut | ||||||
| 300 | |||||||
| 301 | sub community_members { | ||||||
| 302 | 1 | 1 | 1 | 7128 | my ($self, $name) = @_; | ||
| 303 | |||||||
| 304 | # limit=10 | ||||||
| 305 | # offset=12 | ||||||
| 306 | |||||||
| 307 | 1 | 9 | my %NAMES = ( | ||||
| 308 | 'New Members' => 'new', | ||||||
| 309 | 'Top Givers' => 'give', | ||||||
| 310 | 'Top Receivers' => 'receive', | ||||||
| 311 | ); | ||||||
| 312 | |||||||
| 313 | 1 | 2 | my %members; | ||||
| 314 | |||||||
| 315 | 1 | 2 | my $limit = 100; | ||||
| 316 | 1 | 3 | my $offset = 0; | ||||
| 317 | 1 | 1 | my $total; | ||||
| 318 | 1 | 3 | while (1) { | ||||
| 319 | 6 | 33 | my $url = "$BASE_URL/for/$name?limit=$limit&offset=$offset"; | ||||
| 320 | |||||||
| 321 | 6 | 1497 | print "Requesting: $url\n"; | ||||
| 322 | |||||||
| 323 | 6 | 35 | my $response = $self->_get_html($url); | ||||
| 324 | |||||||
| 325 | 6 | 50 | 42 | if (not $response->is_success) { | |||
| 326 | 0 | 0 | warn 'Failed'; | ||||
| 327 | 0 | 0 | return; | ||||
| 328 | } | ||||||
| 329 | |||||||
| 330 | |||||||
| 331 | 6 | 885 | my $html = $response->decoded_content; | ||||
| 332 | 6 | 629763 | my $tree = HTML::TreeBuilder->new; | ||||
| 333 | 6 | 2599 | $tree->parse($html); | ||||
| 334 | |||||||
| 335 | 6 | 100 | 2087998 | if (not $total) { | |||
| 336 | # |
||||||
| 337 | # Perl |
||||||
| 338 | # 516 |
||||||
| 339 | # members |
||||||
| 340 | # | ||||||
| 341 | 1 | 13 | my $cl = $tree->look_down('class', 'on-community'); | ||||
| 342 | 1 | 500 | my $n = $cl->look_down('class', 'number'); | ||||
| 343 | 1 | 69 | $total = $n->as_text; | ||||
| 344 | } | ||||||
| 345 | |||||||
| 346 | 6 | 53 | my $leaderboard = $tree->look_down('id', 'leaderboard'); | ||||
| 347 | 6 | 5701 | foreach my $ch ($leaderboard->content_list) { | ||||
| 348 | 24 | 100 | 66 | 182 | next if not defined $ch or ref($ch) ne 'HTML::Element'; | ||
| 349 | # The page had 4 columns, one of them was empty. | ||||||
| 350 | 18 | 49 | my $h2 = $ch->look_down('_tag', 'h2'); | ||||
| 351 | 18 | 703 | my $type = $NAMES{ $h2->as_text }; | ||||
| 352 | |||||||
| 353 | 18 | 375 | my $group = $ch->look_down('class', 'group'); | ||||
| 354 | 18 | 750 | foreach my $member ($group->content_list) { | ||||
| 355 | 711 | 100 | 66 | 17073 | next if not defined $member or ref($member) ne 'HTML::Element'; | ||
| 356 | # I think these are the anonymous members. | ||||||
| 357 | |||||||
| 358 | 693 | 1633 | my $n = $member->look_down('class', 'name'); | ||||
| 359 | 693 | 58767 | push @{ $members{$type} }, { | ||||
| 693 | 2059 | ||||||
| 360 | name => $n->as_text, | ||||||
| 361 | }; | ||||||
| 362 | } | ||||||
| 363 | } | ||||||
| 364 | |||||||
| 365 | 6 | 21 | $offset += $limit; | ||||
| 366 | 6 | 50 | 12 | if (not $total) { | |||
| 367 | 0 | 0 | warn "Could not find total number of members\n"; | ||||
| 368 | 0 | 0 | last; | ||||
| 369 | } | ||||||
| 370 | 6 | 100 | 8102 | last if $offset >= $total; | |||
| 371 | } | ||||||
| 372 | |||||||
| 373 | 1 | 9 | return \%members; | ||||
| 374 | |||||||
| 375 | # |
||||||
| 376 | # | ||||||
| 377 | # |
||||||
| 378 | # New Members |
||||||
| 379 | # |
||||||
| 380 | # | ||||||
| 381 | # |
||||||
| 382 | # | ||||||
| 383 | # data-tip=""> | ||||||
| 384 | # | ||||||
| 385 | # | ||||||
| 386 | # style="background-image: url(\'https://avatars.githubusercontent.com/u/272648?s=128\')"> | ||||||
| 387 | # | ||||||
| 388 | # 14 hours | ||||||
| 389 | # dwierenga | ||||||
| 390 | # | ||||||
| 391 | # | ||||||
| 392 | # | ||||||
| 393 | |||||||
| 394 | } | ||||||
| 395 | |||||||
| 396 | sub _get_html { | ||||||
| 397 | 13 | 13 | 38 | my ($self, $url) = @_; | |||
| 398 | |||||||
| 399 | 13 | 158 | my $ua = LWP::UserAgent->new; | ||||
| 400 | 13 | 8509 | $ua->timeout(10); | ||||
| 401 | |||||||
| 402 | 13 | 246 | my $api_key = $self->api_key; | ||||
| 403 | 13 | 50 | 46 | if ($api_key) { | |||
| 404 | 0 | 0 | require MIME::Base64; | ||||
| 405 | 0 | 0 | $ua->default_header('Authorization', "Basic " . MIME::Base64::encode("$api_key:", '') ); | ||||
| 406 | } | ||||||
| 407 | |||||||
| 408 | 13 | 67 | my $response = $ua->get($url); | ||||
| 409 | 13 | 19782497 | return $response; | ||||
| 410 | |||||||
| 411 | } | ||||||
| 412 | |||||||
| 413 | |||||||
| 414 | sub _get { | ||||||
| 415 | 7 | 7 | 15 | my ($self, $url) = @_; | |||
| 416 | |||||||
| 417 | 7 | 25 | my $response = $self->_get_html($url); | ||||
| 418 | 7 | 100 | 43 | if (not $response->is_success) { | |||
| 419 | 1 | 26 | warn "Failed request $url\n"; | ||||
| 420 | 1 | 10 | warn $response->status_line . "\n"; | ||||
| 421 | 1 | 868 | return []; | ||||
| 422 | } | ||||||
| 423 | |||||||
| 424 | 6 | 127 | my $charts = $response->decoded_content; | ||||
| 425 | 6 | 50 | 33 | 2445 | if (not defined $charts or $charts eq '') { | ||
| 426 | 0 | 0 | warn "Empty return\n"; | ||||
| 427 | 0 | 0 | return []; | ||||
| 428 | } | ||||||
| 429 | 6 | 10 | my $data = eval { from_json $charts }; | ||||
| 6 | 40 | ||||||
| 430 | 6 | 50 | 3503 | if ($@) { | |||
| 431 | 0 | 0 | warn $@; | ||||
| 432 | 0 | 0 | warn "Data received: '$charts'\n"; | ||||
| 433 | 0 | 0 | $data = []; | ||||
| 434 | } | ||||||
| 435 | 6 | 292 | return $data; | ||||
| 436 | } | ||||||
| 437 | |||||||
| 438 | |||||||
| 439 | |||||||
| 440 | =head1 AUTHOR | ||||||
| 441 | |||||||
| 442 | Gabor Szabo L |
||||||
| 443 | |||||||
| 444 | =head1 LICENSE | ||||||
| 445 | |||||||
| 446 | Copyright (c) 2014, Gabor Szabo L |
||||||
| 447 | |||||||
| 448 | This library is free software; you can redistribute it and/or modify | ||||||
| 449 | it under the same terms as Perl itself. | ||||||
| 450 | |||||||
| 451 | =cut | ||||||
| 452 | |||||||
| 453 | 1; | ||||||
| 454 | |||||||
| 455 |