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 |