I'v recently found myself needing to use a task manager to make some bulk operations (to be more precise, I wanted to copy all of my tasks to a device with no built-in synchronization mechanism for those). I happen to make use of the great Remember The Milk Web application (I've previously worked with Emacs Planner, but I had to drop it in favor of a more mobile version of my life).
Enter Common Lisp. Perhaps the fastest way to do some scripting in my mind, but I've yet to make a library to use a Web-based API. Turned out it was quite clean, and in about 5 hours of work I had the whole API grokked, coded, tested and hosted in GoogleCode here. (I looked into cl.net, but found the google's approach to hosting much more featured for the fast start I was looking for. If this has enough momentum I can look into it again, with more time - the main advantage would be asdf-installability, I suppose.)
I already had experience making project "skeletons" (asdf project file, package declaration, etc.). So the hardest thing I had to do was look into the web API call mechanism and into the md5 encryption. If you don't need the detailed description, tha answers are, respectively, Drakma and Ironclad. ( I also use cl-json to parse RTM's responses into a lisp-friendly format.)
To call a Web-based method, we need to make an HTTP request, and be able to read the response. using drakma (asdf installable and loaded in the usual way), all I had to do was something like:
(http-request rtm-api-endpoint :method :post :parameters '(("param-name" . "param-value")))
With some parameter abstractions, RTM's was easily tamed. A call to one of their methods is now made with the following macro:
(defun rtm-api-call-method (method &optional key-value-pairs &key with-timeline with-authentication (format "json")) "Calls `METHOD'. - Optionally passes pairs of strings in `KEY-VALUE-PAIRS' in the form of (`PARAMETER' . `VALUE'). - Keyword `WITH-TIMELINE', if not null, allows the method to be called within the `*CURRENT-TIMELINE*'. - Keyword `WITH-AUTHENTICATION', if not null, allows the method call to be authenticated with a valid `*RTM-API-TOKEN*'. - Keyword `FORMAT' is one of \"json\" (the default value) or \"rest\", and specifies the server reply format." (declare (special *current-timeline*)) (let* ((parameters `(("api_key" . ,rtm-api-key) ("method" . ,method) ("format" . ,format) ,@(when with-timeline (with-timeline `(("timeline" . ,*current-timeline*)))) ,@(when with-authentication `(("auth_token" . ,*rtm-api-token*))) ,@key-value-pairs)) (api-sig (compute-rtm-api-sig parameters))) (multiple-value-bind (result) (http-request rtm-api-endpoint :method :post :parameters `( ,@parameters ("api_sig" . ,api-sig))) (let* ((response (json-bind (rsp) result rsp)) (stat (assoc :stat response))) (cond ((string= (cdr stat) "ok") (rest response)) ((string= (cdr stat) "fail") (let ((err-info (cdr (assoc :err response)))) (error "RTM error code ~a: ~a~%" (cdr (assoc :code err-info)) (cdr (assoc :msg err-info)))))))))) |
With this (rather big, for the sake of readability, or mayhap due to my lack of experience in mental pretty printing) function, I define a call to an RTM module simply by coding:
(defun rtm-api-tasks-complete (list-id taskseries-id task-id) (rtm-api-call-method "rtm.tasks.complete" `(("list_id" . ,list-id) ("taskseries_id" . ,taskseries-id) ("task_id" . ,task-id)) :with-authentication t :with-timeline t)) |
The only call in the first function definition that cannot be easily guessed how they work is the compute-rpm-api-sig-parameters. This is the function that performs the algorithm required by RTM to encode all parameters and guarantee authenticity in each request (along with a masked API key). Basically it does some mambo-jumbo concatenation with each parameter's name and value, and then performs an MD5 to get the equivalent hash code. Easy, right? But I'm a lazy programmer. That, and I truly believe there's no point in reinventing the wheel, so I looked for encryption functions in Lisp, and I found Ironclad, a collection of most of these functions in Lisp. So producing an md5 functions was as easy as the following:
(defun md5 (string) "MD5 uses ironclad to encode `STRING' into an hexadecimal digest string." (ironclad:byte-array-to-hex-string (ironclad:digest-sequence :md5 (ironclad:ascii-string-to-byte-array string)))) |
And that's about it. The rest was toying around, the API works perfectly (kudos to the RTM dev team!), and I was thrilled to have my tasks exported in a jiffy. If you want to find more info, feel free to snoop through the code (it's quite easy to grasp, I think, and relatively small), in the Google Code site: http://code.google.com/p/rtm-lisp-api/ . So now, what would you, gentle reader, do with this API? Drop me some comments with your ideas.
Until the next time!