Physicist, Emacser, Digitales Spielkind

Note-taking on the go: Capturing messages and images sent via Jami in Org mode
Published on Apr 16, 2023.

I keep most of my life in plain text files and manage them using Org mode. That’s how I keep track of things to do, ideas to flesh out or random information that might come in useful later. However, when ideas strike, I don’t always have a keyboard and Emacs ready; often that is on the bus going to or from work. Or I find myself wanting to document something interesting by taking a picture. But what use is a picture without additional notes and context, ready to be refiled into my second (digital) brain? I have not found any app that fits my workflow and does not disrupt my train of thoughts. The most natural thing would be a message to myself – or more specifically, Emacs…

So I wrote org-jami-bot which builds upon jami-bot and extends it with Org mode capture functionality for text messages and images. It allows me to schedule agenda items at specific dates, compose multi-message captures and even capture URLs including meta-data using org-capture-ref – all by sending a message via the GNU Jami messenger.

jami-bot was covered in detail in an earlier blog post of this series and reference/URL capture will be the focus of the last part. This blog post will take up a more user-centric perspective on capturing notes with Jami.

So the natural start is a demo!

Demo (with cats!)


Figure 1: Animation of a multi-message capture from the Android Jami app.

The animation shows how one initiates a multi-message capture from within the Jami messenger app – that is, a capture process that consists of several messages and can include even images and other files. The process is started by sending the command !start followed by the title of the capture. Every command consists of an exclamation mark and a single word, for example: !help which shows the available commands or !today which captures the remainder of the message as a todo entry scheduled today. Everything else is treated as a normal message (and captured verbatim).

On the other side of the chat is jami-bot running within Emacs on my local computer. It responds to each of my messages to indicate how it was processed.

Once the multi-message capture session is started, every following message is simply added. This includes images which will be downloaded and stored locally on my computer. A reference in the form of a link will be included in the notes.

Putting down the phone and opening the computer again, I will see something like this on the screen:


Figure 2: Screenshot of the Emacs instance running jami-bot: to the left the capture and to the right the screenshot of the conversation (also sent via Jami).

On the left is the capture buffer which includes the individual messages and the image I sent shown inline. Additional meta information like the capture date is also included. Files sent separately as a single message, such as the screenshot in the next entry, are captured as links to the locally downloaded file and tagged as FILE. In principle, further automatic processing (e.g. OCR) could easily be integrated. In clear text, the first example capture looks like this:

* Demonstration of a multi-message capture :)
:CREATED: [2023-04-10 Mon 18:26]
This is the first line to be added.
But there can more!
#+ATTR_ORG: :width 400
Like cute cat pictures 😻
That's it!

Any received file will also be added to the variable org-stored-links and can then be easily inserted as link in any Org mode document using C-c C-l. For me, this has become the easiest and quickest way to transfer specific files from my mobile phone to my computer and into my notes.

Advantages and disadvantages of using Jami and jami-bot

Jami is a distributed and private messenger that is part of the GNU project and mainly developed by Savoir-faire Linux. Private in this context does not only refer to the fact that messages, calls and video chats are encrypted, but that you need to provide essentially no personal information to create a Jami account. Distributed means that you can have encrypted (group) chats without requiring any server to exchange them through – which even works between devices on the same local network while the internet is down.

Jami is easy to deploy on most OS and is available for different mobile devices as well. That coupled with that fact that it was rather straightforward to interface from Emacs made it a ideal candidate for this experiment.

However, Jami has some rougher edges from a user’s perspective (that is to say, my personal one). While the mobile Android client has improved significantly over the past years, it still might quietly fail to sync up with other clients. In those cases, only a restart of the App seems to help reliably. Other quirks can be slightly annoying at times: pasting from the clipboard, for example, wipes the current message draft on my Android client – and I tend to insert links as the last step of composing a message.

Similarly, also the desktop daemon, jamid, which runs in the background while jami-bot interacts with it, sometimes needs a friendly killall jamid followed by M-x jami-bot-register to re-initiate the service. In particular, network state changes, i.e. a temporary loss of connectivity seems to cause a drop from the Jami network which Jami does not recover quickly from.

One more thing to be aware of is that jami-bot only reacts to messages being received. So if the daemon (and/or the GUI app) is already running before jami-bot is registered and started, some messages might slip by unnoticed. Should you not yet have received them yet though, for example because the daemon lost the connection, you can simply follow the killall-and-register procedure outlined above and you will capture any missed messages.

Personally, I can live with these compromises.

One last thing to consider is security: I am not aware of any recent security audit of Jami. Either way, bugs affecting the security of the messenger likely exist. Personally, I assume that by disabling unneeded features such as phone and video calls on my account, disallowing connections with unknown accounts and limiting my accounts exposure will keep it sufficiently secure for me. Timely updates are a given of course. (org-)jami-bot should only marginally increase the attack surface as long as you use it with trusted devices and accounts and do not extend it with functions that directly execute parts of the message received as code. In case you discover any potential security risks with the code I provide or the way I interface with Jami, please let me know!

In any case, you should make your own threat model for your use case and situation.

That said, let’s look into setting things up!


We will need to have the Jami daemon, jamid, installed on the local system. On Debian, this can be done by simply running sudo apt install jami which will also install the GUI application. The latter is not strictly necessary but can be more comfortable to use during the account setup. The version installed through apt, however, is likely older than what is provided on the official Jami download pages – consider updating should you run into any connectivity issues later.

Jami is controlled by jami-bot via a protocol called D-Bus. If you are using a Linux-based system such as Ubuntu, you are almost certainly already running D-Bus and an Emacs with built-in support. If you are using Mac OSX, you need to install D-Bus first (as far as I know). Even MS Windows can run D-Bus. If you succeed with any of the latter options, please let me know – however, this is somewhat outside the scope of this post.

Then you will need to create a Jami account. The easiest is to make a completely new one only for jami-bot, even if you already have a Jami account. Simply use the GUI or, for more advanced users and/or headless machines, follow the steps outlined in the first post of this series. You also need to add any user that should be able to interact with jami-bot as a contact and have the request be accepted on the other side – only then can you start exchanging messages.

By default, jami-bot will react to any message sent to any local Jami account but will ignore message sent from local accounts (to avoid feedback loops). In case you have several local accounts and would like to limit jami-bot to only one of them, you can configure the variable jami-bot-account-user-names.

jami-bot and org-jami-bot

You will also need to install the jami-bot and org-jami-bot packages in Emacs. These will eventually be made available via e.g. MELPA but currently, you need to install from source repository linked above. Once that is done, simply require the org-jami-bot package to load them:

(require 'org-jami-bot)

In order to capture messages automatically and without user interaction, we need to set up an appropriate capture template. Let us start by setting an associated key:

(setq org-jami-bot-capture-key "J")

Just make sure that this does not conflict with any other already defined template in org-capture-templates.

If you just want to get started right way with the default setup for org-jami-bot, simply run


and skip ahead to the next section! If you would like to understand the configuration a little bit better or make adjustments, read on!

Setup explained

For the actual template, use initial content (%i), define the key via the above variable, and set the property :immediate-finish to file the capture away directly. In the code below, you might want to replace org-default-notes-file with another location:

(if (assoc org-jami-bot-capture-key org-capture-templates)
    (message "Capture template referred to by \"%s\" key already defined!"
  (add-to-list 'org-capture-templates
             `(,org-jami-bot-capture-key "Jami message" entry (file org-default-notes-file)
               "%i" :immediate-finish t)))

Extending jami-bot commands for capture

Anytime you send a Jami message that starts with an exclamation mark, jami-bot will interpret this as a command that will trigger a special action. However, jami-bot comes only with a rudimentary set of commands. These are extended via org-jami-bot and need to be registered so that jami-bot knows about them:

(setq jami-bot-command-function-alist (append jami-bot-command-function-alist
  '(("!today" . org-jami-bot--command-function-today)
    ("!schedule" . org-jami-bot--command-function-schedule)
    ("!start" . org-jami-bot--command-function-start)
    ("!done" . org-jami-bot--command-function-done))))

This maps the command strings to the functions that handle them. The latter will be explained in more detail in the next section!

As this list of commands is easily forgotten while on the road, you can always send the command !help via Jami to receive a summary of all known commands and their docstrings as reply. Of course, you can easily add additional mappings to the list above. Just be sure that you do not overwrite the default commands already listed in jami-bot-command-function-alist, or you would lose e.g. the !help command.

Finally, we also want non-command messages captured, whether it is a plain text message or a file being sent. This is accomplished by adding corresponding hooks that will be run when jami-bot processes such messages:

(add-hook 'jami-bot-text-message-functions 'org-jami-bot--capture-plain-messsage)
(add-hook 'jami-bot-data-transfer-functions 'org-jami-bot--capture-file)

While we are at it, you might want to adjust the directory to which files are being downloaded to from its default value:

(setq jami-bot-download-path "~/jami/")

Finally, we need to register jami-bot so it listens to incoming messages:


This is all the setup we need! Now it is time to fire up Jami on your phone or any other device and capture messages!

First steps

Once you have jami-bot and org-jami-bot configured, check that the account you want to send captures to is shown as present in Jami (indicated by a green dot in the profile). Send a simple command such as !help or !ping first. On the computer running jami-bot, you should see a message appear in the minibuffer indicating that the message was received. Shortly after, you should get a response via Jami.

After that, try a capture: simply send a text message (without starting it with an exclamation mark). You should see the response “captured” after only a moment. The message should be filed at the location you specified in your capture template (org-default-notes-file by default).

Try sending an image or starting a multi-message capture (by sending !start) next. If all works as intended, you might want to adjust or extend the format of the capture – so let us look into the code handling the captures!

Code explained: org-jami-bot capture functions

This section is mostly for the curious or those that would like to extend org-jami-bot to scratch their own itch. If you want to go even deeper, a previous post explained jami-bot which might be useful in case you want to explore more of Jami’s functionality.

Note: the code discussed below is what I originally wrote – it probably has evolved since and the newest version will be hosted at this repository: https://gitlab.com/hperrey/org-jami-bot

One central function is the capture processor for any plain text message that is not a command:

 1: (defun org-jami-bot--capture-plain-messsage (account conversation msg)
 2:   "Capture body in MSG and replies to original message.
 4: CONVERSATION and ACCOUNT specify the corresponding ids that the
 5: message belongs to."
 6:   (let* ((buf (format "*jami-capture-%s-%s*" account conversation))
 7:          (continue (get-buffer buf))
 8:          (body (cadr (assoc-string "body" msg)))
 9:          (lines (string-lines body))
10:          ;; use inactive timestamps
11:          (timefmt (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")))
12:     (with-current-buffer (get-buffer-create buf)
13:       (insert (if continue
14:                   ;; multi message capture
15:                   (concat body "\n")
16:                 ;; single message capture
17:                 (format "* %s\n:PROPERTIES:\n:CREATED: %s\n:END:\n%s"
18:                         (car lines) (format-time-string timefmt) (string-join (cdr lines) "\n"))))
19:       (jami-bot-reply-to-message
20:        account
21:        conversation
22:        msg
23:        (if continue
24:            "message added. Finish capture with \"!done\""
25:          (if (and (org-capture-string
26:                    (buffer-string)
27:                    org-jami-bot-capture-key)
28:                   (kill-buffer buf))
29:              "captured!"
30:            "error during org-capture :("))))))

It consists of three parts: starting on line 6 it sets up helper variables, from line 12 onward it sets up the text to be inserted into a capture buffer and after line 19 constructs the reply. The capture template is chosen according to the value of org-jami-bot-capture-key

(defvar org-jami-bot-capture-key "J"
  "Key for the org-capture template to call for Jami messages")

However, the actual capture (line 25) is only performed if the capture buffer did not already exist – if it did, the message has to be part of a multi-message capture process. In that case, the capture buffer will remain until killed by org-jami-bot--command-function-done (triggered by the !done command). This function, and the one to initiate such a multi-message capture and sets up the capture buffer, are defined below:

(defun org-jami-bot--command-function-start (account conversation msg)
  "Initiate a multi-message capture.

It starts with body in MSG by creating a capture buffer for

Further plain text messages processed by
`org-jami-bot--capture-plain-messsage' or files received by
`org-jami-bot--capture-file' will be added to this capture
buffer. The actual capture needs to happen through a separate
function, e.g. `org-jami-bot--command-function-done'. Return a
reply string informing correspondent about how to finish capture
by sending '!done'."
  (let* ((buf (format "*jami-capture-%s-%s*" account conversation))
         (body (cadr (assoc-string "body" msg)))
         (lines (string-lines body))
         ;; use inactive timestamps
         (timefmt (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")))
    (with-current-buffer (get-buffer-create buf)
      (insert (if (string-empty-p buf)
                  (format "* Multi-message note capture %s\n:PROPERTIES:\n:CREATED: %s\n:END:\n"
                          (format-time-string timefmt) (format-time-string timefmt))
                (format "* %s\n:PROPERTIES:\n:CREATED: %s\n:END:\n%s"
                        (car lines) (format-time-string timefmt) (string-join (cdr lines) "\n"))))
      "Multi-message capture started. Finish capture with \"!done\"")))

(defun org-jami-bot--command-function-done (account conversation msg)
  "Finish multi-message capture and return a confirmation string.

        Requires a capture buffer set up for CONVERSATION and
        ACCOUNT, for example through
  (let* ((buf (format "*jami-capture-%s-%s*" account conversation))
         (continue (get-buffer buf))
         (body (cadr (assoc-string "body" msg))))
    (if continue
        (with-current-buffer (get-buffer-create buf)
          (if (and (org-capture-string
                   (kill-buffer buf))
              "capture finished!"
            "error during org-capture :("))
      "No capture to finish. Start multi-message capture with \"!start\"")))

Note that the command functions do not need to actually send the reply message: this is done by the jami-bot function that will perform the message processing and calls above functions.

Very similarly, we can also capture file transfers. The actual download is handled by jami-bot already, so we only need to capture a link and add that to org-stored-links:

(defun org-jami-bot--capture-file (account conversation msg dlname)
  "Capture downloaded file and reply to original message.

DLNAME specifies local file name downloaded from MSG in
  (let* ((buf (format "*jami-capture-%s-%s*" account conversation))
         (continue (get-buffer buf))
         (displayname (cadr (assoc-string "displayName" msg)))
         ;; use inactive timestamps
         (timefmt (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")))
    (with-current-buffer (get-buffer-create buf)
      (insert (if continue
                  ;; multi message capture
                   "#+ATTR_ORG: :width 400\n"
                   (org-link-make-string (file-relative-name dlname)) "\n")
                ;; single message capture
                (format "* FILE %s :FILE:\n:PROPERTIES:\n:CREATED: %s\n:END:\n\n#+ATTR_ORG: :width 400\n%s\n"
                        (org-link-make-string (file-relative-name dlname) displayname)
                        (format-time-string timefmt)
                        (org-link-make-string (file-relative-name dlname)))))
      ;; store link for easy linking
      (push (list dlname displayname) org-stored-links)
       (if continue
           "file added. Finish capture with \"!done\""
         (if (and (org-capture-string
                  (kill-buffer buf))
           "error during org-capture :("))))))

Special purpose commands

While the above command are rather generic, I also have written some that are more tailored to my workflow. Below I define a function that is mapped to the !today command and captures the messages as a todo entry that is scheduled for today.

(defun org-jami-bot--command-function-today (account conversation msg)
  "Capture body of message as todo entry scheduled today.

 Returns a reply string as confirmation. MSG is the full message
  (let* ((body (cadr (assoc-string "body" msg)))
         (lines (string-lines body))
         ;; use inactive timestamps
         (timefmt (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")))

          (if (org-capture-string
               (format "* TODO %s\nSCHEDULED: %s\n:PROPERTIES:\n:CREATED: %s\n:END:\n%s"
                       (car lines)
                       (format-time-string (car org-time-stamp-formats))
                       (format-time-string timefmt)
                       (string-join (cdr lines) "\n"))
           "captured and scheduled!"
        "error during org-capture :(")))

Sometimes, I remember something that I have to do tomorrow or some other day in the future. In that case, I can use the !schedule command which does some additional parsing on the received message: it takes the first string in a message immediately following command as a date e.g. “2023-03-19” or “monday” and schedules a entry consisting of the following lines on that particular date. The date string is parsed through org-read-date and supports the same syntax.

(defun org-jami-bot--command-function-schedule (account conversation msg)
  "Capture body as todo entry and schedule it on the date given after the command.

The entry will be scheduled according to the first line of the
MSG body immediately following the command string. The date will
be parsed through `org-read-date' and supports the same
string-to-date conversations. Returns a reply string as
confirmation. ACCOUNT and CONVERSATION are not used."
  (let* ((body (cadr (assoc-string "body" msg)))
         (lines (string-lines body))
         (swhen (org-read-date nil nil (car lines)))
         ;; inactive timestamp
         (timefmt (concat "[" (substring (cdr org-time-stamp-formats) 1 -1) "]")))
    (if (org-capture-string
         (format "* TODO %s\nSCHEDULED: %s\n:PROPERTIES:\n:CREATED: %s\n:END:\n%s"
                 (cadr lines)
                 (format-time-string timefmt)
                 (string-join (cdr lines) "\n"))
           (format "captured and scheduled on %s!" swhen)
        "error during org-capture :(")))

Where next?

Having a Jami bot running in Emacs has already helped me to dump notes, images and references into my second brain when my phone was the only digital device at hand.

I am also using syncthing and Orgzly on my mobile devices. However, sending a quick message !today buy oat milk or some such feels a lot more natural and quicker than first navigating to the right place, setting a date before even entering what I was thinking of.

And for such a simple todo entry this might be mostly a matter of taste. However, I will soon follow up with a post on how to use org-capture-ref to capture an URL – including bibliographic information in BibTeX format and tags. This has been a great way for me to reduce the number of open tabs on my phone and finally transfer those interesting links and articles into my notes without too much effort or yet another app.

Automatic archiving of web articles, text recognition in images or maybe even speech recognition from audio recordings – let me know what you come up with 😺