blog/data_static/blog_posts/20180402_signal.md

241 lines
7.5 KiB
Markdown
Raw Permalink Normal View History

2020-07-13 11:54:16 +02:00
# Auto adapting signal to day-night scheme
So I wanted to automatically change the day/night theme of my brand new Signal desktop app. The way that it is manually done is through the `File -> Preferences...` menu. Now that's not convinient for programatic person like me. Let's investigate!
## `.config` folder
My first stab is looking into the mostly standard `~/.config` folder in Linux for Signal if one actually exists. It turns out it does. Let's see. It even has a `~/.config/Signal/config.json` file. Unfortunately, that file doesn't contain the settings for the theme, just window positions.
`grep -r`ing in the folder for any of `android` or `theme` just returns the logfile that Signal is writing to.
```
...
./logs/log.log.1:{..."theme-setting changed to android-dark",...}
...
```
No luck.
Another stab is at `/opt/Signal` directory. Again - no luck. And the app is a binary ELF executable
```
$ file signal-desktop
signal-desktop: ELF 64-bit LSB executable,...
```
## `strace`-ing
My next attempt is to attach to attach to the Signal process via `strace`. Find the process by `ps aux|grep signal`, attach by `strace -p <pid>` and try to change the . Boy, this does a lot of stuff! Let's filter by writing:
`strace -e write -p 12345`
Still, too much but I saw something hapenning amid the writing to `40` and `71` file descriptors (fd).
```bash
#...
write(40, "!", 1) = 1
write(71, "\0", 1) = 1
write(40, "!", 1) = 1
write(1, "{\"name\":\"log\",\"hostname\":\"pi2-ho"..., 147) = 147
write(12, "\1\0\0\0\0\0\0\0", 8) = 8
write(71, "\0", 1) = 1
write(40, "!", 1) = 1
#...
```
All right, the `1` fd is the log that is being written that I already saw. How about that 12 fd? Let's explore within the `proc` fs:
```bash
$ cd /proc/1234/fd
$ ls -la
# ...
lrwx------ 1 pi2 pi2 64 Mar 29 23:23 12 -> anon_inode:[eventfd]
# ...
```
Oh boy, some wierd anonymous inode. So we are definately not dealing with a good old filesystem file but rather some on the fly created fd. Let's explore a bit more...
## Signal is an Electron app
Taking a look at the [github repo of the project](https://github.com/signalapp/Signal-Desktop). Searching for `dark` and `theme` reveals css, sass files. Okay
Looking at the menus there is `View -> Toggle Developer Tools` and BAM - a good old Chrome developer tools console pops up! Inspecting the elements, it's pretty clear what is happening:
```html
<body class="android">
<!-- changes to -->
<body class="android-dark">
```
Brilliant, the change of theme just changes the class on the `body` element. Let's see if we have jquery or I have to go to the dark ages of javascript's `findElementByTagName`:
```javascript
> $
< function ( selector, context ) {
// The jQuery object is actually just the init constructor 'enhanced' ...
```
Good, we have. Now I "just" have to find a way to connect to chrome console via a good old terminal in some way...
## Chrome remote debugging
Searching the Internets, I find a [post](https://blog.chromium.org/2011/05/remote-debugging-with-chrome-developer.html) in Chromium blog from way back in 2011 that I can run Chrome with `--remote-debugging-port` flag which enables some sort of a remote console.
Would the electron app do the same?
```bash
signal-desktop --remote-debugging-port=9222
```
Indeed it does. This console however is not just a simple tcp connection to the javascript console. Running `echo "alert('echo'); | netcat localhost 9222` does nothing. More reading to go...
Turns out, Chrome DevTools has its [own protocol](https://chromedevtools.github.io/devtools-protocol/) that among other things exposes websockets. Let's see what clients exist for that
[Quite a lot](https://github.com/ChromeDevTools/awesome-chrome-devtools).
Any pythonic ones? [pychrome](https://github.com/fate0/pychrome) seems simple enough.
Let's be modern, do python3:
```bash
pip3 install pychrome
```
And I will use my favourite repl, `ipython` and follow the Getting started tutorial:
```python
import pychrome
browser = pychrome.Browser(url="http://127.0.0.1:9222")
```
Awesome, I have a connection. The tutorial goes into opening a tab. I don't want that, I assume I should alread have a tab. Exploring `browser` object (via my favourite [ipython repl](http://ipython.org/)):
```python
In [3]: dir(browser)
Out[3]: [... _private_stuff
'activate_tab',
'close_tab',
'dev_url',
'list_tab',
'new_tab',
'version']
In [4]: browser.list_tab()
Out[4]: [<Tab [38473d8e-ad8d-4325-8cb6-81aaec36c124]>]
In [5]: tab = browser.list_tab()[0]
In [6]: dir(tab)
Out[6]: [... _private_stuff
'call_method',
'debug',
'del_all_listeners',
'event_handlers',
'event_queue',
'get_listener',
'id',
'method_results',
'set_listener',
'start',
'status',
'status_initial',
'status_started',
'status_stopped',
'stop',
'type',
'wait']
```
The README goes into navigating pages and the examples folder is also not what I am looking for. But they do tell me `call_method` is probably what I want, just need to find the proper string and parameters in the protocl. I want to execute a javascript call.
Back to the [Chrome DevTools Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/). I look through a few of the Domains listed and finally arrive at [Runtime.evaluate](https://chromedevtools.github.io/devtools-protocol/tot/Runtime#method-evaluate) method.
```python
In [7]: tab.start()
In [8]: tab.call_method('Runtime.evaluate', expression='alert("hi");')
# I GET AN ALERT IN THE BROWSER! Dismiss it.
Out[8]: {'result': {'type': 'undefined'}}
```
Finally! I have access to my javascript console in the browser! Let's write the jquery change of theme:
```python
tab.call_method('Runtime.evaluate', expression='$("body").removeClass("android").addClass("android-dark");')
```
That worked!
## Wrapping it up
Let's write a propper python script now:
```python
#!/usr/bin/env python3
import sys
import pychrome
sys_argv = sys.argv
if len(sys_argv) < 2:
print("first argument needs to be day|night")
exit(1)
command = sys_argv[1]
if command == "night":
expression = js_dark_to_light = """
$("body").removeClass("android").addClass("android-dark");
"""
elif command == "day":
expression = js_light_to_dark = """
$("body").removeClass("android-dark").addClass("android");
"""
else:
print("first argument needs to be day|night")
exit(1)
browser = pychrome.Browser(url="http://127.0.0.1:9222")
tab = browser.list_tab()[0]
tab.start()
tab.call_method('Runtime.evaluate', expression=expression)
tab.stop()
```
Running `./change_signal.py day` changes to white theme and `./change_signal.py night` goes to dark theme.
I also want to change the `signal-desktop` app to always start with the remote-debug flag enabled, so I do that:
```bash
rm /usr/local/bin/signal-desktop
cat > /usr/local/bin/signal-desktop << EOF
#!/bin/bash
/opt/Signal/signal-desktop --remote-debugging-port=9222
EOF
chmod +x /usr/local/bin/signal-desktop
```
And that's it. Code can be found in my [sun](https://gitlab.com/pisquared/sun) repo which also deals with other day-night related dark-light themes including a script to detect whether the sun has set or not:
```bash
$ ./calc_sun.py
usage: calc --loc=location_name [--date-format]
calc --lat=latitude --lon=longitude [--tz|--date-format]
returns
current_time sunrise sunset day|night
$ ./calc_sun.py --lat=51.21 --lon=4.40 --tz=Europe/Brussels
23:57 07:23 20:10 night
```
So now I can chain that:
```bash
state=`./current.sh | cut -f4 -d' '`
./change_signal.py $state
```
Voala! That was fun 🙂😊