Dmitry Petrov Back

Part 2: Client logic

All posts from the series can be found here

There are a lot of different bits related to the client that were pretty fun to solve, especially in order to get git-like behavior, but nothing of that matters if we cannot communicate with the server, so let’s start with that.

XMLRPC

Livejournal uses XMLRPC as a protocol for communication. Documentation is old but serves it’s purpose. However, I had to check the last opensource version of the protocol implementation to make sense out of it for a back sync functionality. I’ll return to it later, but now let’s see how that can be done at all.

I looked for a library to use and initially my choice was s-xml-rpc. I don’t remember exactly why I decided to get rid of it. I think it was due to the fact that s-xml-rpc returns result in term of a hierarchy of objects and it proved to be difficult to maintain.

Instead of that, I’ve settled with rpc4cl which simply returns a nested list and that’s precisely what I need. Now that I’m writing this, it appears that author removed rpc4cl from Github which is very unforunate. To simplify the code I made a simple wrapper that takes adds host information to every call:

(defun rpc-call (method &rest method-parameters)
  (apply #'rpc4cl:rpc-call *service-endpoint*
         nil nil method method-parameters))

After that, all remote calls naturally fit into the code. Here is as example a sub to get a so-called challenge from Livejournal service:

(defun getchallenge ()
  (->
   (rpc-call "LJ.XMLRPC.getchallenge")
   (getf :challenge)))

Please note -> from cl-arrows here. I seriously cannot imagine my lisp coding with it, because using the arrow prevents all sorts of deep nesting and keeps the code clear. Since it’s not always easy to remember in which place the previous result goes by default in the latest code I always use -<> instead which allows putting a placeholder <> that will be replaced with the result. This way makes argument placing more explicit. As an example getchallenge can be transformed to this:

(defun getchallenge ()
  (-<> "LJ.XMLRPC.getchallenge"
   (rpc-call <>)
   (getf <> :challenge)))

One of the distinctive features of Common Lisp is its live editing process where one can start interpreter, load necessary code there and do all the changes right in it compiling changed functions if necessary without a need to compile and start the program all over again.

In case of the API, it’s particularly useful. I defined three dynamic variables - *service-login*, *service-password*, *service-endpoint*. I set them once the repl starts and after that I can do any experimentation with the api with help of all the power the language provides. And after I wrapped all service endpoints as functions I literally could do all blog manipulations without leaving the editor. It proved to be so stable that I kept the running lisp process for months without any need for a restart.

The only improvement I made recently was to add unit tests into the project development workflow and it works wonderfully with repl-driven development. Why is it cool? With a usual way of developing one of the hardest bits is to recreate an environment where tests happen, and in cases where there is no obvious way to implement something, changing implementation can have a lot of impact on how tests are written. With Common Lisp, I can experiment with code freely until I get something working without overhead related to environment and create supporting architecture only after that and I can still run all the tests right there! Change a function and recompile only one specific test till everything works and then compile the whole suite to see if everything’s in place.

It’s so fascinating that I can safely say that it’s one of the features that really make me stick to the language, I enjoy every second of it.

Storage

I wrote the client primarily for myself and I didn’t want to invest in security more than a secure password storage that was achieved but external tools. I wanted some storage, however.

First things first I decided to have a class that represents a posts database, that looks like this:

(defclass <db> ()
  ((posts :initarg :posts :accessor posts)
   (version :initarg :version :reader version)
   (login :initarg :login :reader login)
   (raw-text :initarg :raw-text :reader raw-text)
   (service :initarg :service :reader service)
   (service-endpoint :initarg :service-endpoint :reader service-endpoint)
   (fetch-store :reader fetch-store)
   ))

That’s the most recent version, of course, the most minimal version contained only posts slot and thanks to the lisp interactivity more slots could be added by class recompilation and all the live instances got new slots automatically.

Anyway, as a next step, I wanted to serialize the database and it’s contents and store it into a file. This is very Common Lisp generics step in. They are completely orthogonal to classes and make it super simple to do all sorts of recursive definitions for particular functionality. In this case, I defined a generic to-list and wrote it’s implementation for the database:

(defmethod to-list ((db <db>))
  `(:login ,(login db)
    :version 2
    :service ,(service db)
    :raw-text ,(raw-text db)
    :service-endpoint ,(service-endpoint db)
    :posts ,(mapcar #'to-list (posts db))))

To make this method work completely I had to only implement this method for post <post> class, which I did.

(defmethod to-list ((post <post>))
  (list
   :itemid (itemid post)
   :anum (anum post)
   :ditemid (ditemid post)
   :url (url post)
   :created-at (created-at post)
   :updated-at (updated-at post)
   :ignored-at (ignored-at post)
   :server-changed-at (server-changed-at post)
   :synced-from-fetch (synced-from-fetch post)
   :log-ts (log-ts post)
   :filename (filename post)
   :journal (journal post)
   ))

Super simple, after that saving database to a file became trivial:

(defun save-posts ()
  (with-open-file (out *posts-file*
                       :direction :output
                       :if-exists :supersede)
    (with-standard-io-syntax
      (pprint (to-list *posts*) out))))

*posts* here is another global var that holds a reference to the open database and *posts-file* is a relative path pointing where the posts file should live.

What happens in the sub is that we pretty print s-expression that happens to be a result of to-list method call into a stream that happens to be an open file. with-standard-io-syntax macro ensures that all the output related dynamic variables are reset to their default state for the time of printing.

After this was done using auxiliary methods made it trivial to save changes on all meaningful actions. I had generics for publishing, updating and deleting the post and all save logic is as simple as:

(defmethod publish-post :after ((db <db>) (post-file <post-file>))
  (save-posts))

(defmethod delete-post :after ((db <db>) (post <post>))
  (save-posts))

(defmethod update-post :after ((db <db>) (post <post>))
  (save-posts))

An obvious downside of this approach is that such an implementation is tied to the class and not to object instance and that can lead into all sorts of troubles characteristic to global state.

Now that the file is saved we need to be able to restore it. That’s also easy:

(defun restore-posts ()
  (read-parse-file *posts-file*
                   #'(lambda (l)
                       (setf *posts* (create-db-from-list l))
                       )))

As you see there are no safety checks there, which might be needed were I to worry about it. I chose for simplicity in this case since for the time being I’m the only user of the program.

create-db-from-list is an ordinary function in this case, not generic, and there is a create-post-from-list for posts. Now, during the development, the structure of the database and posts inevitably changes and we need to handle that somehow and that’s why database has version slot.

What I decided to do was to do database migrations right during the read phase. In order to do that I find a unique feature for the next version of config, do migration and run the same function recursively and by this way eventually get a most up-to-date structure that will be nicely serialized on next save.

(defun create-db-from-list (l)
  (cond
    ((null (find :version l)) (create-db-from-list (migrate-db-v0-v1 l)))
    ((null (find :service l)) (create-db-from-list (migrate-db-v1-v2 l)))
    (t (create-db-from-list-finally l))))

Publishing

Now that I had a database I wanted to fill it somehow with posts! Each post is represented by a markdown file with a header that contains custom fields. For example:

title: This is a cool post
tags: this, is
privacy: friends

# Intro

This post will contain

* A header
* A list
* A couple of paragraphs

Please note that till this moment we didn’t reach any cli interface and git-like state management, hence I wanted to implement a simple function that would take a file, parse it, convert markdown into html and publish it as a post.

Task itself is also recursive meaning that after we parse a file and get a list of fields plus a text we need to parse the result again to map all the fields to a representation Livejournal understands.

The first part you can check out an implementation of parse-post-file function. What it does is it reads header line by line and treats all the first field of the form field: value as individual fields and then all the rest as a markdown body of that post which it converts to the html.

After this step is done an instance of <post-file> is initialized and we can already do all sorts of things with it, not just publishing. For example, we can check if it’s a draft. But let’s publish.

For that we need to convert this object to the format lj.postevent understands. For this to-xmlrpc-struct generic is defined, which is super handy since I can do an implementation for both <post> and <post-file> and use whatever is at hand. For example for a new post it’s always <post-file> and for updates it’s <post>.

For <post-file> I decided to go with a layered approach where I have a basic object and then enrich it with all sorts of additional helpers that contain logic for particular fields. Here is to-event-list which is used by to-xmlrpc-struct:

(defmethod to-event-list ((post <post-file>) &optional (transform #'identity))
  (let ((l (list
            :event (if *raw-text* (body-raw post) (body post))
            :subject (title post)
            )))
    (-<> l
         (add-props (fields post))
         (add-privacy-fields (privacy post))
         (add-usejournal (journal post) <>)
         (add-date)
         (funcall transform <>))))

Every add-* function should return a new object that is potentially the same as l but can be modified version of it. Here is add-usejournal which is used whenever I want to post to a different journal than my own:

(defun add-usejournal (journal plist)
  (if (not (null journal))
      (concatenate 'list
                   plist
                   (list :usejournal journal))
      plist))

This conditional is not that elegant by itself, but the general pattern proved to be very useful and I used it in many different places.

Once I got an event in Livejournal view of it the publish function itself becomes really simple:

(defmethod publish-post ((db <db>) (post-file <post-file>))
  (set-credentials db)
  (let ((*raw-text* (raw-text db)))
    (let ((post (create-new-post post-file)))
      (push post (posts db)))))

And if you remember for the previous parts database will be saved to file whenever this method is called. And now, if we call this method from pre-commit hook we will get all changes saved to disk and will be able to add them to the index and be included in the commit. set-credentials here is to ensure that we have all api variables set before we make a call. The most interesting part of it is get-password that not just retrieves the password, but it also requests and saves it in case of absence.

And this is it about general client framework. With a list of <post> objects in the database we can do all sorts of calculations and answer to all sorts of interesting questions especially because there is a direct connection with the corresponding file on disk that can be converted to <post-file> at will.

How can I get the last published post? Easy:

(defun get-last-published-post (db)
  (car (sort (posts db) #'> :key #'created-at)))

How can I know the title of particular <post>? Super easy:

(defmethod title ((post <post>))
  (title (read-from-file (filename post))))

And that’s just two examples, it’s always possible to start the repl, load a posts database from file manually and start playing with contents.

To make repl experience even more pleasant Common Lisp provides print-object generic that is responsible for the text representation of the object. Hence we can print any information we like from the object instance.

(defmethod print-object ((post <post>) stream)
  (format stream "<post filename:~a url:~a>~%" (filename post) (url post)))

The last bit before implementing cli interface is the state management and it can also be easily implemented using the same operations on <post> and <post-file> objects.

What do I mean by state management? Looking at the blog folder we should be able to understand which posts are new, updated deleted or are drafts and they all are defined in different terms:

What they all share is that they all have a function that is able to generate a list of items of a specific kind and a set of messages to be printed depending on a number of items got in the previous step. Of course, the line can be printed differently from case to case.

For example, we can have the following output:

There a drafts file

    2018-06-04-books29.md

There is a new file to publish

    2018-06-06-test.md

There are 2 modified files to update

    2009-10-12-no-title.md
    2013-12-31-eshe-odin.md

There is a deleted file to unpublish

    2011-01-23-no-title.md

Let’s take drafts as an example. Here is a function to retrieve a list:

(defun get-draft-files ()
  (->> (get-markdown-files)
       (remove-if #'(lambda (fname) (get-by-fname *posts* fname)))
       (mapcar #'read-from-file)
       (remove-if-not #'draft)
       (mapcar #'filename)
       ))

cl-arrows rocks again there. What happens is that we get a list of all markdown files in the folder, remove any files that exist in the database, parse the reminder, kick out all files that do not contain the mentioned field and retrieve filenames back from filename slot in resulting <post-file> objects. Not that performant, I know, but it doesn’t really matter on this scale and my main aim was correctness and readability, not speed.

Once we have a list, we need to have a pretty printer for it and it should be similar for all the cases. At this point in time, I decided to test my defmacro-fu and came up with a macro, that looks like this:

(with-files draft (get-draft-files)
  "There are ~a drafts files~%"
  "There a drafts file~%"
  "No drafts found~%")

What this macro does is it generates another function named using the second argument (it’ll be with-draft-files in this case) and that function will print one of the strings depending on the number of items from the second argument ((get-draft-files) in this case) and print a list of items in a specified manner. The resulting print-status function looks very clean after all these manipulations:

(defun print-status ()
  (flet ((print-names (items)
           (format t "~%~{    ~a~^~%~}~%~%" (mapcar #'filename items)))
         (print-string-names (items)
           (format t "~%~{    ~a~^~%~}~%~%" items))
         )
    (with-draft-files #'print-string-names)
    (with-new-files #'print-names)
    (with-modified-files #'print-names)
    (with-deleted-files #'print-names)
    (with-fetched-files #'print-string-names)))

I use one or another printing function depending on what is produced but the with-* function, it can be a plain list of filenames or a list of <post> instances or anything else and I don’t need to worry about it at the generation step and have all the flexibility to do it at printing step, view layer is separate!

Speaking of modified posts. Whenever I publish a post I run get-universal-time and record it along the filename. Then it takes a super simple predicate to check whether a particular post is modified:

(defmethod modified-p ((post <post>))
  (let ((fname (filename post)))
    (and
     (probe-file fname)
     (< (or (ignored-at post) (updated-at post) (created-at post))
        (file-write-date fname)))))

You see file-write-date function there. This logic works really well if no one touches files, however, whenever files get moved or repo gets cloned to another location, the timestamp from it gets distorted and all files are marked as modified. This is precisely the reason for ignored-at field there. Whenever I want to mark all the files as uptodate I run this function:

(defun ignore-all ()
  (let ((ts (get-universal-time)))
    (loop for post in (posts *posts*) do
          (setf (ignored-at post) ts))
    (save-posts)))

This was one of the first times I used loop macro and I really fell in love with it some time after that.

Markdown

Last fun bit from the basic implementation is about markdown formatting. I decided to use it from the very beginning simply because it’s an order of magnitude better to write than html and still allows to have markup in place comparing to real plain text.

And of course, both vim and emacs have excellent markdown modes and make it a real pleasure to write.

What could be interesting about writing markdown? Well, to extend it!

I had two extensions planned. First of all, I didn’t want to use real urls for links between the post. If I did, I would be tied to one particular web service and migration to another one would be error-prone. If all links in the source code point to other markdown files, that means that whenever I decided to republish all posts somewhere else, I could have all the links resolved.

For markdown rendering I used cl-markdown library and I’m not sure if it was the best choice possible. Why? Because it’s source code is close to impenetrable to me and documentation is almost non-existent. Maybe I’m not a good reader, but I took me several evenings to get my head around library internals and to understand ways to extend it.

For links generation I kept the assumption that I only have one database open at a time and it live in *posts* variable. cl-markdown unfortunately doesn’t export any methods it uses for html generation and I had to get into library package to extend it the way I want.

Inline elements are rendered using render-span-to-html generic function and Common Lisp rocks again there because it allows adding an auxiliary method for it and target a specific set of arguments, that allows to keep method body clean from unnecessary modifications:

(defmethod render-span-to-html :before
    ((code (eql 'inline-link)) body encoding-method)
  (let ((record (cl-journal.db:get-by-fname cl-journal::*posts* (cadr body))))
    (if record
        (setf (cadr body) (cl-journal.db:url record)))))

What happens is that for links the first element in the body element holds a link that will be put in href, and in before element we can do a lookup in the database and replace a link if necessary. I think it’s an amazing level of flexibility with this amount of effort.

Another extension that I wanted to have was about links to the other blogs. Livejournal has some custom tags for different purposes and in this case it uses <lj user="can3p">, where user attribute holds the name of a blog. cl-markdown allows extensions and in order to make {lj-user can3p} render to the desired tag it’s possible to use an official extension logic:

(defextension (lj-user :arguments ((name :required)) :insertp t)
  (setf name (ensure-string name))
  (let ((safe-name (html-safe-name name)))
    (ecase phase
      (:parse)
      (:render
       (format nil "<lj user='~a'>" safe-name)))))

This looks and is indeed easy but it took me quite some time to figure all the details out.

This part concludes main highlights of the client. Of course, there is more, you can check out the amount of commands supported at the moment. Some of them like new or last are there just for convenience to make it easier to start writing a new post or quickly open the last one in case of typos, some like status or url are informative in a sense that they help to query the database and give out information, last group like ignore-all is used to fix the imperfections in file state handling logic.

There is one more group I haven’t talked about, which is fetch, merge and remerge and this is what I’m going to talk about next.