+#!/usr/bin/perl
+# $Id$
+# -*- perl -*- vim: ft=perl sw=4 sts=4
+
+use strict;
+use Net::MovableType;
+use Getopt::Long;
+use File::Temp ();
+use Term::Complete;
+use vars qw(%opts $mt);
+
+if (-r $ENV{HOME} . "/.blogclrc") {
+ open CONFIG, $ENV{HOME} . "/.blogclrc";
+ while(<CONFIG>) {
+ s/\#.*//;
+ next unless m/^([^=]*)=(.*)/;
+ $opts{$1}=$2;
+ }
+ close CONFIG;
+}
+
+Getopt::Long::Configure ("bundling", "auto_help");
+die "Invalid arguments\n" unless
+GetOptions (\%opts, 'username|l=s', 'password|p=s', 'url|u=s', 'blogid|b=i', 'title|t=s', 'category|c=s', 'file|f=s');
+
+if ($ARGV[0] eq "help") {
+ Getopt::Long::HelpMessage();
+ exit(0);
+}
+
+sub init_mt {
+ die "No url given.\n" unless(defined("$url"));
+ die "No username given.\n" unless(defined("$username"));
+ die "No passowrd given.\n" unless(defined("$password"));
+ $mt = new Net::MovableType($opts{'url'}) || die "$!";
+ $mt->username($opts{'username'});
+ $mt->password($opts{'password'});
+ my $blogid = $opts{'blogid'} || $mt->resolveBlogId($opts{'username'});
+ unless($blogid) {
+ my $userblogs=$mt->getUsersBlogs();
+ if($userblogs && $userblogs->[0]->{blogid}) {
+ $blogid=$userblogs->[0]->{'blogid'};
+ } else {
+ warn "Blog ID unknown.\n";
+ }
+ }
+ $mt->blogId($blogid);
+}
+
+sub print_recent {
+ my $entries = $mt->getRecentPosts(shift || 10);
+ while ( my $entry = shift @$entries ) {
+ printf("[%02d] - %s\n",
+ $entry->{postid}, $entry->{title} );
+ }
+# postid userid content title description dateCreated
+}
+
+sub print_categories {
+ print join("\n",map {$_->{categoryName}} @{$mt->getCategoryList()}), "\n";
+}
+
+sub show_post {
+ my $postid=shift;
+ my $post = $mt->getPost($postid) or die "$!";
+ my $categories = $mt->getPostCategories($postid) or die "$!";
+ if(defined($opts{file})) {
+ if($opts{file} eq "-") {
+ print STDOUT $post->{description}, "\n";
+ } else {
+ open FH, ">".$opts{file} or die "$!";
+ print $opts{file}, "\n";
+ print FH $post->{description}, "\n";
+ close FH;
+ }
+ } else {
+ printf("Title: %s (%d)\n", $post->{title}, $post->{postid});
+ if(@$categories>1) {
+ print "Categories:";
+ } elsif (@$categories==1) {
+ print "Category:";
+ }
+ foreach(@$categories) {
+ print " ", $_->{'categoryName'};
+ }
+ print "\n" if(@$categories);
+ print "Body:\n";
+ print $post->{description}, "\n";
+ }
+}
+
+sub edit_string {
+ my $string = shift;
+ if(defined($opts{file})) {
+ open FILE, $opts{file} or die "$!";
+ chop($string = join("", <FILE>));
+ close FILE;
+ return $string;
+ }
+ my $editor = $ENV{VISUAL} || $ENV{EDITOR};
+ $editor ||= "/usr/bin/sensible-editor" if (-x "/usr/bin/sensible-editor");
+ $editor ||= "/bin/vi" if (-x "/bin/vi");
+ $editor ||= "/usr/bin/nano" if (-x "/usr/bin/nano");
+ my $tmp = new File::Temp(SUFFIX => '.html');
+ print $tmp $string, "\n";
+ close $tmp;
+ my $mtime = (stat($tmp->filename))[9];
+ system(split (/ /, $editor), $tmp->filename);
+ if($mtime == (stat($tmp->filename))[9]) {
+ return undef;
+ } else {
+ open $tmp;
+ chop($string = join("", <$tmp>));
+ close $tmp;
+ return $string;
+ }
+}
+
+sub new_post {
+ my $title = $opts{title} || Complete('Title: ');
+ my $category = $opts{category} || Complete('Categories: ', map {$_->{categoryName}} @{$mt->getCategoryList()});
+ die "No title given.\n" unless($title);
+ my $description = edit_string("");
+ if(defined($description)) {
+ my $postid = $mt->newPost({ 'title' => $title, 'description' => $description });
+ $mt->setPostCategories($postid, $category ? [split(/[, ]+/, $category)] : $category) || warn "$!"
+ if(defined($category));
+ $mt->publishPost($postid);
+ } else {
+ die "No change. Aborting.\n";
+ }
+}
+
+sub edit_post {
+ my $postid=shift;
+ my $post = $mt->getPost($postid);
+ my $categories = $mt->getPostCategories($postid);
+ my $title = $opts{title} || Complete('Title: ', $post->{title}) || $post->{title};
+ my $description = edit_string($post->{description});
+ if(defined($description)) {
+ $mt->editPost($postid, { 'title' => $title, 'description' => $description });
+ $mt->setPostCategories($postid, $opts{category} ? [split(/[, ]/, $opts{category})] : $opts{category}) || warn "$!"
+ if(defined($opts{'category'}));
+ $mt->publishPost($postid);
+ } else {
+ die "No change. Aborting.\n";
+ }
+}
+
+
+if ($ARGV[0] eq "help") {
+ Getopt::Long::HelpMessage();
+} elsif ($ARGV[0] eq "categories") {
+ init_mt();
+ print_categories();
+} elsif ($ARGV[0] eq "list" && $ARGV[1] =~ /^\d*$/) {
+ init_mt();
+ print_recent($ARGV[1]);
+} elsif ($ARGV[0] eq "new") {
+ init_mt();
+ new_post();
+} elsif ($ARGV[0] eq "edit" && $ARGV[1] =~ /^\d+$/) {
+ init_mt();
+ edit_post($ARGV[1])
+} elsif ($ARGV[0] eq "show" && $ARGV[1] =~ /^\d+$/) {
+ init_mt();
+ show_post($ARGV[1])
+} else {
+ Getopt::Long::HelpMessage();
+ exit(1);
+}
+__END__
+=head1 NAME
+
+blogcl - Blog from the command-line
+
+=head1 SYNOPSIS
+
+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> }
+
+=head1 DESCRIPTION
+
+This is a simple script used to blog from the command-line. It was designed
+for use with Drupal, but may work with any blog featuring a Movable Type
+compatible interface.
+
+One of the commands below must be present.
+
+=over 4
+
+=item B<new>
+
+Creates a new blog entry.
+
+=item B<edit>
+
+Edit an existing blog entry. An existing postid must be specified. To find a
+post's postid, use B<list>.
+
+=item B<show>
+
+Shows the contents of the post specified by the postid given on the
+command-line.
+
+=item B<list>
+
+List recents posts and their postids. An optional integer specifies the number
+of posts to list.
+
+=item B<categories>
+
+Gives categories that are valid parameters to the B<--category> option.
+
+=item B<help>
+
+Show usage information.
+
+=back
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-b>, B<--blogid>
+
+Specify a blog ID. Defaults to the first blog found.
+
+=item B<-c>, B<--category>
+
+Specify a category for a new or existing entry. For multiple categories,
+seperate them with a comma. Ignored except when used with either B<new> or
+B<edit>.
+
+=item B<-f>, B<--file>
+
+When used with B<new> or B<edit>, specifies a file from which to read the
+post's body. When used with B<show>, specifies a file to which to write the
+post's body.
+
+=item B<-l>, B<--username>
+
+Specify a username.
+
+=item B<-p>, B<--password>
+
+Specify a password.
+
+=item B<-t>, B<--title>
+
+Specify a title for a new or existing entry. Ignored except when used with
+either B<new> or B<edit>.
+
+=item B<-u>, B<--url>
+
+Specify the URL of the XMLRPC interface.
+
+=back
+
+=head1 FILES
+
+=over 4
+
+=item F<~/.blogclrc>
+
+Contains options of format parameter=value. Only the long form of
+options may be used. Omit leading hyphens.
+
+=back
+
+=head1 SEE ALSO
+
+L<Net::MovableType>
+
+=head1 BUGS
+
+Error checking is minimal.
+
+=head1 COPYRIGHT
+
+Copyright by Tim Pope. All rights reserved.
+
+This library is a free software; you may redistribute it and/or modify it under
+the same terms as perl itself.
+
+=head1 AUTHORS
+
+Tim Pope, E<lt>perl@relongto.usE<gt>.
+
+L<http://www.sexygeek.org/>
+
+=cut