webXMPP

Check-in [4a044ab760]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Snapshot, a notification client for DBus-ish mobile devices.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:4a044ab760c356e73f9e8bfd0ac431e3a25b75ea41445b0e8e7cfe0bd7686ad0
User & Date: vandys 2018-09-19 23:18:36
Context
2018-09-20
00:04
First code-up, notifications onto a Ham repeater announcement check-in: f9faa86edd user: vandys tags: master, trunk
2018-09-19
23:18
Snapshot, a notification client for DBus-ish mobile devices. check-in: 4a044ab760 user: vandys tags: master, trunk
2018-07-01
14:20
Catch up w. old git source. Catch edge case for status update. Move to Fossil. check-in: b56a71baa3 user: web tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added tools/notified.py.

























































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
#
# notified.py
#	Client for UDP notifications
# vim: expandtab
#
# Ugh, Ubuntu Phone seems to have pretty much gone to Python 3, so
#  here we are.
#
# This client is written to run on a mobile, battery powered
#  device.  Being battery powered means it wants to transmit
#  rarely, and receive infrequently.  Against this, it must
#  accept that its network can go down or change address, and
#  such changes might occur without any other system event
#  to indicate the change.
#
# The initial implementation is for Ubuntu Touch.  It plays
#  a sound and pushes a notification when receiving new
#  events.  It also turns on a blinking LED where it can,
#  and clears it only when the device is next unlocked.
#
# A client's request indicates the current event
#  generation seen (initially 0).  It specifies how many
#  event details it wants at most, along with whether its wants
#  subject and even body.  The server always indicates
#  the latest event generation, and considers the events delivered
#  regardless of how many bodies the client wanted to receie.
# Imagine the request is
#  {nextev: 100, detail: 2, nevent: 1}
# and that the server next event # is 103 (i.e., 100, 101 and
#  102 have arrived).  The server will respond:
#  {nextev: 103, events: [["xmpp", "Joe"]]}
# which tells the client to ask next for 103, and it can
#  display that the first event (100) had a subject line
#  of "event100-subject" from an XMPP message.
#
# Event details are reported with the specified detail.
#  Leve1 1 is just the event source type (XMPP, SMS, ...).
#  Level 2 is the sender.  Level 3 is the subject line,
#  level 4 is a message body.  In the above example the
#  client requested the first two items, thus seeing
#  that it's an XMPP message from Joe.  At detail 0, you
#  would only see that there were new events, but no other
#  detail.
#
# TBD: roster & presence.
#
import sys, json, time, os, traceback
w = sys.stdout.write
import notify2
import pong
import pdb

# For DBus sniffing
import dbus, dbus.exceptions, dbus.mainloop.glib
import threading
from gi.repository import GLib

# Are LEDs available for notification indication?
LED = "/sys/class/leds/red"
blinking = leds = False

# Initial condition, no events ever seen
gen = 0

# Hook for logging
def log(s):
    sys.stderr.write(s)
    sys.stderr.write('\n')

# Quick/easy way to turn dict into an ob with those k/v as attrs
class DictOb(object):
    def __init__(self, d):
        for k,v in d.items():
            setattr(self, str(k), v)

# Configuration, a DictOb
cfg = None

# DBus monitoring.  We need to know if the device is on/active, since
#  we only want to blink the LED if it's off (and only until it
#  goes back on)
is_locked = True
def dbus_watcher(*args, **kwargs):
    global blinking, is_locked

    dd = args[1]
    if "IsActive" not in dd:
        return
    is_locked = bool(dd["IsActive"])
    if blinking and (not is_locked):
        unblink()

def setup_dbus():
    global leds

    # We only need this to turn the LED indication back off, so
    #  skip it if there's no LED's in the first place
    if not leds:
        return

    # We have to give it a thread for watching the bus
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    l = GLib.MainLoop()
    t = threading.Thread(target=l.run)
    t.start()

    # Now register a callback to see "desktop" changes
    sess = dbus.SessionBus()
    sess.add_signal_receiver(dbus_watcher, "PropertiesChanged",
        dbus_interface="org.freedesktop.DBus.Properties")

# LED device manipulation
def set_led(base, val):
    f = open(LED + "/" + base, "w")
    f.write(val)
    f.close()
def get_led(base):
    f = open(LED + "/" + base, "r")
    res = f.read()
    f.close()
    return res

# Set LED mode, if we can
def setup_leds():
    global leds

    try:
        set_led("on_off_ms", "500 2500")
        brt = get_led("max_brightness")
        set_led("brightness", brt)
        set_led("rgb_start", "0")
        leds = True
    except:
        pass

# Start/stop blinking
def blink():
    global blinking
    set_led("rgb_start", "1")
    blinking = True
def unblink():
    global blinking
    set_led("rgb_start", "0")
    blinking = False

# Pull config out of JSON save format
def load_cfg():
    global cfg

    # Get our configuration
    f = open(os.getenv("HOME") + "/.config/notify.json", "r")
    d = json.loads(f.read())
    f.close()
    cfg = DictOb(d)

# New notifications contained in this packet
def notify(pak):
    global gen, w, leds, blinking, is_locked

    inner = pak.inner
    w("Notification: gen %d -> %d\n" % (gen, inner["gen"]))
    for tup in inner["msgs"]:
        lt = len(tup)

        # Ignore mirrors of our own sends on other devices
        if lt and (not tup[0]):
            continue

        # No details at all, so just show one notification
        if lt in (0, 1):
            n1 = "New Message"
            n2 = None

        # Just who
        elif lt == 2:
            n1 = "New Message"
            n2 = tup[1]

        # Who plus headline 
        elif lt == 3:
            n1 = tup[1]
            n2 = tup[2]

        # Who plus headline plus body
        else:
            n1 = tup[1] + ": " + tup[2]
            n2 = tup[3]

        # Build actual notification
        n = notify2.Notification(n1, n2)
        log("Notification: " + n1 + ((", " + n2) if n2 else ""))
        try:
            n.show()
        except:
            traceback.format_exc()
            log("Notification not displayed")

    # Worry about the LEDs?
    if leds and (not blinking) and is_locked:
        blink()

    # Play sound
    os.system("paplay --volume=40000 %s" % (cfg.sound,))

# Endless execution, notification client
def run():
    global cfg, gen

    # Get a wrapper for our pong network connection
    conn = pong.Client(cfg.server, cfg.port, cfg.user, cfg.password)

    # Prep LEDs for use if possible
    setup_leds()

    # Watch DBus to turn LED's off when screen unlocks
    setup_dbus()

    # Prepare for notifications
    notify2.init("Messsaging")

    # Our notification server
    dest = (cfg.server, cfg.port)

    # We need a send-receive-send pattern as a minimum to become
    #  an assured/streaming UDP "connection".  This loop starts
    #  a new socket, does a param get/got, and then a timed
    #  get.  At the timeout, the server provides a "got", after
    #  which we send another "get".  This keeps the firewall/NAT
    #  state alive indefinitely, at a cost of one transmit and
    #  one receive every 2.5 minutes.
    # When we hit a network error (usually due to change in our
    #  IP address), we reset the connection and start over with
    #  the param get/got.
    while True:
        # Params; we in particular need to know the server's
        #  intended timeout.
        while True:
            pak = conn.pingpong(conn.msg("params", "get"))
            if pak is not None:
                break
            # Note that on error, the "pong" library will already
            #  have closed out the socket
            time.sleep(pong.WAITNET)

        # This is how long they'll hold a notify/get pending before
        #  sending back a null result.
        # If we request one and don't hear back in this amount of
        #  time, we have a lost packet or something.
        # 1.1 is our 10% slop factor over the expected timeout
        #  from the server.
        tmo = pak.inner["timeout"] * 1.1

        # Server loop
        while True:
            # Always yield for a second, so no matter
            #  what we never CPU spin hard.
            time.sleep(1)

            # Next round of notifications
            # Request events starting at this serial number
            pak = conn.msg("notify", "get", {
                "gen": gen,
                "detail": cfg.detail,
                "nmsg": cfg.nmsg})
            pak.who = dest
            resp = conn.ping_pong(pak, tmo)

            # Failure
            if resp is None:
                # We drop out of the server loop, and start over
                #  with a fresh socket and param get/got
                time.sleep(pong.WAITNET)
                break

            # Nothing happened
            if resp.inner["gen"] == gen:
                continue

            # New messages
            notify(resp)
            gen = resp.inner["gen"]

if __name__ == "__main__":
    load_cfg()

    # No arg, just be a service daemon
    if len(sys.argv) == 1:
        run()

    # Debug mode; get a connection to our test server
    conn = pong.Client(cfg.server, cfg.port, cfg.user, cfg.password)

    # Get params, display, exit
    if sys.argv[1] == "params":
        params = conn.pingpong(conn.msg("params", "get"))
        print(params)
        sys.exit(0)

    sys.stderr.write("Unknown operation %s\n" % (sys.argv[1],))
    sys.exit(1)