This is an article in a series of unknown length discussing a tool written in Lua to publish posts on a WordPress blog from a PC. The source code is available in a public fossil repository.
In this installment we will describe the wplist.lua
utility that makes it easy to capture lists of posts, categories, tags, media items, and similar things made available from the REST API.
Why?
The WordPress REST API provides a number of lists of entities that are part of each blog. These include the obvious list of all posts, as well as lists of categories, tags, users, comments, and more. In each case, the API provides details about each item, as well as query parameters to effectively search for a specific item in the larger list.
Implementing support for these lists will be useful if only as a quick way to itemize what appears on the blog. Our implementation will provide a terse list by default, or the complete table of details if asked.
Implementation
The actual dirty work is found in cli/wp.lua
in the function getList()
.
--- -- Retrieve a list from a WordPress blog found at args.baseurl, -- passing the OAuth token. function M.getList(list, noenvelope) local E = urlescape -- local alias for easy typing local q = { -- collect all query parameters in a list "context=edit", "http_envelope="..E(not noenvelope), "status="..E(args.status and args.status or "any") } -- add additional query parameters based on command line arguments if args.before then q[#q+1] = "before="..E(args.before) end if args.after then q[#q+1] = "after="..E(args.after) end if args.order then q[#q+1] = "order="..E(args.order) end if args.orderby then q[#q+1] = "order_by="..E(args.orderby) end if args.page then q[#q+1] = "page="..E(args.page) end if args.number then q[#q+1] = "number="..E(args.number) end if args.type then q[#q+1] = "type="..E(args.type) end if args.tag then q[#q+1] = "tag="..E(args.tag) end if args.category then q[#q+1] = "category="..E(args.category) end return M.REST(args.baseurl..'/'..list..'?'..table.concat(q,"&")) end
The command line arguments are passed in to the API endpoint (with suitable URL escaping just in case a category or tag name contains a space or other problematic character), using the table q
to assemble them and table.concat
to collect them all into a string separated by ampersand characters.
The new tool wplist.lua
has the usual collection of modules included, and declares its usage and command line arguments with pl.lapp
as usual.
local args = lapp [[ List information from a WordPress blog to a file or stdout. Part of the WP CLI Tools. https://curiouser.cheshireeng.com/applications/wp-cli-tools/ These options are related to the config file, with the ones marked * actually stored in the file. Either --blog or --site and --token must be available and consistent for posting to be allowed. --authenticate Do web-based authentication and write config --blog (default "") *The blog at which to post. --token (default "") *The OAuth token from the redirect URL. --expires (default "") *The OAuth token expiration date. --site (default "") *The WP Site ID for the token's blog. --tokenurl (default "") The full URL containing the token --showconfig Just display the config file --writeconfig Write the config file with the options General options: -v,--verbose Be more chatty about the process --keepraw (default "") Name a file to fill with raw logging --debug Don't use this. Options identifying what to list: --list (default "posts") List: posts, categories, tags, media, users, comments, stats. Some need --container to be useful. Options for the listing: -n,--number (default 20) Items per page -p,--page (default 1) Page to retrieve --before (default "") Posted before ISO8601 date --after (default "") Posted after ISO8601 date --order (default "DESC") Sort order: DESC or ASC --orderby (default "date") Column to sort by: date, modified, comment_count, ID. --type (default "post") Type: post, page, any. --tag (default "") Tag name to list --category (default "") Category name to list --status (default "") Status to list --out (default stdout) The file to write. --container Save the whole JSON container, not just a simple list. ]] -- erase some optional fields from the args table completely common.clearoptional(args, {'keepraw','before','after','order','orderby','type', 'tag','category','status'} )
We make a number of the options that take strings completely optional by letting lapp
see them has defaulting to zero-length strings, then removing them from the args
table after the command line is parsed. This makes code like we wrote in wp.getList()
safer, since it can just say if args.tag then ... end
rather than also needing to qualify it with #args.tag > 0
.
The --list
option takes a keyword like posts
, pages
, or anything else that will fit into the API’s URL at that position in the path. This version does not restrict the option to just the documented API endpoints, but it is possible that it should.
Similarly, no attempt is made to validate the query parameter values. --after
and --before
should be ISO8601 date strings. --type
comes from a controlled list of post types. And so forth.
After dealing with the stock options, config files, authentication setup, and so forth, the main body of the script is straightforward. We call the API, then dump a few fields from its response.
--- -- Actual main body of the script. -- local ok,res local list = args.list ok,res = wp.getList(args.list) if not ok then lapp.error(res) end if args.debug then print(pretty.write(ok)) end local body = ok.body or ok if body.error then lapp.error(body.error .. "n" .. body.message, true) end
We try to print an error if the API call failed. There may be (read: are) cases where failure gets past this point. Understanding and resolving them is left as an exercise. The otherwise undocumented --debug
option will help here by dumping the complete response packet if execution gets to that point.
if args.container then args.out:write(pretty.write(body)) args.out:close()
If asked for the list’s container, we simply output it as is. Use --out
to put it in a file if stdout
isn’t your taste. Otherwise, we behave more like DIR
or ls
and try to list just enough info about each item to be useful and not over-fill a single line.
else if body.found then args.out:write("Found "..body.found.." items.n") end local i0 = (args.page-1)*args.number for idx,item in ipairs(body[list]) do args.out:write(tostring(idx + i0), ": ", item.ID and ("(ID="..item.ID..") ") or "", item.id and ("(id="..item.id..") ") or "", (item.name or item.title or item.content or "?"):sub(1,50), 'n') end args.out:close() end
One thing to note is that the list API endpoints generally take parameters for a count of items per page and a page number to output. Many of them include a field found
that identifies how many total items match the list and query parameters. This output numbers the lines according to the page and item counts. Also note that most of the endpoints have silent limits on the maximum number of items that can be requested per call.
TODO: A future version could automate the capture of all items from a list by noticing that found
is greater than the number of items actually received in the array, and making additional calls with args.page
and args.number
set accordingly.
What Next?
The next big step is to think about integrating a fossil repository full of posts written in Markdown with a blog, using the WP CLI tools as the communications channel. Doing this successfully will likely drive changes in the tools so that PC side scripting can avoid reposting document that have not been edited on the PC. Allowing for edits made in the WP web UI would be bonus.
Another big step to consider is some amount of handling for media other than the post itself. WP supports pictures, audio, and video. A WP post can also have a “featured image” associated. Being able to handle posts with media and featured images in a useful way could also have value.
Repository and Checkins
All of the code supporting this tool is in a public fossil repository. See the discussion at the tools page for how to get started using this repository. This post documents work that was checked in as [681cb168dd]
.
The Rest of the Series
- Part 1 showed how to use XML-RPC and
wp.newPost
- Part 2 added file reading to make a minimal working utility.
- Part 3 added amenities and made the utility useful.
- We digressed to talk about OAuth and using it at WordPress.com.
- Part 4 switched to cURL, REST, and OAuth, but can’t yet post.
- Part 5 improved the token handling and user display and still can’t post.
- Part 6 implemented posting and added the ability to set title, tags, and categories from command line options.
- Part 7 added a new utility and created modules common to all our utilities.
- We digressed to build a tiny embedded web server to make authentication easier.
- Part 8 used the Spoon server to make authentication go smoothly.
(Written with StackEdit.)