Simplex chat notifications with Syndicated Actors

These days I'm using Simplex as my primary messenger. The Simplex project provides CLI and GUI clients, as well as apps for the rabble to smudge and paw. I have only used the CLI client and I don't plan on upgrading.

SimpleX

The CLI client only outputs text to a terminal (good) and typically new messages are only noticed by visually polling the client terminal. I want desktop notifications but I don't want to compromise the client codebase with "desktop" libraries. I will instead implement notifications using the Syndicate Actor Model and a constellation of reusable components.

Syndicate Actor Model

The componentisation technique I use here comes from the Genode OS framework, where terse and general components pass capabilities and structured data, which is a recent articulation of archaic UNIX philosophy.

This exercise will be Linux hosted so the `syndicate-server` will manage all the components for extracting messages and generating notifications. The server will start components and mediate their conversations.

syndicate-server

libnotify_actor

The Freedesktop.org group has a "Desktop Notifications Specification" which works on my machine. The Libnotify library makes notifications show up so this justifies a component that wraps Libnotify.

libnotify_actor.nim

Libnotify

Desktop Notifications Specification

The `libnotify_actor` listens for messages in the `<notify «TITLE» { body: … icon: … }>` format and forwards the notification to DBus.

The syndicate-server configuration for running libnotify_actor as a daemon:

Now notifications can be generated by sending messages to the `<notifications #!…>` dataspace:

mpv and the json_socket_translator

Audio notifications would be nice as well and this can be done with mpv. mpv exposes a JSON-IPC on a UNIX socket that we can interact with.

mpv

mpv

The syndicate server needs an assertion describing how to start mpv:

json_socket_translator

The `json_socket_translator` component translates JSON messages received on a UNIX socket into Syndicate messages and vice versa. When JSON is parsed on the UNIX socket the `json_socket_translator` will send a `<recv {…}>` message to a dataspace and when it observes a `<send {…}>` message at the dataspace it will forward the body to the socket. The actor is broadcasting and acting on broadcasts to the dataspace, so we can remotely attach to the socket via the dataspace to observe or inject messages, more on that later.

json_socket_translator.nim

As a side note, Syndicate uses the Preserves language for passing data. The JSON format is compatible with Preserves text parsing, so JSON messages could be parsed as Preserves and emitted as Preserves text. This is not what the `json_socket_translator` does. Preserves supports arbitrary key types for its dictionaries but typically uses the "symbol" type for keys. Here we are converting JSON dictionaries to use symbol keys and Preserves dictionaries to use string keys. For example, the JSON message `{ "recv": true }` is converted to the Preserves `{ recv: #t }`. The JSON values `true` and `false` would be parsed as Preserves symbol values but are converted to Preserves boolean values. This is to blur the distinction between data originating from a legacy socket and Syndicate native data.

https://preserves.dev/preserves.html#json-examples

The syndicate server starts the translator:

Now we can play a notification sound by sending `<play-file "…">` to the `<mpv #!…>` dataspace.

simplex-chat

The Simplex project publishes the `simplex-chat` application which exposes basic chat functionality through a terminal and also features a websocket to prop up the Simplex TypeScript SDK.

SimpleX terminal chat

TypeScript SDK

We will connect to the websocket to get data that can be processed into notifications, so a persistent instance of simplex-chat is required. The syndicate-server is configured with a definition of a simplex-chat daemon that listens on a websocket:

websocket_actor

The `simplex-chat` websocket sends and receives JSON-formatted messages. Connecting, sending and receiving, and parsing and encoding messages isn't specific to our end-goal so we can compartmentalize that to a dedicated component that will translate websocket JSON messages to and from a Syndicate dataspace. The websocket actor will translate JSON messages in the same manner as the `json_socket_translator`.

websocket_actor.nim

The component is started and configured by the Syndicate server:

We want to remotely interact with the websocket so a `<bind …>` assertion was used in the syndicate-server to mint a Sturdyref. We can recreate the Sturdyref on the command line using the `mintsturdyref` utility.

Sturdyref

mintsturdyref

With this we can connect to the websocket dataspace and interact using the `syndump` and `msg` utilities

syndump.nim

msg.nim

simplex_bot_actor

JSON data is translated to Syndicate messages but needs to be massaged into Syndicate assertions. The difference between assertions and messages is that assertions are persistent in a dataspace until they are retracted, and messages are asserted and immediately retracted. This means that messages can only be observed in the moment they are broadcast while assertions persist and can be cached.

The `simplex_bot_actor` observes chat messages at the websocket dataspace and asserts `<contact …>`, `<group …>`, and `<chat …>` records. For every "contact" and "group" at `simplex-chat` we assert a record that is updated from websocket messages. Each "contact" and "group" can have at most one `<chat …>` record asserted, which is the chat item most recently received from the websocket.

It's not actually enough to implement a bot, but it's a start.

simplex_bot_actor.nim

The syndicate-server configuration:

Final Composition

Now the interaction of the four daemons is composed at the syndicate-server using patterns and assertions:

Screenshot

simplex_notifications.png

Cost

I prefer to write programs that are not longer than 200 lines of code and the Syndicate DSL keeps programs terse and usually under this limit. The `simplex_bot_actor` will need to parse more messages from Simplex to be useful but shouldn't become unreasonably complicated.

Futher work

The websocket interface on the simplex-chat program is not general enough to reliably extract the information we need. What it provides is snapshots of application state that is traversable for extracting messages rather than a protocol for event-based interaction. Perhaps the Simplex C library is a better backend for the simplex_bot_actor.

Proxied content from gemini://spam.works/users/emery/simplex_notifications.gmi

Gemini request details:

Original URL
gemini://spam.works/users/emery/simplex_notifications.gmi
Status code
Success
Meta
text/gemini
Proxied by
kineto

Be advised that no attempt was made to verify the remote SSL certificate.