Added blogcl
[tpope-extra.git] / perl / blogcl
1 #!/usr/bin/perl
2 # $Id$
3 # -*- perl -*- vim: ft=perl sw=4 sts=4
4
5 use strict;
6 use Net::MovableType;
7 use Getopt::Long;
8 use File::Temp ();
9 use Term::Complete;
10 use vars qw(%opts $mt);
11
12 if (-r $ENV{HOME} . "/.blogclrc") {
13     open CONFIG, $ENV{HOME} . "/.blogclrc";
14     while(<CONFIG>) {
15         s/\#.*//;
16         next unless m/^([^=]*)=(.*)/;
17         $opts{$1}=$2;
18     }
19     close CONFIG;
20 }
21
22 Getopt::Long::Configure ("bundling", "auto_help");
23 die "Invalid arguments\n" unless
24 GetOptions (\%opts, 'username|l=s', 'password|p=s', 'url|u=s', 'blogid|b=i', 'title|t=s', 'category|c=s', 'file|f=s');
25
26 if ($ARGV[0] eq "help") {
27     Getopt::Long::HelpMessage();
28     exit(0);
29 }
30
31 sub init_mt {
32     die "No url given.\n" unless(defined("$url"));
33     die "No username given.\n" unless(defined("$username"));
34     die "No passowrd given.\n" unless(defined("$password"));
35     $mt = new Net::MovableType($opts{'url'}) || die "$!";
36     $mt->username($opts{'username'});
37     $mt->password($opts{'password'});
38     my $blogid = $opts{'blogid'} || $mt->resolveBlogId($opts{'username'});
39     unless($blogid) {
40         my $userblogs=$mt->getUsersBlogs();
41         if($userblogs && $userblogs->[0]->{blogid}) {
42             $blogid=$userblogs->[0]->{'blogid'};
43         } else {
44             warn "Blog ID unknown.\n";
45         }
46     }
47     $mt->blogId($blogid);
48 }
49
50 sub print_recent {
51     my $entries = $mt->getRecentPosts(shift || 10);
52     while ( my $entry = shift @$entries ) {
53         printf("[%02d] - %s\n",
54             $entry->{postid}, $entry->{title} );
55     }
56 #   postid userid content title description dateCreated
57 }
58
59 sub print_categories {
60     print join("\n",map {$_->{categoryName}} @{$mt->getCategoryList()}), "\n";
61 }
62
63 sub show_post {
64     my $postid=shift;
65     my $post = $mt->getPost($postid) or die "$!";
66     my $categories = $mt->getPostCategories($postid) or die "$!";
67     if(defined($opts{file})) {
68         if($opts{file} eq "-") {
69             print STDOUT $post->{description}, "\n";
70         } else {
71             open FH, ">".$opts{file} or die "$!";
72             print $opts{file}, "\n";
73             print FH $post->{description}, "\n";
74             close FH;
75         }
76     } else {
77         printf("Title: %s (%d)\n", $post->{title}, $post->{postid});
78         if(@$categories>1) {
79             print "Categories:";
80         } elsif (@$categories==1) {
81             print "Category:";
82         }
83         foreach(@$categories) {
84             print " ", $_->{'categoryName'};
85         }
86         print "\n" if(@$categories);
87         print "Body:\n";
88         print $post->{description}, "\n";
89     }
90 }
91
92 sub edit_string {
93     my $string = shift;
94     if(defined($opts{file})) {
95         open FILE, $opts{file} or die "$!";
96         chop($string = join("", <FILE>));
97         close FILE;
98         return $string;
99     }
100     my $editor = $ENV{VISUAL} || $ENV{EDITOR};
101     $editor ||= "/usr/bin/sensible-editor" if (-x "/usr/bin/sensible-editor"); 
102     $editor ||= "/bin/vi" if (-x "/bin/vi"); 
103     $editor ||= "/usr/bin/nano" if (-x "/usr/bin/nano"); 
104     my $tmp = new File::Temp(SUFFIX => '.html');
105     print $tmp $string, "\n";
106     close $tmp;
107     my $mtime = (stat($tmp->filename))[9];
108     system(split (/ /, $editor), $tmp->filename);
109     if($mtime == (stat($tmp->filename))[9]) {
110         return undef;
111     } else {
112         open $tmp;
113         chop($string = join("", <$tmp>));
114         close $tmp;
115         return $string;
116     }
117 }
118
119 sub new_post {
120     my $title = $opts{title} || Complete('Title: ');
121     my $category = $opts{category} || Complete('Categories: ', map {$_->{categoryName}} @{$mt->getCategoryList()});
122     die "No title given.\n" unless($title);
123     my $description = edit_string("");
124     if(defined($description)) {
125         my $postid = $mt->newPost({ 'title' => $title, 'description' => $description });
126         $mt->setPostCategories($postid, $category ? [split(/[, ]+/, $category)] : $category) || warn "$!"
127         if(defined($category));
128         $mt->publishPost($postid);
129     } else {
130         die "No change. Aborting.\n";
131     }
132 }
133
134 sub edit_post {
135     my $postid=shift;
136     my $post = $mt->getPost($postid);
137     my $categories = $mt->getPostCategories($postid);
138     my $title = $opts{title} || Complete('Title: ', $post->{title}) || $post->{title};
139     my $description = edit_string($post->{description});
140     if(defined($description)) {
141         $mt->editPost($postid, { 'title' => $title, 'description' => $description });
142         $mt->setPostCategories($postid, $opts{category} ? [split(/[, ]/, $opts{category})] : $opts{category}) || warn "$!"
143         if(defined($opts{'category'}));
144         $mt->publishPost($postid);
145     } else {
146         die "No change. Aborting.\n";
147     }
148 }
149
150
151 if ($ARGV[0] eq "help") {
152     Getopt::Long::HelpMessage();
153 } elsif ($ARGV[0] eq "categories") {
154     init_mt();
155     print_categories();
156 } elsif ($ARGV[0] eq "list" && $ARGV[1] =~ /^\d*$/) {
157     init_mt();
158     print_recent($ARGV[1]);
159 } elsif ($ARGV[0] eq "new") {
160     init_mt();
161     new_post();
162 } elsif ($ARGV[0] eq "edit" && $ARGV[1] =~ /^\d+$/) {
163     init_mt();
164     edit_post($ARGV[1])
165 } elsif ($ARGV[0] eq "show" && $ARGV[1] =~ /^\d+$/) {
166     init_mt();
167     show_post($ARGV[1])
168 } else {
169     Getopt::Long::HelpMessage();
170     exit(1);
171 }
172 __END__
173 =head1 NAME
174
175 blogcl - Blog from the command-line
176
177 =head1 SYNOPSIS
178
179 B<blogcl> S<[ B<--title>|B<-t> "Title" ]> S<[ B<--category>|B<-c> category[,...] ]> S<[ B<--file>|B<-f> F<file> ]> S<[ B<--username>|B<-l> username ]> S<[ B<--password>|B<-p> password ]> S<[ B<--url>|B<-u> url ]> S<[ B<--blogid>|B<-b> blogid ]> { B<new> | B<edit> postid | B<show> postid | B<list> [count] | B<categories> }
180
181 =head1 DESCRIPTION
182
183 This is a simple script used to blog from the command-line.  It was designed
184 for use with Drupal, but may work with any blog featuring a Movable Type
185 compatible interface.
186
187 One of the commands below must be present.
188
189 =over 4
190
191 =item B<new>
192
193 Creates a new blog entry.
194
195 =item B<edit>
196
197 Edit an existing blog entry.  An existing postid must be specified.  To find a
198 post's postid, use B<list>.
199
200 =item B<show>
201
202 Shows the contents of the post specified by the postid given on the
203 command-line.
204
205 =item B<list>
206
207 List recents posts and their postids.  An optional integer specifies the number
208 of posts to list.
209
210 =item B<categories>
211
212 Gives categories that are valid parameters to the B<--category> option.
213
214 =item B<help>
215
216 Show usage information.
217
218 =back
219
220 =head1 OPTIONS
221
222 =over 4
223
224 =item B<-b>, B<--blogid>
225
226 Specify a blog ID.  Defaults to the first blog found.
227
228 =item B<-c>, B<--category>
229
230 Specify a category for a new or existing entry.  For multiple categories,
231 seperate them with a comma.  Ignored except when used with either B<new> or
232 B<edit>.
233
234 =item B<-f>, B<--file>
235
236 When used with B<new> or B<edit>, specifies a file from which to read the
237 post's body.  When used with B<show>, specifies a file to which to write the
238 post's body.
239
240 =item B<-l>, B<--username>
241
242 Specify a username.
243
244 =item B<-p>, B<--password>
245
246 Specify a password.
247
248 =item B<-t>, B<--title>
249
250 Specify a title for a new or existing entry.  Ignored except when used with
251 either B<new> or B<edit>.
252
253 =item B<-u>, B<--url>
254
255 Specify the URL of the XMLRPC interface.
256
257 =back
258
259 =head1 FILES
260
261 =over 4
262
263 =item F<~/.blogclrc>
264
265 Contains options of format parameter=value.  Only the long form of
266 options may be used.  Omit leading hyphens.
267
268 =back
269
270 =head1 SEE ALSO
271
272 L<Net::MovableType>
273
274 =head1 BUGS
275
276 Error checking is minimal.
277
278 =head1 COPYRIGHT
279
280 Copyright by Tim Pope. All rights reserved.
281
282 This library is a free software; you may redistribute it and/or modify it under
283 the same terms as perl itself.
284
285 =head1 AUTHORS
286
287 Tim Pope, E<lt>perl@relongto.usE<gt>.
288
289 L<http://www.sexygeek.org/>
290
291 =cut