Notifications from Python to Android

For the Vigilant Crypto Snatch we currently use Telegram to send notifications from the Python program to the phones (mostly Android, I guess). Each user has independently set up a bot with Telegram. This bot token is registered in the configuration file and the Python script can then use the bot to send messages. The users have to write to their bot using Telegram such that the bot knows about their personal account. Then the bot can send messages to that particular end user.

This works fine. It's just that I don't particularly like Telegram as a service. The messages are not end-to-end encrypted by default, everything resides on their servers and is stored there potentially indefinitely. Then it has attracted quite a strange flock of people, although that in itself doesn't need to mean much. When a platform is really good, various bad people will also use that. But what makes me wary is that the people who speak for Telegram seem to consistently overlook the blatant flaws in their security model. They just use big words like “freedom” or “independent”, but I find that irritating. The server code is closed source (fine), but messages exchanged via that server are unencrypted.

So I have already diverted most of my contacts to Threema, Signal or WhatsApp. One use case are these notifications. I would like to find out whether there are alternatives, and whether they are viable.

I need to find something that people can use and set up on their own. I can't set up a central cloud service because people would then have to route their crypto buying notifications through me. The whole point of the Python program is that people can host that completely independent of me, and I don't get any of the data. This builds trust which one would not have into a hosted solution. I cannot imagine that people would just paste their crypto exchange API keys into some website that I have built. So with that in mind, I need something that users can set up on their own. With Telegram we already have one option which seems popular in the crypto community, but perhaps I can find something else.

Finding alternative services

There is a Stack Overflow question and unsurprisingly a few of the answers mention Telegram. Others mention Slack. The interesting new candidates are Pushsafer and notify.run.

Pushsafer

This seems to be a project from a German web design company. It is a service where one has to pay, so this makes it appear trustworthy. Depending on the package that one books, they charge at most 0.001 EUR per API call. The largest package goes down to 0.0002 EUR per API call.

Given the amount of notifications that the program currently send, one might need to think about changing the logging level a bit. In a short session, one could already have 8 messages, resulting in a cost of 0.008 EUR.

These notifications are only fired when it actually tries to buy. And they also come when there are errors or uncaught notifications. Connection errors are likely not reported any more, I have fixed that. In total I would think that the overal cost should be below 1 EUR/month, which would be okay.

One uses it via a HTTPS POST request, so that is as easy to use as I already use with Telegram. There are native mobile apps which receive the notifications.

This seems like a sensible and potentially trustworthy service. There is a business model behind it, so it appears clear how it is financed.

Notify.run

With notify.run the service apparently doesn't cost anything. There is a centrally hosted instance, but one could also host this independently. The public instance doesn't cost anything, it is just rate limited (to 20 calls per minute). They advise to not send personal data over it:

This public instance is freely provided for your convenience, but does not have an SLA and should not be used for notification messages containing private data. Usage is tracked to prevent abuse, and rate limited to 20 API requests per minute.

The concept seems to be that there are “channels” in which one posts the notifications, and one can then see them. There is no access control, it is just the random name of the channel which will prevent everybody from seeing the content. Because the notifications contain sensitive data (balance on the exchange), but nothing personally identifiable, I think that it might just be okay.

They offer a simple HTTPS POST API, but one can also use a provided Python module. For the sake of fewer dependencies and since I don't need to create new channels within the program, I would just use the HTTPS API directly with a request library that I already depend on. This way I don't add new library dependencies to the code.

Signal

Apparently one can use Signal via the command line, see for instance this article. This article uses the third-party tool signal-cli, which is unoffical. And it provides a D-Bus interface to the official Java library. That official library is archived on GitHub, the copyright notice is only up to 2016. That makes it abandoned for 6 years, which is not a good sign.

The successor seems to be libsignal-client, their Rust library. That doesn't have bindings to Python. Also the license is AGPLv3, so I could not simply use that in my program and keep the MIT license.

Due to Signal's cryptographic architecture, one cannot just send POST requests somewhere. They would need to be encrypted locally and then uploaded. Therefore one needs to use their library to do it.

Preparing for implementation

Before I have looked into alternatives for the Telegram logging facility, the architecture looks like this:

There is a telegram module which contains the necessary code. It comprises of two parts. One is the actual logger (TelegramLogger), which fits into the Python logging facility. This receives log messages from the calls to the logger. This then formats the messages and emits them to the sender (TelegramSender). The sender chunks the message and sends them away in an asynchonous fasion. It therefore needs to handle a separate thread which takes care of this.

Now that I want to replace Telegram with something else, I will need open up a “seam”. It would be possible to just add different logging handlers which inherit from Pythons logging.Handler. But then I would have to re-write the asynchronous sending via HTTPS, which seems like something that I certainly want to reuse. Therefore I need to change my TelegramLogger to an ExternalLogger, which then delegates a message pool. This will contain most of the methods from the current TelegramSender. And only the send_message() method will be put into a new class. All the Telegram specific stuff will be concentrated there. I will need to come up with names as I go.

Splitting the sender

The current TelegramSender needs to be split. There is a message queue in it, and two functions which know how to communicate with Telegram. After the split and assigning new names, the structure looks like this:

The new TelegramSender doesn't have much code any more. It just sends messages. It has some special logic for the chat ID, but it doesn't handle the queue or the thread any more.

Now I have a minimal interface to add new senders. As a side effect I can now also test the message queue by inserting a mock sender. This allows me to test whether the message queue shuts down like expected, for instance.

Adding notify.run

Since notify.run is free to use, it will likely attract more users. I will implement that first. Since they are free, I can actually create one just for testing. On the website it says that I can send a message to there using the following command:

curl https://notify.run/xxx -d "Hello from notify.run"

The resulting messages are then displayed on the website. In the manual for curl one can see what -d means:

       -d, --data <data>
              (HTTP  MQTT)  Sends  the  specified data in a POST request to the HTTP server, in the same way that a
              browser does when a user has filled in an HTML form and presses the submit button.  This  will  cause
              curl  to  pass the data to the server using the content-type application/x-www-form-urlencoded.  Com
              pare to -F, --form.

So it is just a very plain POST request. That's easy! This is all it took:

class NotifyRunSender(Sender):
    def __init__(self, config: NotifyRunConfig):
        self.channel = config.channel

    def send_message(self, message: str) -> None:
        perform_post_request(f'https://notify.run/{self.channel}', message)

There is a bit more factory and configuration code elsewhere. But now the messages show up there:

The notifications show up with Brave Browser on Android. With Brave Browser on Fedora it didn't show up. But that's still okay.

Then I've rounded it all up with some more documentation and a changelog entry for the next release. And that was it! With the clean architecture it is always surprising how easy the actual changes are. The actual work is in making the change possible. Every time I add something to this program, I am amazed how pleasant it is to work with the code. This is the UML diagram now:

The amount of code has increased significantly over time, in part due to many small modules, one import per line and also improved test coverage.

                          Ohloh Line Count Summary                          

Language          Files       Code    Comment  Comment %      Blank      Total
----------------  -----  ---------  ---------  ---------  ---------  ---------
python               80       3492         95       2.6%        822       4409
----------------  -----  ---------  ---------  ---------  ---------  ---------
Total                80       3492         95       2.6%        822       4409

Yet it still is a small project, given the absolute number of lines. And most importantly it still feels completely easy to manage because it is so nicely organized.