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")))
complete)
(push (cons :username (or (cdr (assq :username blog))
(empty-string-is-nil (read-from-minibuffer "Username: "))
(error "Posting cancelled")))
complete)
(push (cons :password (or (cdr (assq :password blog))
(empty-string-is-nil (read-passwd "Password: "))
(error "Posting cancelled")))
complete)
(push (cons :blog-id (or (cdr (assq :blog-id blog)) (ref:blog-id)
(empty-string-is-nil (let ((userblogs (xml-rpc-method-call
(cdr (assq :xmlrpc complete))
'wp.getUsersBlogs
(cdr (assq :username complete))
(cdr (assq :password complete)))))
(cond
;; If there's no blogs, fail
((eq userblogs nil)
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
(t
(reduce
#'(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))))
userblogs
:initial-value (completing-read
"Blog Name: "
(mapcar #'(lambda (entry)
(cdr (assoc "blogName" entry)))
userblogs) nil t))))))
(error "Posting cancelled")))
complete)
(sort
complete
#'(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:
- there's no blog available at that URL, in which case we're done.
- 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.
- 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.