XMLRPC::Lite Perl

2003-08-25

I decided to try to add moblogging to my site and for some reason I thought the service I was going to use used one of the XMLRPC APIs like the Blogger API or the metaWeblog API. Well, it turned out the service I was thinking of using didn't actually support these standards, although it may soon. But, I didn't figure that out until after I had implemented it.

It took a while to dig through the sparse docs and find out how to get it to work so in the interest of helping others here's some sample code. Think of it as a working example of using the XMLRPC::Lite Perl module. It's also a working example of using the APIs above to post to most blog systems and there is also an example of being on the receiving end and excepting these calls for your own custom blog software.

Uploading an image and post to a blog

#!/usr/bin/perl
use strict;
use warnings;

use IO::Handle;                     # 5.004 or higher
use Data::Dumper;
use XMLRPC::Lite;
use File::Basename;

{
   my $blogid   = "1";
   my $username = "username";
   my $password = "password";
   my $proxyurl = "http://mysite.com/mt/mt-xmlrpc.cgi";
   my $jpgfile  = "d:/temp/nutch.jpg";

   my $bits = read_bin_file ($jpgfile);

   #------------------
   # upload the picture
   #
   my $picresult = XMLRPC::Lite
      -> proxy($proxyurl)
      -> call('metaWeblog.newMediaObject', $blogid, $username, $password,
      {
        'bits' => XMLRPC::Data->type('base64', $bits),
        'type' => "image/jpeg",
        'name' => basename($filename)
      }
      )
      -> result;
   if (!defined ($picresult))
   {
      die "failed $!";
   }
   else
   {
      print "url = (", $picresult->{'url'}, ")\n";
      #---------------
      # post the message
      #
      my $msgresult = XMLRPC::Lite
         -> proxy($proxyurl)
         -> call('metaWeblog.newPost', $blogid, $username, $password,
            {
               'title'             => "this is the title",
               'description'       => "and this is\n\n" .
                                      "the body and a link to the picture\n\n" .
                                      "<img src=\"" . $picresult->{'url'} . "\">\n\nthe end :-)\n",
               'mt_allow_comments' => 1,  # must be int, not string
               'mt_allow_pings'    => 1   # must be int, not string
#              'mt_convert_breaks' => "", # string, see mt.supportedTextFilters for a valid value
#              'mt_text_more'      => "", # the extended entry
#              'mt_excerpt'        => "",
#              'mt_keywords'       => "",
#              'mt_tb_ping_urls'   => ??, #array of strings, ping URLs
            },
            1 # 1 = publish
            )
         -> result;
      if (!defined ($msgresult))
      {
         die "failed $!";
      }
      else
      {
         print Dumper ($msgresult);
      }
   }
}

sub read_bin_file
{
   my ($filename) = @_;

   #--------------------
   # read in the picture
   my $fh = IO::Handle->new();
   open($fh, $filename) or die "$!";
   local($/) = undef;  # slurp
   binmode($fh);
   my $bits  = <$fh>;
   close($fh);

   return $bits;
}

Receiving an image and a post through XMLRPC

actually there are stubs for every function listed as being supported by moveable type but only metaWeblog.newPost, metaWeblog.newMediaObject and blogger.getUsersBlogs have any body. The rest just dump all the parameters passed to them to a log file so you can see what they are.

#!/usr/bin/perl
use strict;
use warnings;

# note that the docs for the various APIs were copied from the Moveable Type Docs

# start a package to put all the functions in
package MPost;

use IO::Handle;                     # 5.004 or higher

use XMLRPC::Transport::HTTP;
use Data::Dumper;

my $username     = "username";
my $password     = "password";
my $imageFolder  = "/home/mysite/blogimages/";      # path to put images
my $imageUrlBase = "http://mysite.com/blogimages/"; # base URL for images

# this is so I can see what's happening because this is a server side
# script and as it's being called from another script I'll never see
# the output.  If you know of a better way to debug this stuff
# please tell me.

sub logprint
{
   my $FH = IO::Handle->new();

   # record that we actually made it here
   open ($FH, ">>mpost.txt");
   print $FH @_;
   close ($FH);
}

logprint ("-----------------------------------\n", scalar(localtime), "\n");

#
# tell XMLRPC which Perl packages to search for functions.
#
# in other words, if you get a request for method "blogger.newPost"
# the XMLRPC handler stuff will try to call the perl function
# blogger::newPost()
#
my $server = XMLRPC::Transport::HTTP::CGI
   -> dispatch_to('blogger', 'metaWeblog', 'mt')
   -> handle
;

#
# prints the name of the function and all the arguments passed to it
# so we can see what is actually passed to us vs what we think it will
# pass to us
#
sub dumpit
{
   my $funcname = shift;
   my @args     = @_;

   # record what function was called and what as passed back
   logprint ("-----------------------------------\n",
             "Called: (", $funcname, ")\n",
             localtime(time), "\n",
             Dumper (\@args));

   # return some generic result just for the heck of it
   return {
     flerror => XMLRPC::Data->type('boolean',0),
     message => "called $funcname"
   };
}

sub verifyUser
{
   my ($un, $pw) = @_;

   return ($un eq $username && $pw eq $password);
}

#blogger.newPost
#Description: Creates a new post, and optionally publishes it.
#Parameters: String appkey, String blogid, String username, String password, String content, boolean publish
#
#Return value: on success, String postid of new post; on failure, fault
#
#--------------
#metaWeblog.newPost
#Description: Creates a new post, and optionally publishes it.
#Parameters: String blogid, String username, String password, struct content, boolean publish
#
#Return value: on success, String postid of new post; on failure, fault
#
#Notes: the struct content can contain the following standard keys:
#
#   title, for the title of the entry;
#   description, for the body of the entry;
#   dateCreated, to set the created-on date of the entry.
#
# In addition, Movable Type's implementation allows you to pass in values for five other keys:
#
#   int mt_allow_comments, the value for the allow_comments field;
#   int mt_allow_pings, the value for the allow_pings field;#
#   String mt_convert_breaks, the value for the convert_breaks field;
#   String mt_text_more, the value for the additional entry text;
#   String mt_excerpt, the value for the excerpt field;
#   String mt_keywords, the value for the keywords field;
#   and array mt_tb_ping_urls, the list of TrackBack ping URLs for this entry.
#
# If specified, dateCreated should be in ISO.8601 format.
#
#

sub newPost
{
   dumpit ("newPost", @_);

   my ($class, $appkey, $blogid, $username, $password, $content, $struct);
   my %parts = ();

   $class = shift;

   if ($class eq "blogger")
   {
      ($appkey, $blogid, $username, $password, $content) = @_;
      return;
   }
   elsif ($class eq "metaWeblog")
   {
      ($blogid, $username, $password, $struct) = @_;

      $parts{'title'} = $struct->{'title'};
      $parts{'body'}  = $struct->{'description'};
   }
   else
   {
      return;
   }

   if (!verifyUser($username, $password))
   {
      return;
   }

   # at this point you have the title and body in a "parts" hash.
   # you can pass that to some function to store it in your custom blog
   # you return some string based ID that could be used to reference this
   # post in other API calls.

   # my $id = make_new_post (\%parts);

   # assume make_new_posts() post to the blog and returns some id
   return $id;
}

#
#blogger.editPost
#Description: Updates the information about an existing post.
#Parameters: String appkey, String postid, String username, String password, String content, boolean publish
#
#Return value: on success, boolean true value; on failure, fault
#
#--------------
#metaWeblog.editPost
#Description: Updates information about an existing post.
#Parameters: String postid, String username, String password, struct content, boolean publish
#
#Return value: on success, boolean true value; on failure, fault
#
#Notes: the struct content can contain the following standard keys:
#
#   title, for the title of the entry;
#   description, for the body of the entry;#
#   dateCreated, to set the created-on date of the entry.
#
# In addition, Movable Type's implementation allows you to pass in values for five other keys:
#
#   int mt_allow_comments, the value for the allow_comments field;
#   int mt_allow_pings, the value for the allow_pings field;
#   String mt_convert_breaks, the value for the convert_breaks field;
#   String mt_text_more, the value for the additional entry text;
#   String mt_excerpt, the value for the excerpt field;
#   String mt_keywords, the value for the keywords field;
#   array mt_tb_ping_urls, the list of TrackBack ping URLs for this entry.
#
# If specified, dateCreated should be in ISO.8601 format.
#
#

sub editPost
{
   return dumpit ("editPost", @_);
}

#
#blogger.deletePost
#Description: Deletes a post.
#Parameters: String appkey, String postid, String username, String password, boolean publish
#
#Return value: on success, boolean true value; on failure, fault
#

sub deletePost
{
   return dumpit ("deletePost", @_);
}

#
#blogger.getRecentPosts
#Description: Returns a list of the most recent posts in the system.
#Parameters: String appkey, String blogid, String username, String password, int numberOfPosts
#
#Return value: on success, array of structs containing
#
#   ISO.8601 dateCreated
#   String userid
#   String postid
#   String content
#
# on failure, fault
#
#Notes: dateCreated is in the timezone of the weblog blogid
#
#--------------
#metaWeblog.getRecentPosts
#Description: Returns a list of the most recent posts in the system.
#Parameters: String blogid, String username, String password, int numberOfPosts
#
#Return value: on success, array of structs containing
#
#   ISO.8601 dateCreated
#   String userid
#   String postid
#   String description
#   String title
#   String link
#   String permaLink
#   String mt_excerpt
#   String mt_text_more
#   int mt_allow_comments
#   int mt_allow_pings
#   String mt_convert_breaks
#   String mt_keywords
#
# on failure, fault
#
#Notes: dateCreated is in the timezone of the weblog blogid;
# link and permaLink are the URL pointing to the archived post
#

sub getRecentPosts
{
   return dumpit ("getRecentPosts", @_);
}

#
#blogger.getUsersBlogs
#Description: Returns a list of weblogs to which an author has posting privileges.
#Parameters: String appkey, String username, String password
#
#Return value: on success, array of structs containing
#
#   String url
#   String blogid
#   String blogName
#
# on failure, fault
#

# this function allowed me to get w.bloggar and blogbuddy to at least start up so I could test

sub getUsersBlogs
{
   dumpit ("getUserBlogs", @_);

   my ($class, $appkey, $username, $password) = @_;

   logprint ("u=($username), p=($password)\n");

   if (!verifyUser($username, $password))
   {
      return 0;
   }

   return
      [
          {
            'url' => 'http://mysite.com/',
            'blogid' => '1', # must be string, not int
            'blogName' => 'Myblog'
          },
      ];
}

#
#blogger.getUserInfo
#Description: Returns information about an author in the system.
#Parameters: String appkey, String username, String password
#
#Return value: on success, struct containing
#
#   String userid
#   String firstname
#   String lastname
#   String nickname
#   String email
#   String url
#
# on failure, fault
#
#Notes: firstname is the Movable Type username up to the first space character,
# and lastname is the username after the first space character.

sub getUserInfo
{
   return dumpit ("getUserInfo", @_);
}

#***********************************  ************************************
#***********************************  ************************************
#***********************************  ************************************

#package metaWeblog;

#--------------
#metaWeblog.getPost
#Description: Returns information about a specific post.
#Parameters: String postid, String username, String password
#
#Return value: on success, struct containing
#
#   String userid,
#   ISO.8601 dateCreated
#   String postid
#   String description
#   String title
#   String link
#   String permaLink
#   String mt_excerpt
#   String mt_text_more
#   int mt_allow_comments
#   int mt_allow_pings
#   String mt_convert_breaks
#   String mt_keywords;
#
# on failure, fault
#
#Notes: link and permaLink are both the URL pointing to the archived post.
#The fields prefixed with mt_ are Movable Type extensions to the metaWeblog.getPost API.
#
#

sub getPost
{
   return dumpit ("getPost", @_);
}

#
#metaWeblog.newMediaObject
#Description: Uploads a file to your webserver.
#Parameters: String blogid, String username, String password, struct file
#
#Return value: URL to the uploaded file.
#
#Notes: the struct file should contain two keys:
#
#   base64 bits (the base64-encoded contents of the file)
#   String name (the name of the file).
#   The type key (media type of the file) is currently ignored by MT
#

sub newMediaObject
{
   dumpit ("newMediaObject", @_);

   my ($class, $blogid, $username, $password, $struct) = @_;

   if (!verifyUser($username, $password))
   {
      return;
   }

   # I prefix the file with the date and hope that I won't upload to files
   # with the same name in the same day.  I could check for that case
   # but I'm too lazy.  Plus, I really just want to know the day I posted
   # the file so I can connect it to a particular blog entry

   #   0    1    2     3     4    5     6     7     8
   # ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
   my @date = localtime(time);
   my $filename = sprintf ("%04d-%02d-%02d-%s", $date[5] + 1900, $date[4] + 1, $date[3], $struct->{'name'});

   # remove possibly bad characters
   $filename =~ s/[^.a-z0-9-_]/_/sig;
   my $savename = ">" . $imageFolder . $filename;

   logprint ("savename = ($savename)\n");

   my $fh = IO::Handle->new();
   open($fh, $savename) or die "$!";
   local($/) = undef;  # slurp
   binmode($fh);
   print $fh $struct->{'bits'};
   close($fh);

   logprint ("url      = (", $imageUrlBase , $filename, ")\n");

   # return the URL to the file
   return {
      'url' => $imageUrlBase . $filename
      };
}

#***********************************  ************************************
#***********************************  ************************************
#***********************************  ************************************

#package mt;

#
#mt.getRecentPostTitles
#Description: Returns a bandwidth-friendly list of the most recent posts in the system.
#Parameters: String blogid, String username, String password, int numberOfPosts
#
#Return value: on success
#
#   array of structs containing ISO.8601 dateCreated
#   String userid
#   String postid
#   String title
#
# on failure, fault
#
#Notes: dateCreated is in the timezone of the weblog blogid
#

sub getRecentPostTitles
{
   return dumpit ("getRecentPostTitles", @_);
}

#
#mt.getCategoryList
#Description: Returns a list of all categories defined in the weblog.
#Parameters: String blogid, String username, String password
#
#Return value: on success, an array of structs containing
#
#   String categoryId
#   String categoryName
#
# on failure, fault.
#

sub getCategoryList
{
   return dumpit ("getCategoryList", @_);
}

#
#mt.getPostCategories
#Description: Returns a list of all categories to which the post is assigned.
#Parameters: String postid, String username, String password
#
#Return value: on success, an array of structs containing
#
#   String categoryName
#   String categoryId
#   and boolean isPrimary
#
# on failure, fault.
#
#Notes: isPrimary denotes whether a category is the post's primary category.
#

sub getPostCategories
{
   return dumpit ("getPostCategories", @_);
}

#
#mt.setPostCategories
#Description: Sets the categories for a post.
#Parameters: String postid, String username, String password, array categories
#
#Return value: on success, boolean true value; on failure, fault
#
#Notes: the array categories is an array of structs containing
#
#   String categoryId
#   boolean isPrimary
#
# Using isPrimary to set the primary category is optional--in the absence of this flag,
# the first struct in the array will be assigned the primary category for the post.
#

sub setPostCategories
{
   return dumpit ("setPostCategories", @_);
}

#
#mt.supportedMethods
#Description: Retrieve information about the XML-RPC methods supported by the server.
#Parameters: none
#
#Return value: an array of method names supported by the server.
#

sub supportedMethods
{
   return dumpit ("supportedMethods", @_);
}

#
#mt.supportedTextFilters
#Description: Retrieve information about the text formatting plugins supported by the server.
#Parameters: none
#
#Return value: an array of structs containing
#
#   String key
#   String label.
#
#key is the unique string identifying a text formatting plugin, and label is
#the readable description to be displayed to a user. key is the value that
#should be passed in the mt_convert_breaks parameter to newPost and editPost.
#

sub supportedTextFilters
{
   return dumpit ("supportedTextFilters", @_);
}

#
#mt.getTrackbackPings
#Description: Retrieve the list of TrackBack pings posted to a particular entry
#This could be used to programmatically retrieve the list of pings for a particular
#entry, then iterate through each of those pings doing the same, until one has
#built up a graph of the web of entries referencing one another on a particular
#topic.
#
#Parameters: String postid
#
#Return value: an array of structs containing
#
#   String pingTitle (the title of the entry sent in the ping)
#   String pingURL (the URL of the entry)
#   String pingIP (the IP address of the host that sent the ping).
#

sub getTrackbackPings
{
   return dumpit ("getTrackbackPings", @_);
}

#
#mt.publishPost
#Description: Publish (rebuild) all of the static files related to an entry from
#your weblog. Equivalent to saving an entry in the system (but without the ping).
#
#Parameters: String postid, String username, String password
#
#Return value: on success, boolean true value; on failure, fault
#
#

sub publishPost
{
   return dumpit ("publishPost", @_);
}

#***********************************  ************************************
#***********************************  ************************************
#***********************************  ************************************

#copy all the functions in the "MPost" package to the various packages to handle each API.

package blogger;
BEGIN { @blogger::ISA = qw( MPost ); }

package metaWeblog;
BEGIN { @metaWeblog::ISA = qw( MPost ); }

package mt;
BEGIN { @mt::ISA = qw( MPost ); }
</pre></blockquote>

<b>Simple example of calling a few Blogger API and metaWeblog API functions through XMLRPC</b>

<blockquote><pre>#!/usr/bin/perl
use strict;
use warnings;

use Data::Dumper;

use XMLRPC::Lite;

my $username = "username";
my $password = "password";
my $blogid   = "1";
my $proxyurl = 'http://mysite.com/mt/mt-xmlrpc.cgi';
my $appkey   = "anything"; # the value of this is not important, it's just an ID for your script

#
# uncomment one of the call lines
#
my $res = XMLRPC::Lite
   -> proxy($proxyurl)
   ## -> call('blogger.getUsersBlogs', $appkey, $username, $password)
   ## -> call('blogger.getRecentPosts', $appkey, $blogid, $username, $password, 4 )
   -> call('metaWeblog.getRecentPosts', $blogid, $username, $password, 4)
   -> result;

if (defined ($res))
{
   print "--success--\n";
   print Dumper ($res);
}
else
{
   print "failed: $!";
}
Comments
Do software development *best practices* fit game development
Munchie and Clank