File Coverage

blib/lib/WWW/Fimfiction.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             package WWW::Fimfiction;
2              
3 1     1   23211 use 5.014;
  1         4  
  1         51  
4 1     1   6 use strict;
  1         9  
  1         40  
5 1     1   5 use warnings FATAL => 'all';
  1         17  
  1         44  
6 1     1   1367 use HTML::TreeBuilder;
  1         37173  
  1         14  
7 1     1   1663 use LWP::UserAgent;
  1         473235  
  1         43  
8 1     1   2796 use HTTP::Cookies;
  1         9522  
  1         39  
9 1     1   1158 use XML::Twig;
  0            
  0            
10             use Carp 'croak';
11             use JSON 'decode_json';
12              
13             our $VERSION = 'v0.3.7';
14              
15             =head1 NAME
16              
17             WWW::Fimfiction - CRUD tasks for fimfiction.net
18              
19             =cut
20              
21             =head1 SYNOPSIS
22              
23             use WWW::Fimfiction;
24              
25             my $ua = WWW::Fimfiction->new;
26              
27             $ua->login($username, $password);
28              
29             $ua->add_chapter($story_id, 'My Fabulous Chapter %i%', $text);
30              
31             =head1 METHODS
32              
33             Methods without explicit return values will return the WWW::Fimfiction object. Methods
34             will croak if something goes wrong.
35              
36             Bear in mind that the site doesn't take kindly to request spam, so consecutive calls
37             will have a small delay placed between them so the server doesn't get angry with you.
38              
39             =head2 new
40              
41             Makes a new object.
42              
43             =cut
44              
45             sub new {
46             my $class = shift;
47              
48             my $ua = LWP::UserAgent->new( cookie_jar => HTTP::Cookies->new );
49             $ua->agent("WWW-Fimfiction/$VERSION ");
50              
51             return bless { ua => $ua, last_request => 0 }, $class;
52             }
53              
54             sub _ua {
55             my $self = shift;
56             return $self->{ua};
57             }
58              
59             sub _assert_auth {
60             my $self = shift;
61             unless( $self->{auth} ) {
62             croak "Authentication required. Try calling ->login first.";
63             }
64             }
65              
66             sub _post {
67             my $self = shift;
68              
69             # Fimfiction will return an error if you try and spam requests,
70             # so sleep for a little if there's multiple requests
71             my $phase = $self->{last_request} + 2 - time;
72             sleep($phase) if $phase > 0;
73              
74             my $res = $self->_ua->post(@_);
75              
76             if( $res->is_success ) {
77             $self->{last_request} = time;
78             return $res;
79             }
80             else {
81             croak "Error: " . $res->status_line;
82             }
83             }
84              
85             sub _get {
86             my $self = shift;
87              
88             my $res = $self->_ua->get(@_);
89              
90             if( $res->is_success ) {
91             return $res;
92             }
93             else {
94             croak "Error: " . $res->status_line;
95             }
96             }
97              
98             =head2 login
99              
100             Args: ($username, $password)
101              
102             Authenticates the user. Tasks that manipulate data on the site require authentication,
103             so you'll have to call this before trying to add/edit/delete stuff.
104              
105             =cut
106              
107             sub login {
108             my( $self, $username, $password ) = @_;
109              
110             my $res = $self->_post('http://www.fimfiction.net/ajax/login.php', {
111             username => $username,
112             password => $password,
113             });
114              
115             my $code = $res->decoded_content;
116              
117             if( $code eq '0' ) {
118             $self->{auth} = $username;
119             return $self;
120             }
121             elsif( $code eq '1' ) {
122             croak 'Invalid password';
123             }
124             elsif( $code eq '2' ) {
125             croak 'Invalid username';
126             }
127             else {
128             croak "Bad credentials";
129             }
130             }
131              
132             =head2 add_chapter
133              
134             Args: ($story_id, [$chapter_title, $content])
135              
136             Adds a chapter to the given story. Returns the chapter id.
137              
138             If provided, additional arguments will be given to edit_chapter().
139              
140             =cut
141              
142             sub add_chapter {
143             my( $self, $story_id, $chapter_title, $content ) = @_;
144             my $chapter_id;
145              
146             $self->_assert_auth;
147              
148             my $form = { story => $story_id, title => $chapter_title };
149              
150             my $res = $self->_post('http://www.fimfiction.net/ajax/modify_chapter.php', $form);
151              
152             my $elt = XML::Twig::Elt->parse($res->decoded_content);
153              
154             if( my $error = $elt->field('error') ) {
155             croak $error;
156             }
157              
158             unless( $chapter_id = $elt->field('id') ) {
159             croak "Unexpected response: " . $res->decoded_content;
160             }
161              
162             if( defined $content ) {
163             $self->edit_chapter($chapter_id, $chapter_title, $content);
164             }
165              
166             return $chapter_id;
167             }
168              
169             =head2 edit_chapter
170              
171             Args: ($id, $title, $content)
172              
173             Edits chapter with the given title and content.
174              
175             =cut
176              
177             sub edit_chapter {
178             my( $self, $id, $title, $content ) = @_;
179              
180             $self->_assert_auth;
181              
182             my $form = { chapter => $id, title => $title, content => $content };
183              
184             my $res = $self->_post('http://www.fimfiction.net/ajax/modify_chapter.php', $form);
185              
186             # Reading the XML output here sometimes results in an unexpected error because Fimfiction spits
187             # out what XML::Twig considers invalid markup. The data isn't necessary except to check for
188             # error messages, so we'll just not bother.
189             return $self;
190             }
191              
192             =head2 publish_chapter
193              
194             Args: ($id)
195              
196             Toggles the publish status of a chapter. Returns 1 or 0 indicating the chapter's new publish status.
197              
198             =cut
199              
200             sub publish_chapter {
201             my( $self, $id ) = @_;
202              
203             $self->_assert_auth;
204              
205             my $form = { chapter => $id };
206              
207             my $res = $self->_post('http://www.fimfiction.net/ajax/publish_chapter.php', $form);
208              
209             my $elt = XML::Twig::Elt->parse($res->decoded_content);
210              
211             if( my $error = $elt->field('error') ) {
212             croak $error;
213             }
214             elsif( ( my $status = $elt->field('published') ) ne '' ) {
215             return $status;
216             }
217             else {
218             croak "Unexpected response: " . $res->decoded_content;
219             }
220             }
221              
222             =head2 delete_chapter
223              
224             Args: ($id)
225              
226             Deletes a chapter.
227              
228             =cut
229              
230             sub delete_chapter {
231             my ( $self, $id ) = @_;
232              
233             $self->_assert_auth;
234              
235             my $form = { chapter => $id, confirm => 'on' };
236              
237             # Get the form first, which has a 'noonce' value to confirm deletion (why?)
238             my $res = $self->_get("http://www.fimfiction.net/?view=delete_chapter&chapter=$id");
239              
240             my $tree = HTML::TreeBuilder->new;
241             $tree->parse_content($res->decoded_content);
242              
243             $form->{noonce} = $tree->look_down(_tag => 'input', name => 'noonce')->attr('value')
244             or croak "Unable to find hidden 'noonce' input field";
245              
246             # Do the actual deletion
247             $self->_post('http://www.fimfiction.net/index.php?view=delete_chapter', $form);
248              
249             return $self;
250             }
251              
252             =head2 get_story
253              
254             Args: ($id)
255              
256             Returns a hash ref of story metadata.
257              
258             =cut
259              
260             sub get_story {
261             my( $self, $id ) = @_;
262              
263             my $res = $self->_get("http://www.fimfiction.net/api/story.php?story=$id");
264              
265             return decode_json($res->decoded_content)->{story};
266             }
267              
268             =head1 AUTHOR
269              
270             Cameron Thornton Ecthor@cpan.orgE
271              
272             =head1 COPYRIGHT
273              
274             Copyright (c) 2012 Cameron Thornton.
275              
276             This program is free software; you can redistribute it and/or
277             modify it under the same terms as Perl itself.
278              
279             =cut
280              
281             1;