A Delta Chat push notification bot in 160 LOC
After finding the time to get my Home Assistant to monitor when our washing machine and dryer are done I faced the next challenge. Sending a push notification to me and my wife. Luckily we are already using Delta Chat and there is something that resembles a complete library to send messages to people and a flag to set for bots. So, how hard can it be?
I am a happy Pushover customer. Easily the best $5 I spent a few years ago to have a simple way to send a push notification to myself. Sure, I could have built on the whole "own app, own service" thing, but the VM running the push service alone would cost me more in a year than the one time payment. But to send a push to me and my wife I would either have to opt for the Teams plan or have her get her own account and maintain two integrations.
We are already happy Delta Chat users and while I dismissed the idea of integrating Delta Chat as a chat interface for LazerBunny, I was considering a push only service for some time for other projects. This all seemed like a very simple and easy thing to do, right?
Enter: E2E encryption
Our server enforces end to end encryption. So for the first our of my journey the error I saw the most was:
{"detail":"{'code': -1, 'message': 'Failed to create send jobs: e2e encryption unavailable Msg#14 - true'}"}
Well, now that is a fun one. I obviously set the bot flag. I also tried setting the unencrypted flag. Lesson learned, like the house in Vegas, the server always wins. Honestly, pretty reassuring, so I take it.
So a little bit of browsing around It seemed like I have to start a conversation with the bot first to establish the E2E encryption. Easy enough, except that I still believe the account email addresses are a bit too hard to find. The QR codes are handy, but come on. Just sending the bot a message is not sufficient, it has to fetch the message.
We are getting dangerously close from FastAPI accepting an HTTP request forwarding it to Delta to a full blown chat bot. This is also where I think I started committing crimes against FastAPI and spun up a ThreadPoolExecutor in the lifespan function to fetch messages sent to the bot.
dc_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="dc-events")
event_task = asyncio.create_task(listen_events(dc_executor, account))
This feels wrong. And I really want to give it another go and clean up a bit before I slap the MIT license on the code.
Creating a chat
Here is why things got a bit ugly. The documentation for the Python library is decent when you want to build a chat bot. But for anything more involved you will likely end up in the API reference.
One thing to keep in mind is that glossing over the installation instructions is not a good idea. You want the server and client package, even if you "only" write a client. Honestly it makes sense when you look at the architecture.
Fun fact: I managed to crash my thread pool so badly the server got stuck and everything was horrible. Cannot recommend to try this at home -Ctrl+C did not terminate the process and kill $pid did nothing. We had to resort to a baseball bat and a shady back alley, also known as passing -9 to the kill command.
Some of the examples and tutorials I found created a chat in a two steps. First use Account.get_contact_by_addr to get a contact. The API seems to have changed at some point to return a Contact instance instead of just the ID. With that the next call was Account.create_chat which according to the documentation requires an account, not a contact. Passing the wrong data ends in the above E2E error, not something a little bit more helpful.
The fix was as simple as calling Contact.create_chat().
contact = account.get_contact_by_addr(email)
chat = contact.create_chat()
chat.send_text(payload.message)
And that did the trick. One curl later I received a message on Delta.
Just use an LLM
When I was done I thought this might be a fun little evaluation benchmark for LLMs and harnesses. There are certainly not a million API to Delta Chat repositories around to be scraped and used in training.
When I use an LLM I prefer a locally hosted, open model. But to give it the best shot (based on general consensus, not necessarily reality) I opted to go with Claude, Qwen and Gemini. None of them got it right. They saw the exact same, wrong examples I used before. Gemini and Claude could at least be nudged in the right direction.
Gemini categorically refused to believe in end to end encryption. Not too surprising coming out of Googles labs I guess. Cannot make money with data you do not see.
Qwen just tried to gaslight me that the E2E error is a result of me using the wrong email address.
While this is not supposed to be an anti-AI bit, please let this be a cautious reminder to everyone lost in the AI cool-aid that there are moments where you are not solving the same, already solved problem for the 50th time that require you to actually understand how to read and write code and documentation.
This is also not an invitation to send me a slop repo proofing that AI can do it - we just established it can, with a little kick against the shin (or two).
Progress
This was an unexpected side quest and distracted me a bit but it was also a fun one. My editor is coming along step by step. I am taking my time as there are a few new things to learn, starting with the full feature set of Treesitter and how to best use it. I implemented a basic syntax highlighter that still needs a lot more time. It is parsing and highlighting all of the code, not just the visible piece and it re-parses the AST on every update. (Which roughly translates to the performance likely being horrible for large files.)
posted on Sept. 6, 2026, 7:48 p.m. in lazerbunny, python, self-hosting