Blogging From PowerShell - Editing Posts With the MetaWeblog API

Fri, May 5, 2006 4-minute read

In the last post, I mentioned that I wanted to update all of my old Monad posts to include a reference to Windows PowerShell.  That way, users who use the “PowerShell” keyword to search for a topic I’ve covered (under the Monad codename) can still find it.

I provided a script to get all of the posts from a MetaWeblog-enabled host, so our next goal is to actually make the changes.

To do this, we’ll follow the get, modify, and set pattern.  You get the object-based representation of a post (covered in the previous entry,) modify its properties in Monad, and then upload that post back to the web host.  For example:

$blog = .\get-posts.ps1 $endPoint $null "username" "password" 1
$blog.description += "<p>Extra content here</p>"
.\set-blogpost.ps1 $endpoint $null "username" "password" $blog $true

To get the posts originally, we used the MetaWeblog API called “getRecentPosts.”  Now, to upload the post back to the web host, we use a different API, called “editPost.”  As we did in the previous example, we use .Net to actually talk with the web server.

The XML required for the editPost method only requires 5 things: the Post ID (matching the Post ID we got originally,) a username, password, more XML that describes the post, and a flag that determines if we should publish the post or not.

We could use the Set-BlogPost script to craft the request XML, and also dig around in the $blog object to craft its portion of the XML as well.  But we have an opportunity to introduce a better design here.  In Refactoring parlance, the code has a bad “smell” - namely feature envy.  If your code is overly involved with the methods and properties of another object, then probably that other object should do the work instead.  This is the core of Jeffrey’s point: “Whenever you are adding some functions, you should make a conscious decision about whether those functions are best exposed as a “function” or as a “type extension”.

Since we own the blog object (we create it in get-posts.ps1,) we’ll have the post XML-ify itself in its ToString() method.

So first, our addition to get-posts.ps1:

         ## Pull the ID of the post
         "postid" { $propertyValue = $property.value.string; break }
      }

      ## Add the synthetic property     
      $blogEntry | add-member NoteProperty $propertyName $propertyValue
   } 

-------------8<--------------------------------

   ## Add the ToString method
   ## This method formats the post so that it may be used in an edit
   $blogEntry | add-member -force ScriptMethod ToString {

      ## A function that encoded our content into an XML-friendly
      ## format
      function encode([string] $xml)
      {
         $tempContent = new-object System.IO.StringWriter
         $textwriter = new-object System.Xml.XmlTextWriter $tempContent
         $textWriter.WriteString($xml)
         $tempContent.ToString()
      }

      @"
      <struct>
        <member>
          <name>dateCreated</name>
          <value>
            <dateTime.iso8601>$(
                $this.dateCreated.ToString("yyyyMMddTHH:mm:ss"))</dateTime.iso8601>
          </value>
        </member>
        <member>
          <name>description</name>
          <value>
            <string>$(encode $this.description)</string>
          </value>
        </member>
        <member>
          <name>title</name>
          <value>
            <string>$(encode $this.title)</string>
          </value>
        </member>
        <member>
          <name>categories</name>
          <value>
            <array>
               <data>
              $(
                  foreach($category in $this.categories)
                  {
                     if($category -and $category.Trim()) { "<value>$category</value>" }
                  }
               )
               </data>
            </array>
          </value>
        </member>
        <member>
          <name>link</name>
          <value>
            <string>$(encode $this.link)</string>
          </value>
        </member>
        <member>
          <name>permalink</name>
          <value>
            <string>$(encode $this.permalink)</string>
          </value>
        </member>
        <member>
          <name>postid</name>
          <value>
            <string>$(encode $this.postid)</string>
          </value>
        </member>
      </struct>
"@
   }

-------------8<--------------------------------

   ## Finally output the object that represents the post
   $blogEntry
}

Then, our script to upload the post:

param(
    [string] $endPoint, [string] $blogid, [string] $username,
    [string] $password, $blogPost, [bool] $publish = $true
)

$postTemplate = @"
<methodCall>
  <methodName>metaWeblog.editPost</methodName>
    <params>
      <param>
        <value>$($blogPost.postId)</value>
      </param>
      <param>
        <value>$username</value>
      </param>
      <param>
        <value>$password</value>
      </param>
      <param>
        <value>
           $blogPost.ToString()
        </value>
      </param>
      <param>
         <value>
            <boolean>$(if($publish) { 1 } else { 0 })</boolean>
         </value>
      </param>
    </params>
</methodCall>
"@

(new-object System.Net.WebClient).UploadString($endPoint, $postTemplate)

And finally, the script to drive it all:

$endpoint = "Endpont Here"
$username = "Username Here"
$password = "Password Here"
$posts = .\get-posts.ps1 $endPoint $null $username $password 9999
foreach($post in $posts)
{
   if(($post.description -match "monad|msh") -and ($post.description -notmatch "powershell"))
   {
      $post.description += "<p>[<i>Edit: Monad has now been renamed to Windows PowerShell.  " + 
         "This script or discussion may require slight adjustments before it applies directly " + 
         "to newer builds.</i>]</p>"
      $post.postid
      .\set-blogpost.ps1 $endpoint $null $username $password $post $true
   }
}

What’s unreal is that all of this fancy code is only 89 lines of code when you exclude the XML templates and comments!

[D:\Lee\blogs\Monad]
PS:252 > ((gc get-posts.ps1,set-blogpost.ps1) -notmatch “#|<” | measure-object).Count
89