Everybody Lies

I miss House. What could be more fun than watching Hugh Laurie verbally abuse people for an hour each week?

Anyway, we’ve gotten to the point where we want to start pushing data to the server. Which means we’re going to have to start pulling data from the server. Specifically, configuration data for the blog, because some of what we need to have on hand to create or otherwise manipulate posts isn’t always obvious or easily discoverable—so we need to try and figure it out for ourselves.

For this post, I’m actually going to start off with the test (which, incidentally, I left off of the last post because it was already ginormously long—but rest assured, if you check out the github repository, everything is being tested):

(ert-deftest ob-test-wp-params ()
  "Test getting the blog-id (and correct xmlrpc URL) via xmlrpc"
  (let* ((blog-passwd (read-passwd "Password for blog listing: "))
         (initial-blog-param `((:xmlrpc . "http://wordpress.com/xmlrpc.php")
                               (:username . "mdorman@ironicdesign.com")
                               (:password . ,blog-passwd)))
         (final-blog-param `((:blog-id . "46183217")
                             (:engine . "wp")
                             (:password . ,blog-passwd)
                             (:username . "mdorman@ironicdesign.com")
                             (:xmlrpc . "https://orgblogtest.wordpress.com/xmlrpc.php"))))
    (should (equal (org-blog-wp-params initial-blog-param) final-blog-param))))

I want to start with the test because I think it’s pretty illustrative of the divergence between what people may know about their blog, and what is actually needed. For instance, if you’re on a big hosting service, do you actually know the ID of your blog? Yet this is a necessary component for creating posts, so we have to be able to discover it. And I only stumbled across it by accident, but I assumed that the XML-RPC end-point for a blog on wordpress.com was on wordpress.com…but it’s not.

Anyway, you can see how given very partial and even somewhat erroneous information, we expect org-blog-wp-params (or any equivalent function for another engine) to give us the right stuff to make actual posts.

org-blog-wp-params starts off simple enough, and then suddenly goes non-linear. The reason is simple: the url, username and password are things that we must get from the user before we have a chance to do anything else.

(Actually, that’s not true—for WordPress blogs, at least, you could get the URL of the blog, look for the <link rel=”EditURI”> tag, follow that, parse the XML and get everything but the username and password; but since you need those anyway, it’s a lot less work this way. Perhaps some time in the future I’ll take advantage of the EditURI bit.)

So for each of those first three items, we look them up in an exiting blog structure, and if there’s nothing, we ask the user for the information, and if there’s still nothing, we bail out—there’s nothing more to do. And then we hit the blog-id, and things get interesting.

(defun org-blog-wp-params (blog)
  "Construct the basic paramlist for wordpress calls.

This starts with the information the user may have set for the
blog in their configuration, and then attempts to fill in any
holes so it can produce a list of necessearily generic
parameters.  `org-blog-wp-call' can then use the output of this
function to make other calls."
  (let ((complete (list (cons :engine "wp"))))
    (push (cons :xmlrpc (or (cdr (assq :xmlrpc blog))
                            (empty-string-is-nil (read-from-minibuffer "XML-RPC URL: "))
                            (error "Posting cancelled")))

    (push (cons :username (or (cdr (assq :username blog))
                              (empty-string-is-nil (read-from-minibuffer "Username: "))
                              (error "Posting cancelled")))
    (push (cons :password (or (cdr (assq :password blog))
                              (empty-string-is-nil (read-passwd "Password: "))
                              (error "Posting cancelled")))
    (push (cons :blog-id (or (cdr (assq :blog-id blog))                                          (blog-id)
                             (empty-string-is-nil (let ((userblogs (xml-rpc-method-call
                                                                    (cdr (assq :xmlrpc complete))
                                                                    (cdr (assq :username complete))
                                                                    (cdr (assq :password complete)))))
                                                     ;; If there's no blogs, fail
                                                     ((eq userblogs nil)
                                                     ;; If there's only one blog, use its blog-id (and xml-rpc) automatically
                                                     ((equal (length userblogs) 1)
                                                      (setcdr (assq :xmlrpc complete) (cdr (assoc "xmlrpc" (car userblogs))))
                                                      (cdr (assoc "blogid" (car userblogs))))
                                                     ;; FIXME: Prompt the user from the list of blogs (if there's more than 1
                                                     ;; Then shove the blog info into complete
                                                       #'(lambda (entry)
                                                           (when (string= (cdr (assoc "blogName" entry)))
                                                             (print (format "XMLRPC from server is %s" (cdr (assoc "xmlrpc" userblog))))
                                                             (setcdr (assq :xmlrpc complete) (cdr (assoc "xmlrpc" userblog)))
                                                             (cdr (assoc "blogid" userblog))))
                                                       :initial-value (completing-read
                                                                       "Blog Name: "
                                                                       (mapcar #'(lambda (entry)
                                                                                   (cdr (assoc "blogName" entry)))
                                                                               userblogs) nil t))))))
                             (error "Posting cancelled")))
     #'(lambda (a b)
         (string< (car a) (car b))))))

If the blog-id is in the blog structure we’ve been handed, we assume it’s correct and move on. If it’s not present, though, we assume that the user has no idea what it might be, so we do an XML-RPC call to get the list of blogs belonging to the user.

When looking at the output of that call, there’s three possible scenarios:

  1. there’s no blog available at that URL, in which case we’re done.
  2. there’s one blog available at that URL, in which case we grab it (and also make sure we pull out any XML-RPC endpoint information) and we’re done.
  3. there’s more than one blog available at that URL. In which case, we prompt the user for a selection from among the list of blogs. If they choose one, we grab the blog-id (and the XML-RPC endpoint) and we’re done.

If they don’t opt for one of the above, again, we cancel. Otherwise, we sort the alist we’ve created (to make it easy to test), and we’re done.

One thing that we don’t yet do that I would like to is tell the user what they should be putting in their config in order to avoid us having to do all this consultation—this would lower the barrier to entry to using org-blog even a little more; you just fire up org-blog-new for the first time, give it a name for the blog, then when you do org-blog-save, it would prompt you for the information and spit back a configuration block after it’s done saving.

But that’s the future.

Tomorrow, we’ll look at where this function fits into the bigger scheme of things.