webXMPP

Check-in [6645a08598]
Login

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

Overview
Comment:Split out so we can support Chrome via Google's proprietary messaging.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:6645a085980d0b1974c0eaf1c2172d13484adcef540947141ff1fcb6bb3d789f
User & Date: ajv-899-334-8894@vsta.org 2017-04-19 18:19:13
Context
2017-04-21
03:44
Tidy up how we choose a host name for the notification click URL. Biggie, wrestle with how sometimes we don't post a new get (or, maybe, it doesn't work) when we get freshly exposed from the Android notification click. Actually go to the trouble of closing the server side of pending GET's when the client goes background (it also exit's the thread serving the GET). Seems to help with the issue, but we'll see... check-in: bbcc0a9e94 user: ajv-899-334-8894@vsta.org tags: master, trunk
2017-04-19
18:19
Split out so we can support Chrome via Google's proprietary messaging. check-in: 6645a08598 user: ajv-899-334-8894@vsta.org tags: master, trunk
2017-04-06
04:02
Fiddle with making "them" changes stand out check-in: 83b7abbca1 user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added fcm.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
#
# fcm.py
#	Google Firebase Cloud Messaging
#
# Kinda sucks having to use a proprietary, closed-source
#  suite of API's, but Google sure as hell is slowly choking
#  out all other options.
# Viva la web.
#
import sys, httplib, json

# Google endpoint
FCMHOST = "fcm.googleapis.com"
FCMPATH = "/fcm/send"

class FCM_mixin(object):
    def __init__(self):
	self.fcmkey = self.config.get("fcmkey")
    def fcm(self, iid_token, title, body):

	data = json.dumps(
	    {"notification": {
		 "title": title,
		 "body": body,
		 "click_action": "/html/xmpp_fcm.html"
		},
	     "to": iid_token,
	    }
	)
	h = httplib.HTTPSConnection(FCMHOST)
	headers = {
	    "Authorization": ("key=%s" % (self.fcmkey,)),
	    "Content-Type": "application/json",
	}
	sys.stderr.write("POST FCM to %r\n" % (iid_token,))
	h.request("POST", FCMPATH, data, headers)
	r = h.getresponse()
	sys.stderr.write(" FCM response %r reason %r\n" %
	    (r.status, r.reason))

Changes to get.py.

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
..
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
...
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
	if self.path_match("favicon.ico"):
	    self.send_error(404)
	    return True,None
	return False,None

    # "/"; main UI
    def send_top(self):







	return self.send_redir("/html/xmpp.html")

    # The Service Worker script has to be at the root, peer with
    #  the HTML.  So let it be found here, even though it lives down
    #  with the rest of the JS


    def send_sw(self):
	pp = self.paths
	# "sw*.js" -> "js/sw*.js"
	if (len(pp) == 1) and (pp[0].startswith("sw")) and \
		(pp[0].endswith(".js")):

	    return True,self.send_files("js/" + pp[0])
	return False,None

    # Send what we have, based on generation @gen
    def send_current(self, gen):
	webxmpp = self.server.approot
	user = webxmpp.users[self.user]
	msgs = []
	for tup in user.msgs:
	    if tup[0] >= gen:
		d = {}
		d["rx"] = tup[1]
................................................................................
	# Here's the messages we have right now
	resp = { "serial": user.serial,
	    "gen": user.gen, "rgen": user.rgen,
	    "msgs": msgs }
	user.serial += 1

	# Add a roster update?
	if int(self.vals.get("rgen", "0")) != user.rgen:
	    uroster = []
	    for name in user.roster:
		uroster.append( (name, user.status.get(name, "offline")) )
	    uroster.sort(cmp = _order_stat)
	    resp["roster"] = uroster
	    sys.stderr.write("%s: new roster\n" % (user.name,))
	else:
................................................................................
	    sema = threading.Semaphore(0)
	    user.await(timeout, sema)
	    sema.acquire()
	    # Either a timeout kicked us loose,
	    #  or a message arrived and did so

	# Supply what we have.  (It may well be empty.)
	return True,self.send_current(gen)

    # Supply server config params
    def send_config(self):
	if not self.path_match("config.json"):
	    return False,None

	cfg = self.server.approot.config
	d = {}
	for k in ("poll1", "pollslop", "nmsg"):
	    d[k] = cfg[k]
	return True,self.send_json(d)







>
>
>
>
>
>
>




|
>
>



|
|
>




|







 







|







 







|











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
...
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
...
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
	if self.path_match("favicon.ico"):
	    self.send_error(404)
	    return True,None
	return False,None

    # "/"; main UI
    def send_top(self):
	a = (self.headers.get("User-Agent") or "").lower()

	# You just can't reliably message to mobile Chrome without using
	#  Google's push service.
	# if ("mobile" in a) and ...
	if "chrom" in a:
	    return self.send_redir("/html/xmpp_fcm.html")
	return self.send_redir("/html/xmpp.html")

    # The Service Worker script has to be at the root, peer with
    #  the HTML.  So let it be found here, even though it lives down
    #  with the rest of the JS for purposes of actual storage.
    # Same deal with magic file "manifest.json".  Web apps are dying,
    #  but this stuff sticks around like soap scum in your bathtub.
    def send_sw(self):
	pp = self.paths
	# "sw*.js" -> "js/sw*.js"
	if (len(pp) == 1) and \
		((pp[0].endswith("-sw.js")) or
		 (pp[0] == "manifest.json")):
	    return True,self.send_files("js/" + pp[0])
	return False,None

    # Send what we have, based on generation @gen
    def send_current(self, gen, rgen):
	webxmpp = self.server.approot
	user = webxmpp.users[self.user]
	msgs = []
	for tup in user.msgs:
	    if tup[0] >= gen:
		d = {}
		d["rx"] = tup[1]
................................................................................
	# Here's the messages we have right now
	resp = { "serial": user.serial,
	    "gen": user.gen, "rgen": user.rgen,
	    "msgs": msgs }
	user.serial += 1

	# Add a roster update?
	if rgen != user.rgen:
	    uroster = []
	    for name in user.roster:
		uroster.append( (name, user.status.get(name, "offline")) )
	    uroster.sort(cmp = _order_stat)
	    resp["roster"] = uroster
	    sys.stderr.write("%s: new roster\n" % (user.name,))
	else:
................................................................................
	    sema = threading.Semaphore(0)
	    user.await(timeout, sema)
	    sema.acquire()
	    # Either a timeout kicked us loose,
	    #  or a message arrived and did so

	# Supply what we have.  (It may well be empty.)
	return True,self.send_current(gen, rgen)

    # Supply server config params
    def send_config(self):
	if not self.path_match("config.json"):
	    return False,None

	cfg = self.server.approot.config
	d = {}
	for k in ("poll1", "pollslop", "nmsg"):
	    d[k] = cfg[k]
	return True,self.send_json(d)

Changes to html/xmpp.html.

12
13
14
15
16
17
18

19


20
21
   <select id="ourDests">
    <option value="--" selected>--</option>
   </select>
   <input type="text" id="ourSend" name="message">
   <input type="submit" onclick="return doSend();"
    name = "send" value="Send">
  </form>

 <script src="/js/ui.js"></script>


 </body>
</html>







>

>
>


12
13
14
15
16
17
18
19
20
21
22
23
24
   <select id="ourDests">
    <option value="--" selected>--</option>
   </select>
   <input type="text" id="ourSend" name="message">
   <input type="submit" onclick="return doSend();"
    name = "send" value="Send">
  </form>

 <script src="/js/ui.js"></script>
 <script src="/js/polling.js"></script>

 </body>
</html>

Added html/xmpp_fcm.html.























































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
<html>
 <head>
  <title>WebXMPP Server</title>
  <meta name="viewport"'
   content="width=device-width, initial-scale=1">
  <link rel="stylesheet"' href="/css/main.css">
 </head>
 <body ononline="goOnline" onoffline="goOffline">
  <div id="ourTexts" style="width: 100%;">
  </div>
  <form>
   <select id="ourDests">
    <option value="--" selected>--</option>
   </select>
   <input type="text" id="ourSend" name="message">
   <input type="submit" onclick="return doSend();"
    name = "send" value="Send">
  </form>

 <script src="/js/ui.js"></script>

 <script src="https://www.gstatic.com/firebasejs/3.7.8/firebase.js"></script>
 <script src="/js/fcm.js"></script>

 </body>
</html>

Added js/fcm.js.







































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
//
// fcm.js
//	Support goo for Google's proprietary messaging
//
// This is the app side; firebase-messaging-sw.js also knows
//  the config and appears to be automagically loaded in
//  a Service Worker.
//
"use strict";

const config = {
  apiKey: "AIzaSyAl-NGH3kz7m1Qt8iQF2MgbP4bLTSaaQ1o",
  authDomain: "vatclip.firebaseapp.com",
  databaseURL: "https://vatclip.firebaseio.com",
  projectId: "vatclip",
  storageBucket: "vatclip.appspot.com",
  messagingSenderId: "573826140448"
};
firebase.initializeApp(config);
const messaging = firebase.messaging();

// Initial token value
let myToken = null;
function updateToken() {
    messaging.getToken()
    .then(function(t) {
	myToken = t;
	console.log('Messaging token: ' + t); });
}
updateToken();

// Note new token values
// TBD, if we're background the old token is still up
//  at our server.
messaging.onTokenRefresh(updateToken);

// Notifications come via Google messaging and the Service Worker
function notify(msg) {
    console.log("Unused notify call: " + msg);
}

// UI exposure
let bg = false;

// For Chrome, when we lose focus we'll have to use their push API
function visChange(ev) {
    const vs =  document.visibilityState;

    if (vs == "hidden") {
	if (!bg) {
	    bg = true;
	    // Abort long polling since Chrome will stop running soon
	    goOffline();

	    // Wait for UI exposure
	    ourState = 3;

	    // Tell server to use Google's proprietary, closed-source
	    //  messaging.  Overload this mode switch with telling
	    //  it whatever our latest token value is.
	    const myReq = new XMLHttpRequest();
	    myReq.open("PUT", "/user?bg=" + myToken);
	    myReq.send();
	}
	return;
    }
    if (vs == "visible") {
	if (bg) {
	    // Coming back to foreground, start up long polling
	    bg = false;
	    moreMessages();

	    // Retire messaging token now that we'll get
	    //  our notifications from a regular GET
	    const myReq = new XMLHttpRequest();
	    myReq.open("PUT", "/user?fg=" + myToken);
	    myReq.send();
	}
    }
}

// Know when to count on push messages
document.addEventListener("visibilitychange", visChange);

Added js/firebase-messaging-sw.js.













































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//
// firebase-messaging-sw.js.js
//	Support goo for Google's proprietary messaging
//
// This is the code loaded into a Service Worker
//
importScripts('https://www.gstatic.com/firebasejs/3.5.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.5.2/firebase-messaging.js');

const config = {
  apiKey: "AIzaSyAl-NGH3kz7m1Qt8iQF2MgbP4bLTSaaQ1o",
  authDomain: "vatclip.firebaseapp.com",
  databaseURL: "https://vatclip.firebaseio.com",
  projectId: "vatclip",
  storageBucket: "vatclip.appspot.com",
  messagingSenderId: "573826140448"
};
firebase.initializeApp(config);
const messaging = firebase.messaging();

// If needed, could add:
//  messaging.setBackgroundMessageHandler(function(payload)...

Added js/manifest.json.











>
>
>
>
>
1
2
3
4
5
{
  "//": "Some browsers will use this to enable push notifications.",
  "//": "It is the same for all projects, this is not your project's sender ID",
  "gcm_sender_id": "103953800507"
}

Added js/polling.js.



















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
//
// polling.js
//	Handle page visibility when Long Polling is used
//
"use strict";

// If they let us, post a notification
function notify(s) {
    var n;

    if (Notification.permission == "granted") {

	// Standard Notification
	var opts = {body: s, silent: false};
	n = new Notification("xmpp", opts);
	n.onclick = function() {
	    // Gee, I'd really like to expose my UI
	    // thisWin.focus();

	    // But at least we can start noting new
	    //  messages.
	    moreMessages();

	    // Dispatch this event
	    n.close();
	}
    }
}

// This fires when we've posted a notification event and are now
//  idle until the focus returns here.
function updateVisibility() {
    if (document.hidden == false) {
	// Start listening for more messages actively
	moreMessages();
    }
}

// Visibility hook
document.addEventListener("visibilitychange", updatevisibility);

Deleted js/sw.js.

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
//
// sw.js
//	Service Worker code
//
// In case we ever need to catch fetch's, this source is at the root
//  rather than down in js/; script location is the root of fetch prefix
//  interception, and mismatches seem to break actual connection with
//  a path.
//
// Currently, there's no meaningful way to do "push" messaging via
//  long polling.  So we still long poll from the main page, but on
//  Chrome still have to push out the Notification via this otherwise
//  vacuous Service Worker.
//
self.addEventListener("notificationclick", function(ev) {

    console.log("Closed notification");

    ev.notification.close();

    ev.waitUntil(clients.matchAll({
      type: "window"
    }).then(function(clientList) {
      for (let i = 0; i < clientList.length; i++) {
        const client = clientList[i];
        if (client.focus) {
            return client.focus();
        }
      }
      if (clients.openWindow) {
        return clients.openWindow('/html/xmpp.html');
      }
    }));
});
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































































Changes to js/ui.js.

12
13
14
15
16
17
18

19
20
21
22
23
24
25
..
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
...
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
...
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
...
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339

340
341
342
343
344
345
346
...
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401





402
403
404
405
406
407
408
409
410
411
412
413
...
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
//  a changing IP address, as well as the battery impact of the
//  actions we take.  Our XMLHttpRequest operations include a
//  timeout we specify for the server--which will always answer
//  after that time interval, even if there's nothing new.  We
//  specify our own timeout, moderately larger, which will fire if
//  our network connection goes away, or changes our address.
//


// Keep static handle to this window
const thisWin = window;

// We always have an event pending.  This tracks the state of
//  what we expect next.
//
................................................................................

// XHR timeout value, for network failures
let rxTimeout = null;

// Current XHR request outstanding
let curReq = null;

// First time we need to post a notification event, we'll
//  add the visibility listener as well.
let tbdVisibility = true;

// How long to let POST's go (8s)
const HTTP_TIMEOUT = 8000;

// How long to hold off new XHR after network error (2s)
const RX_TIMEOUT = 2000;
let rxTO = null;

// Dynamic page title; tells us when we heard from the server
let lastTM = '';

// Latest serial # back from server completion
let lastSerial = 0;

// Flag that we have to work around mobile Chrome
let chomeHack = false;
let Reg = null;

// Returning online (network change, cell->wifi, etc.)
function goOnline() {
    if (curReq != null) {
	curReq.abort();
	curReq = null;
    }
    moreMessages();
................................................................................
	ourTexts.appendChild(d);
    }

    // Always look at the bottom of it
    ourTexts.scrollTop = 99999;
}

// If they let us, post a notification
function notify(s) {
    var n;

    if (Notification.permission == "granted") {
	// Mobile Chrome workaround
	if (chromeHack) {
	    Reg.showNotification("xmpp - " + s);
	    return;
	}

	// Standard Notification
	var opts = {body: s, silent: false};
	n = new Notification("xmpp", opts);
	n.onclick = function() {
	    // Gee, I'd really like to expose my UI
	    // thisWin.focus();

	    // But at least we can start noting new
	    //  messages.
	    moreMessages();

	    // Dispatch this event
	    n.close();
	}
    }
}

// This fires when we've posted a notification event and are now
//  idle until the focus returns here.
function updateVisibility() {
    if (document.hidden == false) {
	// Start listening for more messages actively
	moreMessages();
    }
}

// Return HH:MM time
function tmNow() {
    var d = new Date();
    var hr = d.getHours();
    var mn = d.getMinutes();
    if (hr < 10) {
	hr = '0' + String(hr);
................................................................................
    // If we see the same message again, the network is probably
    //  unavailable and our browser is giving us a cached page.
    // (Yes, our server said not to cache, but it can still happen.)
    if (resp.serial == lastSerial) {
	rxerror();
	return;
    }
    lastSerial = req.serial;

    // We flag when we last heard from the mothership in the title.
    // Update it.
    var tm = tmNow();
    if (tm != lastTM) {
	document.title = "WebXMPP updated " + tm;
	lastTM = tm;
................................................................................
	// Clear old
	ourDests.length = 0;

	// Build new
	// First one says "whoever last sent to us", the rest
	//  enumerate buddies in our rosters.
	ourDests[0] = new Option("--", "--", true, false);
	for (var i = 0; i < resp.roster.length; ++i) {
	    // Each tuple is (acct-name, acct-status)
	    var tup = resp.roster[i];
	    var val = tup[0]
	    var st = tup[1]

	    // Label +name for available, -name for away,
	    //  and .name for extended away.  While not an XMPP
	    //  status, "offline" is a leading blank for a roster
	    //  member who is not online.

	    if ((st == "available") || (st == "chat")) {
		nm = "+" + val;
	    } else if (st == "away") {
		nm = "-" + val;
	    } else if (st == "xa") {
		nm = "." + val;
	    } else if (st == "dnd") {
................................................................................

    // Post a Notification?
    if (document.hidden) {
	if (ourState != 3) {
	    notify(msg.body + " - " + msg.them);
	    ourState = 3;
	}

	// No more requests for messages; only UI exposure
	//  will bring us back into action.
	if (tbdVisibility) {
	    document.addEventListener(
	     "visibilitychange", updateVisibility, false);
	    tbdVisibility = false;
	}
	return;
    }

    // Not hidden, looking at screen
    moreMessages();
}

// Deal with an error while asking for messages
function rxerror() {





    // Failed request
    curReq = null;

    // If network down, wait for network up event
    if (ourState == 5) {
	return;
    }

    // Don't spin on error; insert a small delay
    rxTO = setTimeout(
	function() { rxTO = null; moreMessages(); },
	RX_TIMEOUT);
................................................................................
    // Now get initial messages to display
    moreMessages();
}

// Entry point for processing
function ourStart() {

    // Precalc if this is mobile Chrome.  They don't offer the
    //  standard Notification API, so we have to route it through
    //  a null Service Worker.
    const a = navigator.userAgent.toLowerCase();
    chromeHack = ((a.search("chrom") != -1) && (a.search("mobil") != -1));

    // We'll want to send notifications (if the user
    //  lets us)
    Notification.requestPermission(function(result) {
	if (result == "granted") {
	    if (chromeHack) {
		console.log("chrome hack notify active");
		navigator.serviceWorker.register("/sw.js")
		    .then(function(reg) {
			console.log("registered: " + reg.scope);
		    });
		navigator.serviceWorker.ready.then(function(registration) {
		    console.log("sw ready");
		    Reg = registration;
		});
	    }
	}
    });

    // Send our initial update request
    ourState = 1;
    curReq = new XMLHttpRequest();
    curReq.open("GET", "/config.json");







>







 







<
<
<
<













<
<
<
<







 







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







 







|







 







|

|
|
|





>







 







<
<
<
<
<
<
<
<









>
>
>
>
>



|
|







 







<
<
<
<
<
<




<
<
<
<
<
<
<
|
<
<
<







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
..
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
...
211
212
213
214
215
216
217





































218
219
220
221
222
223
224
...
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
...
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
...
335
336
337
338
339
340
341








342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
...
423
424
425
426
427
428
429






430
431
432
433







434



435
436
437
438
439
440
441
//  a changing IP address, as well as the battery impact of the
//  actions we take.  Our XMLHttpRequest operations include a
//  timeout we specify for the server--which will always answer
//  after that time interval, even if there's nothing new.  We
//  specify our own timeout, moderately larger, which will fire if
//  our network connection goes away, or changes our address.
//
"use strict";

// Keep static handle to this window
const thisWin = window;

// We always have an event pending.  This tracks the state of
//  what we expect next.
//
................................................................................

// XHR timeout value, for network failures
let rxTimeout = null;

// Current XHR request outstanding
let curReq = null;





// How long to let POST's go (8s)
const HTTP_TIMEOUT = 8000;

// How long to hold off new XHR after network error (2s)
const RX_TIMEOUT = 2000;
let rxTO = null;

// Dynamic page title; tells us when we heard from the server
let lastTM = '';

// Latest serial # back from server completion
let lastSerial = 0;





// Returning online (network change, cell->wifi, etc.)
function goOnline() {
    if (curReq != null) {
	curReq.abort();
	curReq = null;
    }
    moreMessages();
................................................................................
	ourTexts.appendChild(d);
    }

    // Always look at the bottom of it
    ourTexts.scrollTop = 99999;
}






































// Return HH:MM time
function tmNow() {
    var d = new Date();
    var hr = d.getHours();
    var mn = d.getMinutes();
    if (hr < 10) {
	hr = '0' + String(hr);
................................................................................
    // If we see the same message again, the network is probably
    //  unavailable and our browser is giving us a cached page.
    // (Yes, our server said not to cache, but it can still happen.)
    if (resp.serial == lastSerial) {
	rxerror();
	return;
    }
    lastSerial = resp.serial;

    // We flag when we last heard from the mothership in the title.
    // Update it.
    var tm = tmNow();
    if (tm != lastTM) {
	document.title = "WebXMPP updated " + tm;
	lastTM = tm;
................................................................................
	// Clear old
	ourDests.length = 0;

	// Build new
	// First one says "whoever last sent to us", the rest
	//  enumerate buddies in our rosters.
	ourDests[0] = new Option("--", "--", true, false);
	for (let i = 0; i < resp.roster.length; ++i) {
	    // Each tuple is (acct-name, acct-status)
	    const tup = resp.roster[i];
	    const val = tup[0]
	    const st = tup[1]

	    // Label +name for available, -name for away,
	    //  and .name for extended away.  While not an XMPP
	    //  status, "offline" is a leading blank for a roster
	    //  member who is not online.
	    let nm = null;
	    if ((st == "available") || (st == "chat")) {
		nm = "+" + val;
	    } else if (st == "away") {
		nm = "-" + val;
	    } else if (st == "xa") {
		nm = "." + val;
	    } else if (st == "dnd") {
................................................................................

    // Post a Notification?
    if (document.hidden) {
	if (ourState != 3) {
	    notify(msg.body + " - " + msg.them);
	    ourState = 3;
	}








	return;
    }

    // Not hidden, looking at screen
    moreMessages();
}

// Deal with an error while asking for messages
function rxerror() {
    // Already cancelled
    if (curReq == null) {
	return;
    }

    // Failed request
    curReq = null;

    // If network down or not needed, don't try new operations
    if ((ourState == 5) || (ourState == 3)) {
	return;
    }

    // Don't spin on error; insert a small delay
    rxTO = setTimeout(
	function() { rxTO = null; moreMessages(); },
	RX_TIMEOUT);
................................................................................
    // Now get initial messages to display
    moreMessages();
}

// Entry point for processing
function ourStart() {







    // We'll want to send notifications (if the user
    //  lets us)
    Notification.requestPermission(function(result) {
	if (result == "granted") {







	    console.log("Notifications OK");



	}
    });

    // Send our initial update request
    ourState = 1;
    curReq = new XMLHttpRequest();
    curReq.open("GET", "/config.json");

Changes to main.py.

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
..
56
57
58
59
60
61
62



63
64
65
66
67
68
69
..
78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
...
107
108
109
110
111
112
113



114
115
116
117
118
119
120
# main.py
#	Main driver for WWW-XMPP interface
#
import sys, threading, time
from user import User
from get import GET_mixin
from post import POST_mixin


from chore.authen import Authen_mixin, Authen_Server_mixin
import chore

# Seconds between search for timeouts
TIME_GRANULARITY = 5

# Web interface handling
class App_Handler(chore.handlers.Chore_Handler, GET_mixin,
	POST_mixin, Authen_mixin):
    def __init__(self, conn, tup, approot):
	self.title = "WebXMPP Messaging"

	chore.handlers.Chore_Handler.__init__(self, conn, tup, approot,
	    (GET_mixin.__init__,
	     POST_mixin.__init__,

	     Authen_mixin.__init__))

    # SMS postings are defended by IP checks, not authentication
    def handle_noauth(self):
	# This check is before options are parsed, so self.paths
	#  isn't available and we have to look directly at the
	#  specified path.
................................................................................
    chore.www.add_config(c)

    # Top-level numeric singletons
    for f in ("nmsg", "poll1"):
	c.ints.add( (f,) )
    for f in ("pollslop",):
	c.floats.add( (f,) )




    # Multiple of these, each with a list of args
    c.args.add( ("sms",) )
    c.mults.add( ("sms",) )

    # Multiple of these, with sub-config
    c.onearg.add( ("user",) )
................................................................................
    c.mults.add( ("user", "account", "sms") )

    # Now parse our input with this kind of config
    res = c.load_cfg(fn)
    return res

# Root of all Web/XMPP server state
class WebXMPP(chore.server.Server, Authen_Server_mixin):


    def __init__(self, cfg):

	Authen_Server_mixin.__init__(self)

	# Sanity--poll times
	if not cfg.get("poll1"):
................................................................................

	# Sanity--other required stuff
	if not cfg.get("nmsg"):
	    raise Exception, "# of messages to display not configured"

	# Chore level setup
	chore.server.Server.__init__(self, cfg, App_Handler)




	# Users who have been loaded
	self.users = {}

	# Our timeout service thread
	self.timer = None








>
>








|






>







 







>
>
>







 







|
>







 







>
>
>







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
..
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
..
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
...
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# main.py
#	Main driver for WWW-XMPP interface
#
import sys, threading, time
from user import User
from get import GET_mixin
from post import POST_mixin
from put import PUT_mixin
from fcm import FCM_mixin
from chore.authen import Authen_mixin, Authen_Server_mixin
import chore

# Seconds between search for timeouts
TIME_GRANULARITY = 5

# Web interface handling
class App_Handler(chore.handlers.Chore_Handler, GET_mixin,
	POST_mixin, PUT_mixin, Authen_mixin):
    def __init__(self, conn, tup, approot):
	self.title = "WebXMPP Messaging"

	chore.handlers.Chore_Handler.__init__(self, conn, tup, approot,
	    (GET_mixin.__init__,
	     POST_mixin.__init__,
	     PUT_mixin.__init__,
	     Authen_mixin.__init__))

    # SMS postings are defended by IP checks, not authentication
    def handle_noauth(self):
	# This check is before options are parsed, so self.paths
	#  isn't available and we have to look directly at the
	#  specified path.
................................................................................
    chore.www.add_config(c)

    # Top-level numeric singletons
    for f in ("nmsg", "poll1"):
	c.ints.add( (f,) )
    for f in ("pollslop",):
	c.floats.add( (f,) )

    # Messaging key from Google for Firebase Cloud Messaging
    c.onearg.add( ("fcmkey",) )

    # Multiple of these, each with a list of args
    c.args.add( ("sms",) )
    c.mults.add( ("sms",) )

    # Multiple of these, with sub-config
    c.onearg.add( ("user",) )
................................................................................
    c.mults.add( ("user", "account", "sms") )

    # Now parse our input with this kind of config
    res = c.load_cfg(fn)
    return res

# Root of all Web/XMPP server state
class WebXMPP(chore.server.Server, FCM_mixin,
	Authen_Server_mixin):

    def __init__(self, cfg):

	Authen_Server_mixin.__init__(self)

	# Sanity--poll times
	if not cfg.get("poll1"):
................................................................................

	# Sanity--other required stuff
	if not cfg.get("nmsg"):
	    raise Exception, "# of messages to display not configured"

	# Chore level setup
	chore.server.Server.__init__(self, cfg, App_Handler)

	# Hook for Google's stuff
	FCM_mixin.__init__(self)

	# Users who have been loaded
	self.users = {}

	# Our timeout service thread
	self.timer = None

Added put.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
#
# put.py
#	HTML PUT handling
# /user? - Update user status
#	[fg|bg] - Tab open?
#
import sys, json
import chore

class PUT_mixin(object):
    def __init__(self):
	self.dispatchers.append( ("PUT", self.put_user) )

    # Set various bits of /user
    #	bg/fg - Lost/gained tab focus, note Google API key
    def put_user(self, buf):
	# /user
	if not self.path_match("user"):
	    return False,None

	# Focus
	approot = self.server.approot
	u = approot.users.get(self.user)
	if u is None:
	    return False,None
	if "bg" in self.vals:
	    u.pushkeys.add(self.vals["bg"])
	elif "fg" in self.vals:
	    key = self.vals["fg"]
	    if key in u.pushkeys:
		u.pushkeys.remove(key)

	# Ok
	return True,self.send_result("", "text/html")

Changes to user.py.

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
...
121
122
123
124
125
126
127








# pending[] - List of clients waiting (Ajax) for new messages
# roster{} - Map from recipient to the Account they're under
# status{} - Map from account name to their status
#	(it was under the User directly, but you can get back
#	 presence messages before the user shows up in the roster)
# serial - Running counter, so clients can detect dup (from cache)
#	completions.

#
class User(object):
    def __init__(self, top, user, accounts):
	self.top = top
	self.name = user
	self.accounts = accounts
	self.activity = None
	self.timeout = None
	self.exclusion = Exclusion()
	self.active = {}
	self.roster = {}
	self.status = {}

	self.serial = 1

	# Initial generation of content;
	#  roster current, and
	#  generation of messages.
	self.rgen = self.gen = 1

................................................................................
	# Pending Ajax requests wake up now, clearing
	#  the list
	if self.pending:
	    pends = tuple(self.pending)
	    self.pending.clear()
	    for tup in pends:
		tup[1].release()















>












>







 







>
>
>
>
>
>
>
>
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
...
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# pending[] - List of clients waiting (Ajax) for new messages
# roster{} - Map from recipient to the Account they're under
# status{} - Map from account name to their status
#	(it was under the User directly, but you can get back
#	 presence messages before the user shows up in the roster)
# serial - Running counter, so clients can detect dup (from cache)
#	completions.
# pushkeys[] - Set of active Google messaging endpoints
#
class User(object):
    def __init__(self, top, user, accounts):
	self.top = top
	self.name = user
	self.accounts = accounts
	self.activity = None
	self.timeout = None
	self.exclusion = Exclusion()
	self.active = {}
	self.roster = {}
	self.status = {}
	self.pushkeys = set()
	self.serial = 1

	# Initial generation of content;
	#  roster current, and
	#  generation of messages.
	self.rgen = self.gen = 1

................................................................................
	# Pending Ajax requests wake up now, clearing
	#  the list
	if self.pending:
	    pends = tuple(self.pending)
	    self.pending.clear()
	    for tup in pends:
		tup[1].release()

	# Pending Google Chrome browsers
	if self.pushkeys:
	    # Grab current list and reset
	    keys = self.pushkeys
	    self.pushkeys = set()
	    for k in keys:
		self.top.fcm(k, them, mBody)