Merge lp:~ralsina/ubuntu-push/merge-automatic into lp:ubuntu-push

Proposed by Roberto Alsina
Status: Merged
Approved by: Roberto Alsina
Approved revision: no longer in the source branch.
Merged at revision: 129
Proposed branch: lp:~ralsina/ubuntu-push/merge-automatic
Merge into: lp:ubuntu-push
Diff against target: 3493 lines (+2572/-506)
43 files modified
PACKAGE_DEPS (+1/-0)
accounts/accounts.go (+42/-0)
accounts/caccounts.go (+28/-0)
bus/endpoint.go (+11/-1)
client/client.go (+16/-2)
client/client_test.go (+21/-5)
debian/changelog (+13/-0)
debian/control (+1/-0)
debian/ubuntu-push-client.conf (+4/-0)
docs/_common.txt (+186/-0)
docs/_description.txt (+46/-0)
docs/example-client/Makefile (+22/-0)
docs/example-client/README (+28/-0)
docs/example-client/components/ChatClient.qml (+99/-0)
docs/example-client/hello.desktop (+8/-0)
docs/example-client/hello.json (+7/-0)
docs/example-client/helloHelper (+7/-0)
docs/example-client/helloHelper-apparmor.json (+6/-0)
docs/example-client/helloHelper.json (+3/-0)
docs/example-client/main.qml (+266/-0)
docs/example-client/manifest.json (+19/-0)
docs/example-client/push-example.qmlproject (+52/-0)
docs/example-client/tests/autopilot/push-example/__init__.py (+72/-0)
docs/example-client/tests/autopilot/push-example/test_main.py (+25/-0)
docs/example-client/tests/autopilot/run (+12/-0)
docs/example-client/tests/unit/tst_hellocomponent.qml (+50/-0)
docs/example-server/.bzrignore (+1/-0)
docs/example-server/.gitignore (+3/-0)
docs/example-server/app.js (+212/-0)
docs/example-server/config/config.js (+15/-0)
docs/example-server/index.html (+9/-0)
docs/example-server/lib/inbox.js (+47/-0)
docs/example-server/lib/notifier.js (+89/-0)
docs/example-server/lib/registry.js (+62/-0)
docs/example-server/notify-form.html (+14/-0)
docs/example-server/package.json (+25/-0)
docs/example-server/server.js (+29/-0)
docs/example-server/test/app_test.js (+551/-0)
docs/example-server/test/inbox_test.js (+79/-0)
docs/example-server/test/notifier_test.js (+192/-0)
docs/example-server/test/registry_test.js (+179/-0)
docs/highlevel.txt (+11/-251)
docs/lowlevel.txt (+9/-247)
To merge this branch: bzr merge lp:~ralsina/ubuntu-push/merge-automatic
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Review via email: mp+233755@code.launchpad.net

Commit message

Latest changes from the automatic branch.

Description of the change

Latest changes from the automatic branch.

To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) :
review: Approve
129. By Roberto Alsina

Latest changes from the automatic branch.
Approved by: Roberto Alsina

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'PACKAGE_DEPS'
2--- PACKAGE_DEPS 2014-08-21 20:26:36 +0000
3+++ PACKAGE_DEPS 2014-09-08 16:59:38 +0000
4@@ -11,3 +11,4 @@
5 libubuntu-app-launch2-dev
6 libclick-0.4-dev
7 liburl-dispatcher1-dev
8+libaccounts-glib-dev
9
10=== added directory 'accounts'
11=== added file 'accounts/accounts.go'
12--- accounts/accounts.go 1970-01-01 00:00:00 +0000
13+++ accounts/accounts.go 2014-09-08 16:59:38 +0000
14@@ -0,0 +1,42 @@
15+/*
16+ Copyright 2014 Canonical Ltd.
17+
18+ This program is free software: you can redistribute it and/or modify it
19+ under the terms of the GNU General Public License version 3, as published
20+ by the Free Software Foundation.
21+
22+ This program is distributed in the hope that it will be useful, but
23+ WITHOUT ANY WARRANTY; without even the implied warranties of
24+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
25+ PURPOSE. See the GNU General Public License for more details.
26+
27+ You should have received a copy of the GNU General Public License along
28+ with this program. If not, see <http://www.gnu.org/licenses/>.
29+*/
30+
31+// Package accounts wraps libaccounts
32+package accounts
33+
34+/*
35+#cgo pkg-config: libaccounts-glib
36+
37+void start();
38+
39+*/
40+import "C"
41+
42+type Changed struct{}
43+
44+var ch chan Changed
45+
46+//export gocb
47+func gocb() {
48+ ch <- Changed{}
49+}
50+
51+func Watch() <-chan Changed {
52+ ch = make(chan Changed, 1)
53+ C.start()
54+
55+ return ch
56+}
57
58=== added file 'accounts/caccounts.go'
59--- accounts/caccounts.go 1970-01-01 00:00:00 +0000
60+++ accounts/caccounts.go 2014-09-08 16:59:38 +0000
61@@ -0,0 +1,28 @@
62+package accounts
63+
64+/*
65+#include <libaccounts-glib/accounts-glib.h>
66+
67+static void cb(AgManager *manager, AgAccountId account_id, gpointer p) {
68+ AgAccount *account = ag_manager_get_account(manager, account_id);
69+ if (!account) {
70+ return;
71+ }
72+ GList *services = ag_account_list_services(account);
73+ if (!services || !services->data) {
74+ return;
75+ }
76+
77+ gocb();
78+}
79+
80+void start() {
81+ AgManager *manager = ag_manager_new_for_service_type("ubuntuone");
82+ g_signal_connect(manager, "account-created", G_CALLBACK(cb), NULL);
83+ g_signal_connect(manager, "account-deleted", G_CALLBACK(cb), NULL);
84+ g_signal_connect(manager, "account-updated", G_CALLBACK(cb), NULL);
85+
86+}
87+
88+*/
89+import "C"
90
91=== modified file 'bus/endpoint.go'
92--- bus/endpoint.go 2014-07-04 23:00:42 +0000
93+++ bus/endpoint.go 2014-09-08 16:59:38 +0000
94@@ -19,6 +19,7 @@
95 // Here we define the Endpoint, which represents the DBus connection itself.
96
97 import (
98+ "encoding/base64"
99 "errors"
100 "fmt"
101
102@@ -268,7 +269,16 @@
103 reply = dbus.NewErrorMessage(msg, err_iface, err.Error())
104 endp.log.Errorf("WatchMethod: %s(%v, %#v, %#v) failure: %#v", msg.Member, msg.Path, args, extra, err)
105 } else {
106- endp.log.Debugf("WatchMethod: %s(%v, %#v, %#v) success: %#v", msg.Member, msg.Path, args, extra, rvals)
107+ var san_rvals []string
108+ for _, element := range rvals {
109+ sane := fmt.Sprintf("%v", element)
110+ _, err := base64.StdEncoding.DecodeString(sane)
111+ if err == nil {
112+ sane = "LooksLikeAToken=="
113+ }
114+ san_rvals = append(san_rvals, sane)
115+ }
116+ endp.log.Debugf("WatchMethod: %s(%v, %#v, %#v) success: %#v", msg.Member, msg.Path, args, extra, san_rvals)
117 reply = dbus.NewMethodReturnMessage(msg)
118 err = reply.AppendArgs(rvals...)
119 if err != nil {
120
121=== modified file 'client/client.go'
122--- client/client.go 2014-08-24 13:09:01 +0000
123+++ client/client.go 2014-09-08 16:59:38 +0000
124@@ -32,6 +32,7 @@
125 "os/exec"
126 "strings"
127
128+ "launchpad.net/ubuntu-push/accounts"
129 "launchpad.net/ubuntu-push/bus"
130 "launchpad.net/ubuntu-push/bus/connectivity"
131 "launchpad.net/ubuntu-push/bus/networkmanager"
132@@ -123,6 +124,7 @@
133 trackAddressees map[string]*click.AppId
134 installedChecker click.InstalledChecker
135 poller poller.Poller
136+ accountsCh <-chan accounts.Changed
137 }
138
139 // Creates a new Ubuntu Push Notifications client-side daemon that will use
140@@ -175,6 +177,7 @@
141
142 client.connCh = make(chan bool, 1)
143 client.sessionConnectedCh = make(chan uint32, 1)
144+ client.accountsCh = accounts.Watch()
145
146 if client.config.CertPEMFile != "" {
147 client.pem, err = ioutil.ReadFile(client.config.CertPEMFile)
148@@ -464,10 +467,19 @@
149 return nil
150 }
151
152+// handleAccountsChange deals with the user adding or removing (or
153+// changing) the u1 account used to auth
154+func (client *PushClient) handleAccountsChange() {
155+ client.log.Infof("U1 account changed; restarting session")
156+ client.session.Close()
157+}
158+
159 // doLoop connects events with their handlers
160-func (client *PushClient) doLoop(connhandler func(bool), bcasthandler func(*session.BroadcastNotification) error, ucasthandler func(session.AddressedNotification) error, errhandler func(error), unregisterhandler func(*click.AppId)) {
161+func (client *PushClient) doLoop(connhandler func(bool), bcasthandler func(*session.BroadcastNotification) error, ucasthandler func(session.AddressedNotification) error, errhandler func(error), unregisterhandler func(*click.AppId), accountshandler func()) {
162 for {
163 select {
164+ case <-client.accountsCh:
165+ accountshandler()
166 case state := <-client.connCh:
167 connhandler(state)
168 case bcast := <-client.session.BroadcastCh:
169@@ -501,7 +513,9 @@
170 client.handleBroadcastNotification,
171 client.handleUnicastNotification,
172 client.handleErr,
173- client.handleUnregister)
174+ client.handleUnregister,
175+ client.handleAccountsChange,
176+ )
177 }
178
179 func (client *PushClient) setupPushService() error {
180
181=== modified file 'client/client_test.go'
182--- client/client_test.go 2014-08-24 13:09:01 +0000
183+++ client/client_test.go 2014-09-08 16:59:38 +0000
184@@ -32,6 +32,7 @@
185
186 . "launchpad.net/gocheck"
187
188+ "launchpad.net/ubuntu-push/accounts"
189 "launchpad.net/ubuntu-push/bus"
190 "launchpad.net/ubuntu-push/bus/networkmanager"
191 "launchpad.net/ubuntu-push/bus/systemimage"
192@@ -994,6 +995,7 @@
193 var nopUcast = func(session.AddressedNotification) error { return nil }
194 var nopError = func(error) {}
195 var nopUnregister = func(*click.AppId) {}
196+var nopAcct = func() {}
197
198 func (cs *clientSuite) TestDoLoopConn(c *C) {
199 cli := NewPushClient(cs.configPath, cs.leveldbPath)
200@@ -1004,7 +1006,7 @@
201 c.Assert(cli.initSessionAndPoller(), IsNil)
202
203 ch := make(chan bool, 1)
204- go cli.doLoop(func(bool) { ch <- true }, nopBcast, nopUcast, nopError, nopUnregister)
205+ go cli.doLoop(func(bool) { ch <- true }, nopBcast, nopUcast, nopError, nopUnregister, nopAcct)
206 c.Check(takeNextBool(ch), Equals, true)
207 }
208
209@@ -1017,7 +1019,7 @@
210 cli.session.BroadcastCh <- &session.BroadcastNotification{}
211
212 ch := make(chan bool, 1)
213- go cli.doLoop(nopConn, func(_ *session.BroadcastNotification) error { ch <- true; return nil }, nopUcast, nopError, nopUnregister)
214+ go cli.doLoop(nopConn, func(_ *session.BroadcastNotification) error { ch <- true; return nil }, nopUcast, nopError, nopUnregister, nopAcct)
215 c.Check(takeNextBool(ch), Equals, true)
216 }
217
218@@ -1030,7 +1032,7 @@
219 cli.session.NotificationsCh <- session.AddressedNotification{}
220
221 ch := make(chan bool, 1)
222- go cli.doLoop(nopConn, nopBcast, func(session.AddressedNotification) error { ch <- true; return nil }, nopError, nopUnregister)
223+ go cli.doLoop(nopConn, nopBcast, func(session.AddressedNotification) error { ch <- true; return nil }, nopError, nopUnregister, nopAcct)
224 c.Check(takeNextBool(ch), Equals, true)
225 }
226
227@@ -1043,7 +1045,7 @@
228 cli.session.ErrCh <- nil
229
230 ch := make(chan bool, 1)
231- go cli.doLoop(nopConn, nopBcast, nopUcast, func(error) { ch <- true }, nopUnregister)
232+ go cli.doLoop(nopConn, nopBcast, nopUcast, func(error) { ch <- true }, nopUnregister, nopAcct)
233 c.Check(takeNextBool(ch), Equals, true)
234 }
235
236@@ -1056,7 +1058,21 @@
237 cli.unregisterCh <- app1
238
239 ch := make(chan bool, 1)
240- go cli.doLoop(nopConn, nopBcast, nopUcast, nopError, func(app *click.AppId) { c.Check(app.Original(), Equals, appId1); ch <- true })
241+ go cli.doLoop(nopConn, nopBcast, nopUcast, nopError, func(app *click.AppId) { c.Check(app.Original(), Equals, appId1); ch <- true }, nopAcct)
242+ c.Check(takeNextBool(ch), Equals, true)
243+}
244+
245+func (cs *clientSuite) TestDoLoopAcct(c *C) {
246+ cli := NewPushClient(cs.configPath, cs.leveldbPath)
247+ cli.log = cs.log
248+ cli.systemImageInfo = siInfoRes
249+ c.Assert(cli.initSessionAndPoller(), IsNil)
250+ acctCh := make(chan accounts.Changed, 1)
251+ acctCh <- accounts.Changed{}
252+ cli.accountsCh = acctCh
253+
254+ ch := make(chan bool, 1)
255+ go cli.doLoop(nopConn, nopBcast, nopUcast, nopError, nopUnregister, func() { ch <- true })
256 c.Check(takeNextBool(ch), Equals, true)
257 }
258
259
260=== modified file 'debian/changelog'
261--- debian/changelog 2014-09-02 17:28:19 +0000
262+++ debian/changelog 2014-09-08 16:59:38 +0000
263@@ -1,3 +1,16 @@
264+ubuntu-push (0.64) UNRELEASED; urgency=medium
265+
266+ [ Roberto Alsina ]
267+ * Remove tokens from debug output
268+ * Doc updates
269+ * Included example code in docs directory
270+
271+ [ John R. Lenton]
272+ * Use libaccounts to track changes to the u1 account used for auth; restart the session on change.
273+ * Set the MEDIA_PROP env var to select the right media role for notification sounds
274+
275+ -- Roberto Alsina <ralsina@yoga> Mon, 08 Sep 2014 13:25:33 -0300
276+
277 ubuntu-push (0.63.2+14.10.20140902.1-0ubuntu1) utopic; urgency=medium
278
279 [ Roberto Alsina ]
280
281=== modified file 'debian/control'
282--- debian/control 2014-08-21 19:48:06 +0000
283+++ debian/control 2014-09-08 16:59:38 +0000
284@@ -21,6 +21,7 @@
285 libnih-dbus-dev,
286 libclick-0.4-dev,
287 liburl-dispatcher1-dev,
288+ libaccounts-glib-dev,
289 cmake,
290 python3,
291 Standards-Version: 3.9.5
292
293=== modified file 'debian/ubuntu-push-client.conf'
294--- debian/ubuntu-push-client.conf 2014-08-06 14:08:29 +0000
295+++ debian/ubuntu-push-client.conf 2014-09-08 16:59:38 +0000
296@@ -3,6 +3,10 @@
297 start on started unity8
298 stop on stopping unity8
299
300+# set the media role for sounds to notifications' role
301+env PULSE_PROP="media.role=alert"
302+export PULSE_PROP
303+
304 exec /usr/lib/ubuntu-push-client/ubuntu-push-client
305 respawn
306
307
308=== added file 'docs/_common.txt'
309--- docs/_common.txt 1970-01-01 00:00:00 +0000
310+++ docs/_common.txt 2014-09-08 16:59:38 +0000
311@@ -0,0 +1,186 @@
312+Application Helpers
313+-------------------
314+
315+The payload delivered to push-client will be passed onto a helper program that can modify it as needed before passing it onto
316+the postal service (see `Helper Output Format <#helper-output-format>`__).
317+
318+The helper receives two arguments ``infile`` and ``outfile``. The message is delivered via ``infile`` and the transformed
319+version is placed in ``outfile``.
320+
321+This is the simplest possible useful helper, which simply passes the message through unchanged::
322+
323+ #!/usr/bin/python3
324+
325+ import sys
326+ f1, f2 = sys.argv[1:3]
327+ open(f2, "w").write(open(f1).read())
328+
329+Helpers need to be added to the click package manifest::
330+
331+ {
332+ "name": "com.ubuntu.developer.ralsina.hello",
333+ "description": "description of hello",
334+ "framework": "ubuntu-sdk-14.10-qml-dev2",
335+ "architecture": "all",
336+ "title": "hello",
337+ "hooks": {
338+ "hello": {
339+ "apparmor": "hello.json",
340+ "desktop": "hello.desktop"
341+ },
342+ "helloHelper": {
343+ "apparmor": "helloHelper-apparmor.json",
344+ "push-helper": "helloHelper.json"
345+ }
346+ },
347+ "version": "0.2",
348+ "maintainer": "Roberto Alsina <roberto.alsina@canonical.com>"
349+ }
350+
351+Here, we created a helloHelper entry in hooks that has an apparmor profile and an additional JSON file for the push-helper hook.
352+
353+helloHelper-apparmor.json must contain **only** the push-notification-client policy group::
354+
355+ {
356+ "policy_groups": [
357+ "push-notification-client"
358+ ],
359+ "policy_version": 1.2
360+ }
361+
362+And helloHelper.json must have at least a exec key with the path to the helper executable relative to the json, and optionally
363+an app_id key containing the short id of one of the apps in the package (in the format packagename_appname without a version).
364+If the app_id is not specified, the helper will be used for all apps in the package::
365+
366+ {
367+ "exec": "helloHelper",
368+ "app_id": "com.ubuntu.developer.ralsina.hello_hello"
369+ }
370+
371+.. note:: For deb packages, helpers should be installed into /usr/lib/ubuntu-push-client/legacy-helpers/ as part of the package.
372+
373+Helper Output Format
374+--------------------
375+
376+Helpers output has two parts, the postal message (in the "message" key) and a notification to be presented to the user (in the "notification" key).
377+
378+.. note:: This format **will** change with future versions of the SDK and it **may** be incompatible.
379+
380+Here's a simple example::
381+
382+ {
383+ "message": "foobar",
384+ "notification": {
385+ "tag": "foo",
386+ "card": {
387+ "summary": "yes",
388+ "body": "hello",
389+ "popup": true,
390+ "persist": true,
391+ "timestamp": 1407160197
392+ }
393+ "sound": "buzz.mp3",
394+ "vibrate": {
395+ "pattern": [200, 100],
396+ "repeat": 2
397+ }
398+ "emblem-counter": {
399+ "count": 12,
400+ "visible": true
401+ }
402+ }
403+ }
404+
405+The notification can contain a **tag** field, which can later be used by the `persistent notification management API. <#persistent-notification-management>`__
406+
407+:message: (optional) A JSON object that is passed as-is to the application via PopAll.
408+:notification: (optional) Describes the user-facing notifications triggered by this push message.
409+
410+The notification can contain a **card**. A card describes a specific notification to be given to the user,
411+and has the following fields:
412+
413+:summary: (required) a title. The card will not be presented if this is missing.
414+:body: longer text, defaults to empty.
415+:actions: If empty (the default), a bubble notification is non-clickable.
416+ If you add a URL, then bubble notifications are clickable and launch that URL. One use for this is using a URL like
417+ ``appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version`` which will switch to the app or launch
418+ it if it's not running. See `URLDispatcher <https://wiki.ubuntu.com/URLDispatcher>`__ for more information.
419+
420+:icon: An icon relating to the event being notified. Defaults to empty (no icon);
421+ a secondary icon relating to the application will be shown as well, regardless of this field.
422+:timestamp: Seconds since the unix epoch, only used for persist (for now). If zero or unset, defaults to current timestamp.
423+:persist: Whether to show in notification centre; defaults to false
424+:popup: Whether to show in a bubble. Users can disable this, and can easily miss them, so don't rely on it exclusively. Defaults to false.
425+
426+.. note:: Keep in mind that the precise way in which each field is presented to the user depends on factors such as
427+ whether it's shown as a bubble or in the notification centre, or even the version of Ubuntu Touch the user
428+ has on their device.
429+
430+The notification can contain a **sound** field. This is either a boolean (play a predetermined sound) or the path to a sound file. The user can disable it, so don't rely on it exclusively.
431+Defaults to empty (no sound). The path is relative, and will be looked up in (a) the application's .local/share/<pkgname>, and (b)
432+standard xdg dirs.
433+
434+The notification can contain a **vibrate** field, causing haptic feedback, which can be either a boolean (if true, vibrate a predetermined way) or an object that has the following content:
435+
436+:pattern: a list of integers describing a vibration pattern (duration of alternating vibration/no vibration times, in milliseconds).
437+:repeat: number of times the pattern has to be repeated (defaults to 1, 0 is the same as 1).
438+
439+The notification can contain a **emblem-counter** field, with the following content:
440+
441+:count: a number to be displayed over the application's icon in the launcher.
442+:visible: set to true to show the counter, or false to hide it.
443+
444+.. note:: Unlike other notifications, emblem-counter needs to be cleaned by the app itself.
445+ Please see `the persistent notification management section. <#persistent-notification-management>`__
446+
447+.. FIXME crosslink to hello example app on each method
448+
449+Security
450+~~~~~~~~
451+
452+To use the push API, applications need to request permission in their security profile, using something like this::
453+
454+ {
455+ "policy_groups": [
456+ "networking",
457+ "push-notification-client"
458+ ],
459+ "policy_version": 1.2
460+ }
461+
462+
463+Ubuntu Push Server API
464+----------------------
465+
466+The Ubuntu Push server is located at https://push.ubuntu.com and has a single endpoint: ``/notify``.
467+To notify a user, your application has to do a POST with ``Content-type: application/json``.
468+
469+.. note:: The contents of the data field are arbitrary. They should be enough for your helper to build
470+ a notification using it, and decide whether it should be displayed or not. Keep in mind
471+ that this will be processed by more than one version of the helper, because the user may be using
472+ an older version of your app.
473+
474+Here is an example of the POST body using all the fields::
475+
476+ {
477+ "appid": "com.ubuntu.music_music",
478+ "expire_on": "2014-10-08T14:48:00.000Z",
479+ "token": "LeA4tRQG9hhEkuhngdouoA==",
480+ "clear_pending": true,
481+ "replace_tag": "tagname",
482+ "data": {
483+ "id": 43578,
484+ "timestamp": 1409583746,
485+ "serial": 1254,
486+ "sender": "Joe",
487+ "snippet": "Hi there!"
488+ }
489+ }
490+
491+
492+:appid: ID of the application that will receive the notification, as described in the client side documentation.
493+:expire_on: Expiration date/time for this message, in `ISO8601 Extendend format <http://en.wikipedia.org/wiki/ISO_8601>`__
494+:token: The token identifying the user+device to which the message is directed, as described in the client side documentation.
495+:clear_pending: Discards all previous pending notifications. Usually in response to getting a "too-many-pending" error.
496+:replace_tag: If there's a pending notification with the same tag, delete it before queuing this new one.
497+:data: A JSON object.
498
499=== added file 'docs/_description.txt'
500--- docs/_description.txt 1970-01-01 00:00:00 +0000
501+++ docs/_description.txt 2014-09-08 16:59:38 +0000
502@@ -0,0 +1,46 @@
503+Let's describe the push system by way of an example.
504+
505+Alice has written a chat application called Chatter. Using it, Bob can send messages to Carol and viceversa. Alice has a
506+web application for it, so the way it works now is that Bob connects to the service, posts a message, and when Carol
507+connects, she gets it. If Carol leaves the browser window open, it beeps when messages arrive.
508+
509+Now Alice wants to create an Ubuntu Touch app for Chatter, so she implements the same architecture using a client that
510+does the same thing as the web browser. Sadly, since applications on Ubuntu Touch don't run continuously, messages are
511+only delivered when Carol opens the app, and the user experience suffers.
512+
513+Using the Ubuntu Push Server, this problem is alleviated: the Chatter server will deliver the messages to the Ubuntu
514+Push Server, which in turn will send it in an efficient manner to the Ubuntu Push Client running in Bob and Carol's
515+devices. The user sees a notification (all without starting the app) and then can launch it if he's interested in
516+reading messages at that point.
517+
518+Since the app is not started and messages are delivered oportunistically, this is both battery and bandwidth-efficient.
519+
520+.. figure:: push.svg
521+
522+The Ubuntu Push system provides:
523+
524+* A push server which receives **push messages** from the app servers, queues them and delivers them efficiently
525+ to the devices.
526+* A push client which receives those messages, queues messages to the app and displays notifications to the user
527+
528+The full lifecycle of a push message is:
529+
530+* Created in a application-specific server
531+* Sent to the Ubuntu Push server, targeted at a user or user+device pair
532+* Delivered to one or more Ubuntu devices
533+* Passed through the application helper for processing
534+* Notification displayed to the user (via different mechanisms)
535+* Application Message queued for the app's use
536+
537+If the user interacts with the notification, the application is launched and should check its queue for messages
538+it has to process.
539+
540+For the app developer, there are several components needed:
541+
542+* A server that sends the **push messages** to the Ubuntu Push server
543+* Support in the client app for registering with the Ubuntu Push client
544+* Support in the client app to react to **notifications** displayed to the user and process **application messages**
545+* A helper program with application-specific knowledge that transforms **push messages** as needed.
546+
547+In the following sections, we'll see how to implement all the client side parts. For the application server, see the
548+`Ubuntu Push Server API section <#ubuntu-push-server-api>`__
549
550=== added directory 'docs/example-client'
551=== added file 'docs/example-client/.excludes'
552=== added file 'docs/example-client/Makefile'
553--- docs/example-client/Makefile 1970-01-01 00:00:00 +0000
554+++ docs/example-client/Makefile 2014-09-08 16:59:38 +0000
555@@ -0,0 +1,22 @@
556+# More information: https://wiki.ubuntu.com/Touch/Testing
557+#
558+# Notes for autopilot tests:
559+# -----------------------------------------------------------
560+# In order to run autopilot tests:
561+# sudo apt-add-repository ppa:autopilot/ppa
562+# sudo apt-get update
563+# sudo apt-get install python-autopilot autopilot-qt
564+#############################################################
565+
566+all:
567+
568+autopilot:
569+ chmod +x tests/autopilot/run
570+ tests/autopilot/run
571+
572+check:
573+ qmltestrunner -input tests/unit
574+
575+run:
576+ /usr/bin/qmlscene $@ push-example.qml
577+
578
579=== added file 'docs/example-client/README'
580--- docs/example-client/README 1970-01-01 00:00:00 +0000
581+++ docs/example-client/README 2014-09-08 16:59:38 +0000
582@@ -0,0 +1,28 @@
583+Example App for the QML notifications API. This is an example application
584+showing how to use push notifications in Ubuntu Touch devices.
585+
586+= Running on Ubuntu Touch =
587+
588+Since push is currently only meant for Ubuntu Touch devices, this is meant
589+to be used in the emulator or on a real device.
590+
591+* Open the example project in Ubuntu-SDK
592+* Build a click file
593+* Run in the emulator or device
594+
595+= Running on the desktop =
596+
597+This is more complicated but may be convenient while experimenting:
598+
599+* Install qtdeclarative5-ubuntu-push-notifications-plugin
600+* Install ubuntu-push-client
601+* Run ubuntu-push-client in trivial helper mode:
602+
603+ UBUNTU_PUSH_USE_TRIVIAL_HELPER=1 ./ubuntu-push-client
604+
605+* Build click package
606+* Install in your desktop:
607+
608+ sudo click install --all-users com.ubuntu.developer.push.ubuntu-push-example_0.1_all.click
609+
610+* Run example app from the SDK using the "Desktop" kit
611
612=== added directory 'docs/example-client/components'
613=== added file 'docs/example-client/components/ChatClient.qml'
614--- docs/example-client/components/ChatClient.qml 1970-01-01 00:00:00 +0000
615+++ docs/example-client/components/ChatClient.qml 2014-09-08 16:59:38 +0000
616@@ -0,0 +1,99 @@
617+import QtQuick 2.0
618+import Ubuntu.Components 0.1
619+
620+Item {
621+ property string nick
622+ property string token
623+ property bool registered: false
624+ signal error (string msg)
625+ onNickChanged: {
626+ if (nick) {
627+ register()
628+ } else {
629+ registered = false
630+ }
631+ }
632+ onTokenChanged: {register()}
633+ function register() {
634+ console.log("registering ", nick, token);
635+ if (nick && token) {
636+ var req = new XMLHttpRequest();
637+ req.open("post", "http://direct.ralsina.me:8001/register", true);
638+ req.setRequestHeader("Content-type", "application/json");
639+ req.onreadystatechange = function() {//Call a function when the state changes.
640+ if(req.readyState == 4) {
641+ if (req.status == 200) {
642+ registered = true;
643+ } else {
644+ error(JSON.parse(req.responseText)["error"]);
645+ }
646+ }
647+ }
648+ req.send(JSON.stringify({
649+ "nick" : nick.toLowerCase(),
650+ "token": token
651+ }))
652+ }
653+ }
654+
655+ /* options is of the form:
656+ {
657+ enabled: false,
658+ persist: false,
659+ popup: false,
660+ sound: "buzz.mp3",
661+ vibrate: false,
662+ counter: 5
663+ }
664+ */
665+ function sendMessage(message, options) {
666+ var to_nick = message["to"]
667+ var data = {
668+ "from_nick": nick.toLowerCase(),
669+ "from_token": token,
670+ "nick": to_nick.toLowerCase(),
671+ "data": {
672+ "message": message,
673+ "notification": {}
674+ }
675+ }
676+ if (options["enabled"]) {
677+ data["data"]["notification"] = {
678+ "card": {
679+ "summary": nick + " says: " + message["message"],
680+ "body": "",
681+ "popup": options["popup"],
682+ "persist": options["persist"],
683+ "actions": ["appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version"]
684+ }
685+ }
686+ if (options["sound"]) {
687+ data["data"]["notification"]["sound"] = options["sound"]
688+ }
689+ if (options["vibrate"]) {
690+ data["data"]["notification"]["vibrate"] = {
691+ "duration": 200
692+ }
693+ }
694+ if (options["counter"]) {
695+ data["data"]["notification"]["emblem-counter"] = {
696+ "count": Math.floor(options["counter"]),
697+ "visible": true
698+ }
699+ }
700+ }
701+ var req = new XMLHttpRequest();
702+ req.open("post", "http://direct.ralsina.me:8001/message", true);
703+ req.setRequestHeader("Content-type", "application/json");
704+ req.onreadystatechange = function() {//Call a function when the state changes.
705+ if(req.readyState == 4) {
706+ if (req.status == 200) {
707+ registered = true;
708+ } else {
709+ error(JSON.parse(req.responseText)["error"]);
710+ }
711+ }
712+ }
713+ req.send(JSON.stringify(data))
714+ }
715+}
716
717=== added file 'docs/example-client/hello.desktop'
718--- docs/example-client/hello.desktop 1970-01-01 00:00:00 +0000
719+++ docs/example-client/hello.desktop 2014-09-08 16:59:38 +0000
720@@ -0,0 +1,8 @@
721+[Desktop Entry]
722+Name=hello
723+Exec=qmlscene $@ main.qml
724+Icon=push-example.png
725+Terminal=false
726+Type=Application
727+X-Ubuntu-Touch=true
728+
729
730=== added file 'docs/example-client/hello.json'
731--- docs/example-client/hello.json 1970-01-01 00:00:00 +0000
732+++ docs/example-client/hello.json 2014-09-08 16:59:38 +0000
733@@ -0,0 +1,7 @@
734+{
735+ "policy_groups": [
736+ "networking",
737+ "push-notification-client"
738+ ],
739+ "policy_version": 1.2
740+}
741\ No newline at end of file
742
743=== added file 'docs/example-client/helloHelper'
744--- docs/example-client/helloHelper 1970-01-01 00:00:00 +0000
745+++ docs/example-client/helloHelper 2014-09-08 16:59:38 +0000
746@@ -0,0 +1,7 @@
747+#!/usr/bin/python3
748+
749+import sys
750+
751+f1, f2 = sys.argv[1:3]
752+
753+open(f2, "w").write(open(f1).read())
754
755=== added file 'docs/example-client/helloHelper-apparmor.json'
756--- docs/example-client/helloHelper-apparmor.json 1970-01-01 00:00:00 +0000
757+++ docs/example-client/helloHelper-apparmor.json 2014-09-08 16:59:38 +0000
758@@ -0,0 +1,6 @@
759+{
760+ "policy_groups": [
761+ "push-notification-client"
762+ ],
763+ "policy_version": 1.2
764+}
765
766=== added file 'docs/example-client/helloHelper.json'
767--- docs/example-client/helloHelper.json 1970-01-01 00:00:00 +0000
768+++ docs/example-client/helloHelper.json 2014-09-08 16:59:38 +0000
769@@ -0,0 +1,3 @@
770+{
771+ "exec": "helloHelper"
772+}
773
774=== added file 'docs/example-client/main.qml'
775--- docs/example-client/main.qml 1970-01-01 00:00:00 +0000
776+++ docs/example-client/main.qml 2014-09-08 16:59:38 +0000
777@@ -0,0 +1,266 @@
778+import QtQuick 2.0
779+import Qt.labs.settings 1.0
780+import Ubuntu.Components 0.1
781+import Ubuntu.Components.ListItems 0.1 as ListItem
782+import Ubuntu.PushNotifications 0.1
783+import "components"
784+
785+MainView {
786+ id: "mainView"
787+ // objectName for functional testing purposes (autopilot-qt5)
788+ objectName: "mainView"
789+
790+ // Note! applicationName needs to match the "name" field of the click manifest
791+ applicationName: "com.ubuntu.developer.ralsina.hello"
792+
793+ automaticOrientation: true
794+ useDeprecatedToolbar: false
795+
796+ width: units.gu(100)
797+ height: units.gu(75)
798+
799+ Settings {
800+ property alias nick: chatClient.nick
801+ property alias nickText: nickEdit.text
802+ property alias nickPlaceholder: nickEdit.placeholderText
803+ property alias nickEnabled: nickEdit.enabled
804+ }
805+
806+ ChatClient {
807+ id: chatClient
808+ onRegisteredChanged: {nickEdit.registered()}
809+ onError: {messageList.handle_error(msg)}
810+ token: pushClient.token
811+ }
812+
813+ PushClient {
814+ id: pushClient
815+ Component.onCompleted: {
816+ notificationsChanged.connect(messageList.handle_notifications)
817+ error.connect(messageList.handle_error)
818+ }
819+ appId: "com.ubuntu.developer.ralsina.hello_hello"
820+ }
821+
822+ TextField {
823+ id: nickEdit
824+ focus: true
825+ placeholderText: "Your nickname"
826+ anchors.left: parent.left
827+ anchors.right: loginButton.left
828+ anchors.top: parent.top
829+ anchors.leftMargin: units.gu(.5)
830+ anchors.rightMargin: units.gu(1)
831+ anchors.topMargin: units.gu(.5)
832+ function registered() {
833+ readOnly = true
834+ text = "Your nick is " + chatClient.nick
835+ messageEdit.focus = true
836+ messageEdit.enabled = true
837+ loginButton.text = "Logout"
838+ }
839+ onAccepted: { loginButton.clicked() }
840+ }
841+
842+ Button {
843+ id: loginButton
844+ text: chatClient.rgistered? "Logout": "Login"
845+ anchors.top: nickEdit.top
846+ anchors.right: parent.right
847+ anchors.rightMargin: units.gu(.5)
848+ onClicked: {
849+ if (chatClient.nick) { // logout
850+ chatClient.nick = ""
851+ text = "Login"
852+ nickEdit.enabled = true
853+ nickEdit.readOnly = false
854+ nickEdit.text = ""
855+ nickEdit.focus = true
856+ messageEdit.enabled = false
857+ } else { // login
858+ chatClient.nick = nickEdit.text
859+ }
860+ }
861+ }
862+
863+ TextField {
864+ id: messageEdit
865+ anchors.right: parent.right
866+ anchors.left: parent.left
867+ anchors.top: nickEdit.bottom
868+ anchors.topMargin: units.gu(1)
869+ anchors.rightMargin: units.gu(.5)
870+ anchors.leftMargin: units.gu(.5)
871+ placeholderText: "Your message"
872+ enabled: false
873+ onAccepted: {
874+ console.log("sending " + text)
875+ var idx = text.indexOf(":")
876+ var nick_to = text.substring(0, idx).trim()
877+ var msg = text.substring(idx+1, 9999).trim()
878+ var i = {
879+ "from" : chatClient.nick,
880+ "to" : nick_to,
881+ "message" : msg
882+ }
883+ var o = {
884+ enabled: annoyingSwitch.checked,
885+ persist: persistSwitch.checked,
886+ popup: popupSwitch.checked,
887+ sound: soundSwitch.checked,
888+ vibrate: vibrateSwitch.checked,
889+ counter: counterSlider.value
890+ }
891+ chatClient.sendMessage(i, o)
892+ i["type"] = "sent"
893+ messagesModel.insert(0, i)
894+ text = ""
895+ }
896+ }
897+ ListModel {
898+ id: messagesModel
899+ ListElement {
900+ from: ""
901+ to: ""
902+ type: "info"
903+ message: "Register by typing your nick and clicking Login."
904+ }
905+ ListElement {
906+ from: ""
907+ to: ""
908+ type: "info"
909+ message: "Send messages in the form \"destination: hello\""
910+ }
911+ ListElement {
912+ from: ""
913+ to: ""
914+ type: "info"
915+ message: "Slide from the bottom to control notification behaviour."
916+ }
917+ }
918+
919+ UbuntuShape {
920+ anchors.left: parent.left
921+ anchors.right: parent.right
922+ anchors.bottom: notificationSettings.bottom
923+ anchors.top: messageEdit.bottom
924+ anchors.topMargin: units.gu(1)
925+ ListView {
926+ id: messageList
927+ model: messagesModel
928+ anchors.fill: parent
929+ delegate: Rectangle {
930+ MouseArea {
931+ anchors.fill: parent
932+ onClicked: {
933+ if (from != "") {
934+ messageEdit.text = from + ": "
935+ messageEdit.focus = true
936+ }
937+ }
938+ }
939+ height: label.height + units.gu(2)
940+ width: parent.width
941+ Rectangle {
942+ color: {
943+ "info": "#B5EBB9",
944+ "received" : "#A2CFA5",
945+ "sent" : "#FFF9C8",
946+ "error" : "#FF4867"}[type]
947+ height: label.height + units.gu(1)
948+ anchors.fill: parent
949+ radius: 5
950+ anchors.margins: units.gu(.5)
951+ Text {
952+ id: label
953+ text: "<b>" + ((type=="sent")?to:from) + ":</b> " + message
954+ wrapMode: Text.Wrap
955+ width: parent.width - units.gu(1)
956+ x: units.gu(.5)
957+ y: units.gu(.5)
958+ horizontalAlignment: (type=="sent")?Text.AlignRight:Text.AlignLeft
959+ }
960+ }
961+ }
962+
963+ function handle_error(error) {
964+ messagesModel.insert(0, {
965+ "from" : "",
966+ "to" : "",
967+ "type" : "error",
968+ "message" : "<b>ERROR: " + error + "</b>"
969+ })
970+ }
971+
972+ function handle_notifications(list) {
973+ list.forEach(function(notification) {
974+ var item = JSON.parse(notification)
975+ item["type"] = "received"
976+ messagesModel.insert(0, item)
977+ })
978+ }
979+ }
980+ }
981+ Panel {
982+ id: notificationSettings
983+ anchors {
984+ left: parent.left
985+ right: parent.right
986+ bottom: parent.bottom
987+ }
988+ height: item1.height * 7
989+ UbuntuShape {
990+ anchors.fill: parent
991+ color: Theme.palette.normal.overlay
992+ Column {
993+ id: settingsColumn
994+ anchors.fill: parent
995+ ListItem.Header {
996+ text: "<b>Notification Settings</b>"
997+ }
998+ ListItem.Standard {
999+ id: item1
1000+ text: "Enable Notifications"
1001+ control: Switch {
1002+ id: annoyingSwitch
1003+ }
1004+ }
1005+ ListItem.Standard {
1006+ text: "Enable Popup"
1007+ enabled: annoyingSwitch.checked
1008+ control: Switch {
1009+ id: popupSwitch
1010+ }
1011+ }
1012+ ListItem.Standard {
1013+ text: "Persistent"
1014+ enabled: annoyingSwitch.checked
1015+ control: Switch {
1016+ id: persistSwitch
1017+ }
1018+ }
1019+ ListItem.Standard {
1020+ text: "Make Sound"
1021+ enabled: annoyingSwitch.checked
1022+ control: Switch {
1023+ id: soundSwitch
1024+ }
1025+ }
1026+ ListItem.Standard {
1027+ text: "Vibrate"
1028+ enabled: annoyingSwitch.checked
1029+ control: Switch {
1030+ id: vibrateSwitch
1031+ }
1032+ }
1033+ ListItem.Standard {
1034+ text: "Counter Value"
1035+ enabled: annoyingSwitch.checked
1036+ control: Slider {
1037+ id: counterSlider
1038+ }
1039+ }
1040+ }
1041+ }
1042+ }
1043+}
1044
1045=== added file 'docs/example-client/manifest.json'
1046--- docs/example-client/manifest.json 1970-01-01 00:00:00 +0000
1047+++ docs/example-client/manifest.json 2014-09-08 16:59:38 +0000
1048@@ -0,0 +1,19 @@
1049+{
1050+ "architecture": "all",
1051+ "description": "Example app for Ubuntu push notifications.",
1052+ "framework": "ubuntu-sdk-14.10-dev2",
1053+ "hooks": {
1054+ "hello": {
1055+ "apparmor": "hello.json",
1056+ "desktop": "hello.desktop"
1057+ },
1058+ "helloHelper": {
1059+ "apparmor": "helloHelper-apparmor.json",
1060+ "push-helper": "helloHelper.json"
1061+ }
1062+ },
1063+ "maintainer": "Roberto Alsina <roberto.alsina@canonical.com>",
1064+ "name": "com.ubuntu.developer.ralsina.hello",
1065+ "title": "ubuntu-push-example",
1066+ "version": "0.4"
1067+}
1068
1069=== added file 'docs/example-client/push-example.png'
1070Binary files docs/example-client/push-example.png 1970-01-01 00:00:00 +0000 and docs/example-client/push-example.png 2014-09-08 16:59:38 +0000 differ
1071=== added file 'docs/example-client/push-example.qmlproject'
1072--- docs/example-client/push-example.qmlproject 1970-01-01 00:00:00 +0000
1073+++ docs/example-client/push-example.qmlproject 2014-09-08 16:59:38 +0000
1074@@ -0,0 +1,52 @@
1075+import QmlProject 1.1
1076+
1077+Project {
1078+ mainFile: "main.qml"
1079+
1080+ /* Include .qml, .js, and image files from current directory and subdirectories */
1081+ QmlFiles {
1082+ directory: "."
1083+ }
1084+ JavaScriptFiles {
1085+ directory: "."
1086+ }
1087+ ImageFiles {
1088+ directory: "."
1089+ }
1090+ Files {
1091+ filter: "*.desktop"
1092+ }
1093+ Files {
1094+ filter: "www/*.html"
1095+ }
1096+ Files {
1097+ filter: "Makefile"
1098+ }
1099+ Files {
1100+ directory: "www"
1101+ filter: "*"
1102+ }
1103+ Files {
1104+ directory: "www/img/"
1105+ filter: "*"
1106+ }
1107+ Files {
1108+ directory: "www/css/"
1109+ filter: "*"
1110+ }
1111+ Files {
1112+ directory: "www/js/"
1113+ filter: "*"
1114+ }
1115+ Files {
1116+ directory: "tests/"
1117+ filter: "*"
1118+ }
1119+ Files {
1120+ directory: "debian"
1121+ filter: "*"
1122+ }
1123+ /* List of plugin directories passed to QML runtime */
1124+ importPaths: [ "." ,"/usr/bin","/usr/lib/x86_64-linux-gnu/qt5/qml" ]
1125+}
1126+
1127
1128=== added directory 'docs/example-client/tests'
1129=== added directory 'docs/example-client/tests/autopilot'
1130=== added directory 'docs/example-client/tests/autopilot/push-example'
1131=== added file 'docs/example-client/tests/autopilot/push-example/__init__.py'
1132--- docs/example-client/tests/autopilot/push-example/__init__.py 1970-01-01 00:00:00 +0000
1133+++ docs/example-client/tests/autopilot/push-example/__init__.py 2014-09-08 16:59:38 +0000
1134@@ -0,0 +1,72 @@
1135+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
1136+
1137+"""Ubuntu Touch App autopilot tests."""
1138+
1139+import os
1140+import subprocess
1141+
1142+from autopilot import input, platform
1143+from autopilot.matchers import Eventually
1144+from testtools.matchers import Equals
1145+from ubuntuuitoolkit import base, emulators
1146+
1147+
1148+def _get_module_include_path():
1149+ return os.path.join(get_path_to_source_root(), 'modules')
1150+
1151+
1152+def get_path_to_source_root():
1153+ return os.path.abspath(
1154+ os.path.join(
1155+ os.path.dirname(__file__), '..', '..', '..', '..'))
1156+
1157+
1158+class ClickAppTestCase(base.UbuntuUIToolkitAppTestCase):
1159+ """Common test case that provides several useful methods for the tests."""
1160+
1161+ package_id = '' # TODO
1162+ app_name = 'push-example'
1163+
1164+ def setUp(self):
1165+ super(ClickAppTestCase, self).setUp()
1166+ self.pointing_device = input.Pointer(self.input_device_class.create())
1167+ self.launch_application()
1168+
1169+ self.assertThat(self.main_view.visible, Eventually(Equals(True)))
1170+
1171+ def launch_application(self):
1172+ if platform.model() == 'Desktop':
1173+ self._launch_application_from_desktop()
1174+ else:
1175+ self._launch_application_from_phablet()
1176+
1177+ def _launch_application_from_desktop(self):
1178+ app_qml_source_location = self._get_app_qml_source_path()
1179+ if os.path.exists(app_qml_source_location):
1180+ self.app = self.launch_test_application(
1181+ base.get_qmlscene_launch_command(),
1182+ '-I' + _get_module_include_path(),
1183+ app_qml_source_location,
1184+ app_type='qt',
1185+ emulator_base=emulators.UbuntuUIToolkitEmulatorBase)
1186+ else:
1187+ raise NotImplementedError(
1188+ "On desktop we can't install click packages yet, so we can "
1189+ "only run from source.")
1190+
1191+ def _get_app_qml_source_path(self):
1192+ qml_file_name = '{0}.qml'.format(self.app_name)
1193+ return os.path.join(self._get_path_to_app_source(), qml_file_name)
1194+
1195+ def _get_path_to_app_source(self):
1196+ return os.path.join(get_path_to_source_root(), self.app_name)
1197+
1198+ def _launch_application_from_phablet(self):
1199+ # On phablet, we only run the tests against the installed click
1200+ # package.
1201+ self.app = self.launch_click_package(self.pacakge_id, self.app_name)
1202+
1203+ @property
1204+ def main_view(self):
1205+ return self.app.select_single(emulators.MainView)
1206+
1207
1208=== added file 'docs/example-client/tests/autopilot/push-example/test_main.py'
1209--- docs/example-client/tests/autopilot/push-example/test_main.py 1970-01-01 00:00:00 +0000
1210+++ docs/example-client/tests/autopilot/push-example/test_main.py 2014-09-08 16:59:38 +0000
1211@@ -0,0 +1,25 @@
1212+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
1213+
1214+"""Tests for the Hello World"""
1215+
1216+import os
1217+
1218+from autopilot.matchers import Eventually
1219+from testtools.matchers import Equals
1220+
1221+import push-example
1222+
1223+
1224+class MainViewTestCase(push-example.ClickAppTestCase):
1225+ """Generic tests for the Hello World"""
1226+
1227+ def test_initial_label(self):
1228+ label = self.main_view.select_single(objectName='label')
1229+ self.assertThat(label.text, Equals('Hello..'))
1230+
1231+ def test_click_button_should_update_label(self):
1232+ button = self.main_view.select_single(objectName='button')
1233+ self.pointing_device.click_object(button)
1234+ label = self.main_view.select_single(objectName='label')
1235+ self.assertThat(label.text, Eventually(Equals('..world!')))
1236+
1237
1238=== added file 'docs/example-client/tests/autopilot/run'
1239--- docs/example-client/tests/autopilot/run 1970-01-01 00:00:00 +0000
1240+++ docs/example-client/tests/autopilot/run 2014-09-08 16:59:38 +0000
1241@@ -0,0 +1,12 @@
1242+#!/bin/bash
1243+
1244+if [[ -z `which autopilot` ]]; then
1245+ echo "Autopilot is not installed. Skip"
1246+ exit
1247+fi
1248+
1249+SCRIPTPATH=`dirname $0`
1250+pushd ${SCRIPTPATH}
1251+autopilot run push-example
1252+popd
1253+
1254
1255=== added directory 'docs/example-client/tests/unit'
1256=== added file 'docs/example-client/tests/unit/tst_hellocomponent.qml'
1257--- docs/example-client/tests/unit/tst_hellocomponent.qml 1970-01-01 00:00:00 +0000
1258+++ docs/example-client/tests/unit/tst_hellocomponent.qml 2014-09-08 16:59:38 +0000
1259@@ -0,0 +1,50 @@
1260+import QtQuick 2.0
1261+import QtTest 1.0
1262+import Ubuntu.Components 0.1
1263+import "../../components"
1264+
1265+// See more details @ http://qt-project.org/doc/qt-5.0/qtquick/qml-testcase.html
1266+
1267+// Execute tests with:
1268+// qmltestrunner
1269+
1270+Item {
1271+ // The objects
1272+ HelloComponent {
1273+ id: objectUnderTest
1274+ }
1275+
1276+ TestCase {
1277+ name: "HelloComponent"
1278+
1279+ function init() {
1280+ console.debug(">> init");
1281+ compare("",objectUnderTest.text,"text was not empty on init");
1282+ console.debug("<< init");
1283+ }
1284+
1285+ function cleanup() {
1286+ console.debug(">> cleanup");
1287+ console.debug("<< cleanup");
1288+ }
1289+
1290+ function initTestCase() {
1291+ console.debug(">> initTestCase");
1292+ console.debug("<< initTestCase");
1293+ }
1294+
1295+ function cleanupTestCase() {
1296+ console.debug(">> cleanupTestCase");
1297+ console.debug("<< cleanupTestCase");
1298+ }
1299+
1300+ function test_canReadAndWriteText() {
1301+ var expected = "Hello World";
1302+
1303+ objectUnderTest.text = expected;
1304+
1305+ compare(expected,objectUnderTest.text,"expected did not equal result");
1306+ }
1307+ }
1308+}
1309+
1310
1311=== added directory 'docs/example-server'
1312=== added file 'docs/example-server/.bzrignore'
1313--- docs/example-server/.bzrignore 1970-01-01 00:00:00 +0000
1314+++ docs/example-server/.bzrignore 2014-09-08 16:59:38 +0000
1315@@ -0,0 +1,1 @@
1316+.git
1317
1318=== added file 'docs/example-server/.gitignore'
1319--- docs/example-server/.gitignore 1970-01-01 00:00:00 +0000
1320+++ docs/example-server/.gitignore 2014-09-08 16:59:38 +0000
1321@@ -0,0 +1,3 @@
1322+.bzr
1323+.bzrignore
1324+node_modules
1325
1326=== added file 'docs/example-server/app.js'
1327--- docs/example-server/app.js 1970-01-01 00:00:00 +0000
1328+++ docs/example-server/app.js 2014-09-08 16:59:38 +0000
1329@@ -0,0 +1,212 @@
1330+var express = require('express')
1331+ , bodyParser = require('body-parser')
1332+
1333+var Registry = require('./lib/registry')
1334+ , Inbox = require('./lib/inbox')
1335+ , Notifier = require('./lib/notifier')
1336+
1337+function wire(db, cfg) {
1338+ var reg, inbox, notifier, app
1339+
1340+ // control whether not to have persistent inboxes
1341+ var no_inbox = cfg.no_inbox
1342+
1343+ app = express()
1344+
1345+ reg = new Registry(db)
1346+ if (!no_inbox) {
1347+ inbox = new Inbox(db)
1348+ } else {
1349+ inbox = null
1350+ }
1351+ var push_url = process.env.PUSH_URL || cfg.push_url
1352+ notifier = new Notifier(push_url, cfg)
1353+ notifier.on('unknownToken', function(nick, token) {
1354+ reg.removeToken(nick, token, function() {},
1355+ function(err) {
1356+ app.emit('mongoError', err)
1357+ })
1358+ })
1359+ notifier.on('pushError', function(err, resp, body) {
1360+ app.emit('pushError', err, resp, body)
1361+ })
1362+
1363+ function unavailable(resp, err) {
1364+ app.emit('mongoError', err)
1365+ var ctype = resp.get('Content-Type')
1366+ if (ctype&&ctype.substr(0,10) == 'text/plain') {
1367+ resp.send(503, "db is hopefully only momentarily :(\n")
1368+ } else {
1369+ resp.json(503, {error: "unavailable"})
1370+ }
1371+ }
1372+
1373+ app.get("/_check", function(req, resp) {
1374+ db.command({ping: 1}, function(err, res) {
1375+ if(!err) {
1376+ resp.json({ok: true})
1377+ } else {
1378+ unavailable(resp, err)
1379+ }
1380+ })
1381+ })
1382+
1383+ app.use(bodyParser.json())
1384+ app.use('/play-notify-form', bodyParser.urlencoded({extended: false}))
1385+ app.use(function(err, req, resp, next) {
1386+ resp.json(err.status, {error: "invalid", message: err.message})
1387+ })
1388+
1389+ app.get("/", function(req, resp) {
1390+ if (!cfg.play_notify_form) {
1391+ resp.sendfile(__dirname + '/index.html')
1392+ } else {
1393+ resp.sendfile(__dirname + '/notify-form.html')
1394+ }
1395+ })
1396+
1397+ // NB: simplified, minimal identity and auth piggybacking on push tokens
1398+
1399+ /*
1400+ POST /register let's register a pair nick, token taking a JSON obj:
1401+ { "nick": string, "token": token-string }
1402+ */
1403+ app.post("/register", function(req, resp) {
1404+ if(typeof(req.body.token) != "string" ||
1405+ typeof(req.body.nick) != "string" ||
1406+ req.body.token == "" || req.body.nick == "") {
1407+ resp.json(400, {"error": "invalid"})
1408+ return
1409+ }
1410+ reg.insertToken(req.body.nick, req.body.token, function() {
1411+ resp.json({ok: true})
1412+ }, function() {
1413+ resp.json(400, {"error": "dup"})
1414+ }, function(err) {
1415+ unavailable(resp, err)
1416+ })
1417+ })
1418+
1419+ function checkToken(nick, token, okCb, resp) {
1420+ function bad() {
1421+ resp.json(401, {error: "unauthorized"})
1422+ }
1423+ reg.findToken(nick, function(nickToken) {
1424+ if (nickToken == token) {
1425+ okCb()
1426+ return
1427+ }
1428+ bad()
1429+ }, bad, function(err) {
1430+ unavailable(resp, err)
1431+ })
1432+ }
1433+
1434+ /* doNotify
1435+
1436+ ephemeral is true: message not put in the inbox, _ephemeral flag added
1437+
1438+ ephemeral is false: message put in inbox, with added unix _timestamp and
1439+ increasing _serial
1440+
1441+ */
1442+ function doNotify(ephemeral, nick, data, okCb, unknownNickCb, resp) {
1443+ function notify(token, data) {
1444+ notifier.notify(nick, token, data)
1445+ okCb()
1446+ }
1447+ reg.findToken(nick, function(token) {
1448+ if (ephemeral||no_inbox) {
1449+ data._ephemeral = Date.now()
1450+ notify(token, data)
1451+ } else {
1452+ inbox.pushMessage(token, data, function(msg) {
1453+ notify(token, msg)
1454+ }, function(err) {
1455+ unavailable(resp, err)
1456+ })
1457+ }
1458+ }, function() { // not found
1459+ unknownNickCb()
1460+ }, function(err) {
1461+ unavailable(resp, err)
1462+ })
1463+ }
1464+
1465+ /*
1466+ POST /message let's send a message to nick taking a JSON obj:
1467+ { "nick": string, "data": data, "from_nick": string, "from_token": string}
1468+ */
1469+ app.post("/message", function(req, resp) {
1470+ if (!req.body.data||!req.body.nick||!req.body.from_token||!req.body.from_nick) {
1471+ resp.json(400, {"error": "invalid"})
1472+ return
1473+ }
1474+ checkToken(req.body.from_nick, req.body.from_token, function() {
1475+ var data = req.body.data
1476+ data._from = req.body.from_nick
1477+ doNotify(false, req.body.nick, data, function() {
1478+ resp.json({ok: true})
1479+ }, function() { // not found
1480+ resp.json(400, {"error": "unknown-nick"})
1481+ }, resp)
1482+ }, resp)
1483+ })
1484+
1485+ if (!no_inbox) { // /drain supported only if no_inbox false
1486+ /*
1487+ POST /drain let's get pending messages for nick+token:
1488+ it removes messages older than timestamp and return newer ones
1489+ { "nick": string, "token": string, "timestamp": unix-timestamp }
1490+ */
1491+ app.post("/drain", function(req, resp) {
1492+ if(!req.body.token||!req.body.nick||
1493+ typeof(req.body.timestamp) != "number") {
1494+ resp.json(400, {"error": "invalid"})
1495+ return
1496+ }
1497+ checkToken(req.body.nick, req.body.token, function() {
1498+ inbox.drain(req.body.token, req.body.timestamp, function(msgs) {
1499+ resp.json(200, {ok: true, msgs: msgs})
1500+ }, function(err) {
1501+ unavailable(resp, err)
1502+ })
1503+ }, resp)
1504+ })
1505+ }
1506+
1507+ /*
1508+ Form /play-notify-form
1509+ messages sent through the form are ephemeral, just transmitted through PN,
1510+ without being pushed into the inbox
1511+ */
1512+ if (cfg.play_notify_form) {
1513+ app.post("/play-notify-form", function(req, resp) {
1514+ resp.type('text/plain')
1515+ if (!req.body.data||!req.body.nick) {
1516+ resp.send(400, "invalid/empty fields\n")
1517+ return
1518+ }
1519+ var data
1520+ try {
1521+ data = JSON.parse(req.body.data)
1522+ } catch(e) {
1523+ resp.send(400, "data is not JSON\n")
1524+ return
1525+ }
1526+ doNotify(true, req.body.nick, data, function() {
1527+ resp.send(200, 'OK\n')
1528+ }, function() { // not found
1529+ resp.send(400, "unknown nick\n")
1530+ }, resp)
1531+ })
1532+ }
1533+
1534+ // for testing
1535+ app.set('_reg', reg)
1536+ app.set('_inbox', inbox)
1537+ app.set('_notifier', notifier)
1538+ return app
1539+}
1540+
1541+exports.wire = wire
1542
1543=== added directory 'docs/example-server/config'
1544=== added file 'docs/example-server/config/config.js'
1545--- docs/example-server/config/config.js 1970-01-01 00:00:00 +0000
1546+++ docs/example-server/config/config.js 2014-09-08 16:59:38 +0000
1547@@ -0,0 +1,15 @@
1548+module.exports = config = {
1549+ "name" : "pushAppServer"
1550+ ,"app_id" : "appEx"
1551+ ,"listen_port" : 8000
1552+ ,"mongo_host" : "localhost"
1553+ ,"mongo_port" : 27017
1554+ ,"mongo_opts" : {}
1555+ ,"push_url": "https://push.ubuntu.com"
1556+ ,"retry_batch": 5
1557+ ,"retry_secs" : 30
1558+ ,"happy_retry_secs": 5
1559+ ,"expire_mins": 120
1560+ ,"no_inbox": true
1561+ ,"play_notify_form": false
1562+}
1563
1564=== added file 'docs/example-server/index.html'
1565--- docs/example-server/index.html 1970-01-01 00:00:00 +0000
1566+++ docs/example-server/index.html 2014-09-08 16:59:38 +0000
1567@@ -0,0 +1,9 @@
1568+<!doctype html>
1569+<html>
1570+<head>
1571+<title>pushAppServer</title>
1572+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
1573+</head>
1574+<body>
1575+<h1>Push Notifications Application Server Example</h1>
1576+</html>
1577
1578=== added directory 'docs/example-server/lib'
1579=== added file 'docs/example-server/lib/inbox.js'
1580--- docs/example-server/lib/inbox.js 1970-01-01 00:00:00 +0000
1581+++ docs/example-server/lib/inbox.js 2014-09-08 16:59:38 +0000
1582@@ -0,0 +1,47 @@
1583+/* token -> token inbox */
1584+
1585+function Inbox(db) {
1586+ this.db = db
1587+}
1588+
1589+Inbox.prototype.pushMessage = function(token, msg, doneCb, errCb) {
1590+ if (!msg._timestamp) {
1591+ var now = Date.now()
1592+ msg._timestamp = now
1593+ }
1594+ this.db.collection('inbox').findAndModify({_id: token}, null, {
1595+ $push: {msgs: msg},
1596+ $inc: {serial: 1},
1597+ }, {upsert: true, new: true, fields: {serial: 1}}, function(err, doc) {
1598+ if (err) {
1599+ errCb(err)
1600+ return
1601+ }
1602+ msg._serial = doc.serial
1603+ doneCb(msg)
1604+ })
1605+}
1606+
1607+Inbox.prototype.drain = function(token, timestamp, doneCb, errCb) {
1608+ this.db.collection('inbox').findAndModify({_id: token}, null, {
1609+ $pull: {msgs: {_timestamp: {$lt: timestamp}}}
1610+ }, {new: true}, function(err, doc) {
1611+ if (err) {
1612+ errCb(err)
1613+ return
1614+ }
1615+ if (!doc) {
1616+ doneCb([])
1617+ return
1618+ }
1619+ var serial = doc.serial
1620+ var msgs = doc.msgs
1621+ for (var i = msgs.length-1; i >= 0; i--) {
1622+ msgs[i].serial = serial
1623+ serial--
1624+ }
1625+ doneCb(msgs)
1626+ })
1627+}
1628+
1629+module.exports = Inbox
1630
1631=== added file 'docs/example-server/lib/notifier.js'
1632--- docs/example-server/lib/notifier.js 1970-01-01 00:00:00 +0000
1633+++ docs/example-server/lib/notifier.js 2014-09-08 16:59:38 +0000
1634@@ -0,0 +1,89 @@
1635+/* Notifier sends notifications, with some retry support */
1636+
1637+var request = require('request')
1638+ , url = require('url')
1639+ , EventEmitter = require('events').EventEmitter
1640+ , util = require('util')
1641+
1642+
1643+function Notifier(baseURL, cfg) {
1644+ this.baseURL = baseURL
1645+ this.cfg = cfg
1646+ this._retrier = null
1647+ this._retryInterval = 0
1648+ this._q = []
1649+}
1650+
1651+util.inherits(Notifier, EventEmitter)
1652+
1653+Notifier.prototype._retry = function(nick, token, data, cb, expireOn) {
1654+ var self = this
1655+ self._q.push([nick, token, data, cb, expireOn])
1656+ if (!self._retrier || self._retryInterval == self.cfg.happy_retry_secs) {
1657+ clearTimeout(self._retrier)
1658+ self._retryInterval = self.cfg.retry_secs
1659+ self._retrier = setTimeout(function() { self._doRetry() }, 1000*self._retryInterval)
1660+ }
1661+}
1662+
1663+Notifier.prototype._doRetry = function() {
1664+ var self = this
1665+ self._retryInterval = 0
1666+ self._retrier = null
1667+ var i = 0
1668+ while (self._q.length > 0) {
1669+ var toRetry = self._q.shift()
1670+ if (new Date() > toRetry[4]) { // expired
1671+ continue
1672+ }
1673+ self.notify(toRetry[0], toRetry[1], toRetry[2], toRetry[3], toRetry[4])
1674+ i++
1675+ if (i >= self.cfg.retry_batch) {
1676+ break
1677+ }
1678+ }
1679+ if (self._q.length) {
1680+ self._retryInterval = self.cfg.happy_retry_secs
1681+ self._retrier = setTimeout(function() { self._doRetry() }, 1000*self._retryInterval)
1682+ }
1683+}
1684+
1685+Notifier.prototype.unknownToken = function(nick, token) {
1686+ this.emit('unknownToken', nick, token)
1687+}
1688+
1689+
1690+Notifier.prototype.pushError = function(err, resp, body) {
1691+ this.emit('pushError', err, resp, body)
1692+}
1693+
1694+Notifier.prototype.notify = function(nick, token, data, cb, expireOn) {
1695+ var self = this
1696+ if (!expireOn) {
1697+ expireOn = new Date
1698+ expireOn.setUTCMinutes(expireOn.getUTCMinutes() + self.cfg.expire_mins)
1699+ }
1700+ var unicast = {
1701+ 'appid': self.cfg.app_id,
1702+ 'expire_on': expireOn.toISOString(),
1703+ 'token': token,
1704+ 'data': data
1705+ }
1706+ request.post(url.resolve(self.baseURL, 'notify'), {json: unicast}, function(error, resp, body) {
1707+ if (!error) {
1708+ if (resp.statusCode == 200) {
1709+ if (cb) cb()
1710+ return
1711+ } else if (resp.statusCode > 500) {
1712+ self._retry(nick, token, data, cb, expireOn)
1713+ return
1714+ } else if (resp.statusCode == 400 && body.error == "unknown-token") {
1715+ self.unknownToken(nick, token)
1716+ return
1717+ }
1718+ }
1719+ self.pushError(error, resp, body)
1720+ })
1721+}
1722+
1723+module.exports = Notifier
1724
1725=== added file 'docs/example-server/lib/registry.js'
1726--- docs/example-server/lib/registry.js 1970-01-01 00:00:00 +0000
1727+++ docs/example-server/lib/registry.js 2014-09-08 16:59:38 +0000
1728@@ -0,0 +1,62 @@
1729+/* nick -> token registry */
1730+
1731+function Registry(db) {
1732+ this.db = db
1733+}
1734+
1735+Registry.prototype.findToken = function(nick, foundCb, notFoundCb, errCb) {
1736+ var self = this
1737+ self.db.collection('registry').findOne({_id: nick}, function(err, doc) {
1738+ if (err) {
1739+ errCb(err)
1740+ return
1741+ }
1742+ if (doc == null) {
1743+ notFoundCb()
1744+ return
1745+ }
1746+ foundCb(doc.token)
1747+ })
1748+}
1749+
1750+Registry.prototype.insertToken = function(nick, token, doneCb, dupCb, errCb) {
1751+ var self = this
1752+ doc = {_id: nick, token: token}
1753+ self.db.collection('registry').insert(doc, function(err) {
1754+ if (!err) {
1755+ doneCb()
1756+ } else {
1757+ if (err.code == 11000) { // dup
1758+ self.findToken(nick, function(token2) {
1759+ if (token == token2) {
1760+ // same, idempotent
1761+ doneCb()
1762+ return
1763+ }
1764+ dupCb()
1765+ }, function() {
1766+ // not found, try again
1767+ self.insertToken(nick, token, doneCb, dupCb, errCb)
1768+ }, function(err) {
1769+ errCb(err)
1770+ })
1771+ return
1772+ }
1773+ errCb(err)
1774+ }
1775+ })
1776+}
1777+
1778+Registry.prototype.removeToken = function(nick, token, doneCb, errCb) {
1779+ var self = this
1780+ doc = {_id: nick, token: token}
1781+ self.db.collection('registry').remove(doc, function(err) {
1782+ if (err) {
1783+ errCb(err)
1784+ return
1785+ }
1786+ doneCb()
1787+ })
1788+}
1789+
1790+module.exports = Registry
1791
1792=== added file 'docs/example-server/notify-form.html'
1793--- docs/example-server/notify-form.html 1970-01-01 00:00:00 +0000
1794+++ docs/example-server/notify-form.html 2014-09-08 16:59:38 +0000
1795@@ -0,0 +1,14 @@
1796+<!doctype html>
1797+<html>
1798+<head>
1799+<title>notify</title>
1800+<meta http-equiv="content-type" content="text/html; charset=UTF-8">
1801+</head>
1802+<body>
1803+<form method="POST" action="/play-notify-form" >
1804+<label for="nick">Nick:</label><br><input id="nick" name="nick" type="text" autofocus><br>
1805+<label for="data">Message (JSON):</label><br><textarea id="data" name="data" cols="64" rows="8"></textarea><br>
1806+<input type="submit" value="notify">
1807+</form>
1808+</body>
1809+</html>
1810
1811=== added file 'docs/example-server/package.json'
1812--- docs/example-server/package.json 1970-01-01 00:00:00 +0000
1813+++ docs/example-server/package.json 2014-09-08 16:59:38 +0000
1814@@ -0,0 +1,25 @@
1815+{
1816+ "name": "pushAppServer",
1817+ "version": "0.0.2",
1818+ "description": "Push Notifications App Server Example",
1819+ "main": "server.js",
1820+ "dependencies": {
1821+ "body-parser": "~1.4.3",
1822+ "express": "~4.4.5",
1823+ "mongodb": "~1.4.7",
1824+ "request": "~2.36.0"
1825+ },
1826+ "scripts": {
1827+ "test": "mocha --ui tdd",
1828+ "start": "PUSH_URL=http://localhost:8080 node server.js"
1829+ },
1830+ "author": "Canonical",
1831+ "license": "LGPL",
1832+ "directories": {
1833+ "test": "test"
1834+ },
1835+ "devDependencies": {
1836+ "mocha": "~1.20.1",
1837+ "supertest": "~0.13.0"
1838+ }
1839+}
1840
1841=== added file 'docs/example-server/server.js'
1842--- docs/example-server/server.js 1970-01-01 00:00:00 +0000
1843+++ docs/example-server/server.js 2014-09-08 16:59:38 +0000
1844@@ -0,0 +1,29 @@
1845+/*
1846+ Push Notifications App Server Example
1847+*/
1848+
1849+var wire = require('./app').wire
1850+
1851+var cfg = require('./config/config')
1852+var mongoURL = 'mongodb://' + cfg.mongo_host + ':' + cfg.mongo_port + '/pushApp'
1853+
1854+var MongoClient = require('mongodb').MongoClient
1855+
1856+MongoClient.connect(mongoURL, cfg.mongo_opts, function(err, database) {
1857+ if(err) throw err
1858+
1859+ // wire appplication
1860+ var app = wire(database, cfg)
1861+
1862+ // log errors
1863+ app.on('pushError', function(err, resp, body) {
1864+ console.error('pushError', err, resp, body)
1865+ })
1866+ app.on('mongoError', function(err) {
1867+ console.error('mongoError', err)
1868+ })
1869+
1870+ // connection ready => start app
1871+ app.listen(cfg.listen_port)
1872+ console.info("Listening on:", cfg.listen_port)
1873+})
1874
1875=== added directory 'docs/example-server/test'
1876=== added file 'docs/example-server/test/app_test.js'
1877--- docs/example-server/test/app_test.js 1970-01-01 00:00:00 +0000
1878+++ docs/example-server/test/app_test.js 2014-09-08 16:59:38 +0000
1879@@ -0,0 +1,551 @@
1880+var assert = require('assert')
1881+ , http = require('http')
1882+ , request = require('supertest')
1883+
1884+var app = require('../app')
1885+
1886+var cfg = {
1887+ 'app_id': 'app1',
1888+ 'push_url': 'http://push',
1889+ 'expire_mins': 10,
1890+ 'retry_secs': 0.05,
1891+ 'retry_batch': 1,
1892+ 'happy_retry_secs': 0.02,
1893+}
1894+
1895+function cloneCfg() {
1896+ return JSON.parse(JSON.stringify(cfg))
1897+}
1898+
1899+var PLAY_NOTIFY_FORM = '/play-notify-form'
1900+
1901+suite('app', function() {
1902+ setup(function() {
1903+ this.db = {}
1904+ this.app = app.wire(this.db, cloneCfg())
1905+ this.reg = this.app.get('_reg')
1906+ this.inbox = this.app.get('_inbox')
1907+ this.notifier = this.app.get('_notifier')
1908+ })
1909+
1910+ test('wire', function() {
1911+ assert.ok(this.reg)
1912+ assert.ok(this.notifier)
1913+ assert.equal(this.notifier.baseURL, 'http://push')
1914+ var emitted
1915+ this.app.on('pushError', function(err, resp, body) {
1916+ emitted = [err, resp, body]
1917+ })
1918+ this.notifier.pushError('err', 'resp', 'body')
1919+ assert.deepEqual(emitted, ["err", "resp", "body"])
1920+ })
1921+
1922+ test('wire-unknownToken', function() {
1923+ var got
1924+ this.reg.removeToken = function(nick, token, doneCb, errCb) {
1925+ got = [nick, token]
1926+ doneCb()
1927+ }
1928+ this.notifier.emit('unknownToken', "N", "T")
1929+ assert.deepEqual(got, ["N", "T"])
1930+ })
1931+
1932+ test('wire-unknownToken-mongoError', function() {
1933+ var emitted
1934+ this.app.on('mongoError', function(err) {
1935+ emitted = err
1936+ })
1937+ this.reg.removeToken = function(nick, token, doneCb, errCb) {
1938+ errCb({})
1939+ }
1940+ this.notifier.emit('unknownToken', "N", "T")
1941+ assert.ok(emitted)
1942+ })
1943+
1944+ test('_check', function(done) {
1945+ var pingCmd
1946+ this.db.command = function(cmd, cb) {
1947+ pingCmd = cmd
1948+ cb(null)
1949+ }
1950+ request(this.app)
1951+ .get('/_check')
1952+ .expect('Content-Type', 'application/json; charset=utf-8')
1953+ .expect({ok: true})
1954+ .expect(200, function(err) {
1955+ assert.deepEqual(pingCmd, {ping: 1})
1956+ done(err)
1957+ })
1958+ })
1959+
1960+ test('_check-unavailable', function(done) {
1961+ var pingCmd
1962+ this.db.command = function(cmd, cb) {
1963+ pingCmd = cmd
1964+ cb({})
1965+ }
1966+ request(this.app)
1967+ .get('/_check')
1968+ .expect('Content-Type', 'application/json; charset=utf-8')
1969+ .expect({error: "unavailable"})
1970+ .expect(503, function(err) {
1971+ done(err)
1972+ })
1973+ })
1974+
1975+ test('any-broken', function(done) {
1976+ request(this.app)
1977+ .post('/register')
1978+ .set('Content-Type', 'application/json')
1979+ .send("")
1980+ .expect('Content-Type', 'application/json; charset=utf-8')
1981+ .expect({error: 'invalid', message: 'invalid json, empty body'})
1982+ .expect(400, done)
1983+ })
1984+
1985+ test('register', function(done) {
1986+ var got
1987+ this.reg.insertToken = function(nick, token, doneCb, dupCb, errCb) {
1988+ got = [nick, token]
1989+ doneCb()
1990+ }
1991+ request(this.app)
1992+ .post('/register')
1993+ .set('Content-Type', 'application/json')
1994+ .send({nick: "N", token: "T"})
1995+ .expect('Content-Type', 'application/json; charset=utf-8')
1996+ .expect({ok: true})
1997+ .expect(200, function(err) {
1998+ assert.deepEqual(got, ["N", "T"])
1999+ done(err)
2000+ })
2001+ })
2002+
2003+ test('register-invalid', function(done) {
2004+ request(this.app)
2005+ .post('/register')
2006+ .set('Content-Type', 'application/json')
2007+ .send({token: "T"})
2008+ .expect('Content-Type', 'application/json; charset=utf-8')
2009+ .expect({error: 'invalid'})
2010+ .expect(400, done)
2011+ })
2012+
2013+ test('register-dup', function(done) {
2014+ this.reg.insertToken = function(nick, token, doneCb, dupCb, errCb) {
2015+ dupCb()
2016+ }
2017+ request(this.app)
2018+ .post('/register')
2019+ .set('Content-Type', 'application/json')
2020+ .send({nick: "N", token: "T"})
2021+ .expect('Content-Type', 'application/json; charset=utf-8')
2022+ .expect({error: 'dup'})
2023+ .expect(400, done)
2024+ })
2025+
2026+ test('register-unavailable', function(done) {
2027+ this.reg.insertToken = function(nick, token, doneCb, dupCb, errCb) {
2028+ errCb({})
2029+ }
2030+ request(this.app)
2031+ .post('/register')
2032+ .set('Content-Type', 'application/json')
2033+ .send({nick: "N", token: "T"})
2034+ .expect('Content-Type', 'application/json; charset=utf-8')
2035+ .expect({error: 'unavailable'})
2036+ .expect(503, done)
2037+ })
2038+
2039+ test('message', function(done) {
2040+ var lookup = []
2041+ var pushed
2042+ var notify
2043+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2044+ lookup.push(nick)
2045+ if (nick == "N") {
2046+ foundCb("T")
2047+ } else {
2048+ foundCb("T2")
2049+ }
2050+ }
2051+ this.inbox.pushMessage = function(token, msg, doneCb, errCb) {
2052+ pushed = [token]
2053+ msg._serial = 10
2054+ doneCb(msg)
2055+ }
2056+ this.notifier.notify = function(nick, token, data) {
2057+ notify = [nick, token, data]
2058+ }
2059+ request(this.app)
2060+ .post('/message')
2061+ .set('Content-Type', 'application/json')
2062+ .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"})
2063+ .expect('Content-Type', 'application/json; charset=utf-8')
2064+ .expect({ok: true})
2065+ .expect(200, function(err) {
2066+ assert.deepEqual(lookup, ["N", "N2"])
2067+ assert.deepEqual(pushed, ["T2"])
2068+ assert.deepEqual(notify, ["N2", "T2", {m: 1,_from: "N",_serial: 10}])
2069+ done(err)
2070+ })
2071+ })
2072+
2073+ test('message-unauthorized', function(done) {
2074+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2075+ if (nick == "N") {
2076+ foundCb("T")
2077+ } else {
2078+ notFoundCb()
2079+ }
2080+ }
2081+ request(this.app)
2082+ .post('/message')
2083+ .set('Content-Type', 'application/json')
2084+ .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "Z"})
2085+ .expect('Content-Type', 'application/json; charset=utf-8')
2086+ .expect({error: "unauthorized"})
2087+ .expect(401, done)
2088+ })
2089+
2090+ test('message-unknown-nick', function(done) {
2091+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2092+ if (nick == "N") {
2093+ foundCb("T")
2094+ } else {
2095+ notFoundCb()
2096+ }
2097+ }
2098+ request(this.app)
2099+ .post('/message')
2100+ .set('Content-Type', 'application/json')
2101+ .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"})
2102+ .expect('Content-Type', 'application/json; charset=utf-8')
2103+ .expect({error: "unknown-nick"})
2104+ .expect(400, done)
2105+ })
2106+
2107+ test('message-invalid', function(done) {
2108+ request(this.app)
2109+ .post('/message')
2110+ .set('Content-Type', 'application/json')
2111+ .send({nick: "N"}) // missing data
2112+ .expect('Content-Type', 'application/json; charset=utf-8')
2113+ .expect({error: 'invalid'})
2114+ .expect(400, done)
2115+ })
2116+
2117+ test('message-check-token-unavailable', function(done) {
2118+ var emitted
2119+ this.app.on('mongoError', function(err) {
2120+ emitted = err
2121+ })
2122+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2123+ if (nick == "N") {
2124+ errCb({})
2125+ } else {
2126+ foundCb("T2")
2127+ }
2128+ }
2129+ request(this.app)
2130+ .post('/message')
2131+ .set('Content-Type', 'application/json')
2132+ .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"})
2133+ .expect('Content-Type', 'application/json; charset=utf-8')
2134+ .expect({error: 'unavailable'})
2135+ .expect(503, function(err) {
2136+ assert.ok(emitted)
2137+ done(err)
2138+ })
2139+ })
2140+
2141+ test('message-inbox-unavailable', function(done) {
2142+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2143+ if (nick == "N") {
2144+ foundCb("T")
2145+ } else {
2146+ foundCb("T2")
2147+ }
2148+ }
2149+ this.inbox.pushMessage = function(token, msg, doneCb, errCb) {
2150+ errCb({})
2151+ }
2152+ request(this.app)
2153+ .post('/message')
2154+ .set('Content-Type', 'application/json')
2155+ .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"})
2156+ .expect('Content-Type', 'application/json; charset=utf-8')
2157+ .expect({error: 'unavailable'})
2158+ .expect(503, done)
2159+ })
2160+
2161+ test('message-notify-unavailable', function(done) {
2162+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2163+ if (nick == "N") {
2164+ foundCb("T")
2165+ } else {
2166+ errCb({})
2167+ }
2168+ }
2169+ request(this.app)
2170+ .post('/message')
2171+ .set('Content-Type', 'application/json')
2172+ .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"})
2173+ .expect('Content-Type', 'application/json; charset=utf-8')
2174+ .expect({error: 'unavailable'})
2175+ .expect(503, function(err) {
2176+ done(err)
2177+ })
2178+ })
2179+
2180+ test('index', function(done) {
2181+ request(this.app)
2182+ .get('/')
2183+ .expect(new RegExp('<title>pushAppServer'))
2184+ .expect('Content-Type', 'text/html; charset=UTF-8')
2185+ .expect(200, done)
2186+ })
2187+
2188+ test('play-notify-form-absent', function(done) {
2189+ request(this.app)
2190+ .post(PLAY_NOTIFY_FORM)
2191+ .type('form')
2192+ .send({nick: "N", data: '{"m": 1}'})
2193+ .expect(404, done)
2194+ })
2195+
2196+ test('drain', function(done) {
2197+ var lookup
2198+ var got
2199+ var msgs = [{'m': 42, _timestamp: 4000, _serial: 20}]
2200+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2201+ lookup = [nick]
2202+ foundCb("T")
2203+ }
2204+ this.inbox.drain = function(token, timestamp, doneCb, errCb) {
2205+ got = [token, timestamp]
2206+ doneCb(msgs)
2207+ }
2208+ request(this.app)
2209+ .post('/drain')
2210+ .set('Content-Type', 'application/json')
2211+ .send({nick: "N", token: "T", timestamp: 4000})
2212+ .expect('Content-Type', 'application/json; charset=utf-8')
2213+ .expect({ok: true, msgs: msgs})
2214+ .expect(200, function(err) {
2215+ assert.deepEqual(lookup, ["N"])
2216+ assert.deepEqual(got, ["T", 4000])
2217+ done(err)
2218+ })
2219+ })
2220+
2221+ test('drain-unavailable', function(done) {
2222+ var msgs = [{'m': 42, _timestamp: 4000, _serial: 20}]
2223+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2224+ foundCb("T")
2225+ }
2226+ this.inbox.drain = function(token, timestamp, doneCb, errCb) {
2227+ errCb()
2228+ }
2229+ request(this.app)
2230+ .post('/drain')
2231+ .set('Content-Type', 'application/json')
2232+ .send({nick: "N", token: "T", timestamp: 4000})
2233+ .expect('Content-Type', 'application/json; charset=utf-8')
2234+ .expect({error: 'unavailable'})
2235+ .expect(503, function(err) {
2236+ done(err)
2237+ })
2238+ })
2239+
2240+ test('drain-invalid', function(done) {
2241+ request(this.app)
2242+ .post('/drain')
2243+ .set('Content-Type', 'application/json')
2244+ .send({nick: "N"}) // missing data
2245+ .expect('Content-Type', 'application/json; charset=utf-8')
2246+ .expect({error: 'invalid'})
2247+ .expect(400, done)
2248+ })
2249+
2250+ test('drain-invalid-timestamp', function(done) {
2251+ request(this.app)
2252+ .post('/drain')
2253+ .set('Content-Type', 'application/json')
2254+ .send({nick: "N", token: "T", timestamp: "foo"})
2255+ .expect('Content-Type', 'application/json; charset=utf-8')
2256+ .expect({error: 'invalid'})
2257+ .expect(400, done)
2258+ })
2259+
2260+})
2261+
2262+suite('app-with-play-notify', function() {
2263+ setup(function() {
2264+ this.db = {}
2265+ var cfg = cloneCfg()
2266+ cfg.play_notify_form = true
2267+ this.app = app.wire(this.db, cfg)
2268+ this.reg = this.app.get('_reg')
2269+ this.notifier = this.app.get('_notifier')
2270+ })
2271+
2272+ test('root-form', function(done) {
2273+ request(this.app)
2274+ .get('/')
2275+ .expect(new RegExp('<form.*action="' + PLAY_NOTIFY_FORM + '"(.|\n)*<input id="nick" name="nick"'))
2276+ .expect('Content-Type', 'text/html; charset=UTF-8')
2277+ .expect(200, done)
2278+ })
2279+
2280+ test('play-notify-form', function(done) {
2281+ var notify
2282+ , start = Date.now()
2283+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2284+ foundCb("T")
2285+ }
2286+ this.notifier.notify = function(nick, token, data) {
2287+ notify = [nick, token, data]
2288+ }
2289+ request(this.app)
2290+ .post(PLAY_NOTIFY_FORM)
2291+ .type('form')
2292+ .send({nick: "N", data: '{"m": 1}'})
2293+ .expect('Content-Type', 'text/plain; charset=utf-8')
2294+ .expect("OK\n")
2295+ .expect(200, function(err) {
2296+ assert.deepEqual(notify.slice(0, 2), ["N", "T"])
2297+ var data = notify[2]
2298+ assert.equal(typeof(data._ephemeral), "number")
2299+ assert.ok(data._ephemeral >= start)
2300+ delete data._ephemeral
2301+ assert.deepEqual(data, {"m": 1})
2302+ done(err)
2303+ })
2304+ })
2305+
2306+ test('play-notify-form-not-json', function(done) {
2307+ var notify
2308+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2309+ foundCb("T")
2310+ }
2311+ this.notifier.notify = function(nick, token, data) {
2312+ notify = [nick, token, data]
2313+ }
2314+ request(this.app)
2315+ .post(PLAY_NOTIFY_FORM)
2316+ .type('form')
2317+ .send({nick: "N", data: '{X'})
2318+ .expect('Content-Type', 'text/plain; charset=utf-8')
2319+ .expect("data is not JSON\n")
2320+ .expect(400, done)
2321+ })
2322+
2323+ test('play-notify-form-unknown-nick', function(done) {
2324+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2325+ notFoundCb()
2326+ }
2327+ request(this.app)
2328+ .post(PLAY_NOTIFY_FORM)
2329+ .set('Content-Type', 'application/json')
2330+ .send({nick: "N", data: '{"m": 1}'})
2331+ .expect('Content-Type', 'text/plain; charset=utf-8')
2332+ .expect("unknown nick\n")
2333+ .expect(400, done)
2334+ })
2335+
2336+ test('play-notify-form-unavailable', function(done) {
2337+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2338+ errCb({})
2339+ }
2340+ request(this.app)
2341+ .post(PLAY_NOTIFY_FORM)
2342+ .type('form')
2343+ .send({nick: "N", data: '{"m": 1}'})
2344+ .expect('Content-Type', 'text/plain; charset=utf-8')
2345+ .expect('db is hopefully only momentarily :(\n')
2346+ .expect(503, function(err) {
2347+ done(err)
2348+ })
2349+ })
2350+
2351+ test('play-notify-form-invalid', function(done) {
2352+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2353+ notFoundCb()
2354+ }
2355+ request(this.app)
2356+ .post(PLAY_NOTIFY_FORM)
2357+ .set('Content-Type', 'application/json')
2358+ .send({nick: "", data: '{"m": 1}'})
2359+ .expect('Content-Type', 'text/plain; charset=utf-8')
2360+ .expect("invalid/empty fields\n")
2361+ .expect(400, done)
2362+ })
2363+
2364+ test('play-notify-form-broken', function(done) {
2365+ request(this.app)
2366+ .post(PLAY_NOTIFY_FORM)
2367+ .type('form')
2368+ .send("=")
2369+ .expect(400, done)
2370+ })
2371+
2372+})
2373+
2374+suite('app-with-no-inbox', function() {
2375+ setup(function() {
2376+ this.db = {}
2377+ var cfg = cloneCfg()
2378+ cfg.no_inbox = true
2379+ this.app = app.wire(this.db, cfg)
2380+ this.reg = this.app.get('_reg')
2381+ this.inbox = this.app.get('_inbox')
2382+ this.notifier = this.app.get('_notifier')
2383+ })
2384+
2385+ test('no-inbox', function() {
2386+ assert.equal(this.inbox, null)
2387+ })
2388+
2389+ test('message-no-inbox', function(done) {
2390+ var lookup = []
2391+ var notify
2392+ , start = Date.now()
2393+ this.reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2394+ lookup.push(nick)
2395+ if (nick == "N") {
2396+ foundCb("T")
2397+ } else {
2398+ foundCb("T2")
2399+ }
2400+ }
2401+ this.notifier.notify = function(nick, token, data) {
2402+ notify = [nick, token, data]
2403+ }
2404+ request(this.app)
2405+ .post('/message')
2406+ .set('Content-Type', 'application/json')
2407+ .send({nick: "N2", data: {"m": 1}, from_nick: "N", from_token: "T"})
2408+ .expect('Content-Type', 'application/json; charset=utf-8')
2409+ .expect({ok: true})
2410+ .expect(200, function(err) {
2411+ assert.deepEqual(lookup, ["N", "N2"])
2412+ assert.deepEqual(notify.slice(0, 2), ["N2", "T2"])
2413+ var data = notify[2]
2414+ assert.equal(typeof(data._ephemeral), "number")
2415+ assert.ok(data._ephemeral >= start)
2416+ delete data._ephemeral
2417+ assert.deepEqual(data, {"m": 1, _from:"N"})
2418+ done(err)
2419+ })
2420+ })
2421+
2422+ test('drain-not-there', function(done) {
2423+ request(this.app)
2424+ .post('/drain')
2425+ .set('Content-Type', 'application/json')
2426+ .send({nick: "N", token: "T", timestamp: 4000})
2427+ .expect(404, done)
2428+ })
2429+
2430+})
2431
2432=== added file 'docs/example-server/test/inbox_test.js'
2433--- docs/example-server/test/inbox_test.js 1970-01-01 00:00:00 +0000
2434+++ docs/example-server/test/inbox_test.js 2014-09-08 16:59:38 +0000
2435@@ -0,0 +1,79 @@
2436+var assert = require('assert')
2437+
2438+var MongoClient = require('mongodb').MongoClient
2439+
2440+function unexpected(msg) {
2441+ assert.ok(false, "unexpected: "+msg)
2442+}
2443+
2444+var Inbox = require('../lib/inbox')
2445+
2446+suite('Inbox', function(){
2447+ setup(function(done) {
2448+ var self = this
2449+ MongoClient.connect("mongodb://localhost:27017/pushAppTestDb", function(err, database) {
2450+ if(err) throw err
2451+ self.db = database
2452+ // cleanup
2453+ self.db.collection('inbox').drop(function(err) {
2454+ if(err && err.errmsg != 'ns not found') throw err
2455+ done()
2456+ })
2457+ })
2458+ })
2459+
2460+ test('push-1', function(done) {
2461+ var inbox = new Inbox(this.db)
2462+ inbox.pushMessage('foo', {m: 42}, function(msg) {
2463+ assert.ok(msg._timestamp)
2464+ assert.equal(msg._serial, 1)
2465+ done()
2466+ }, function(err) {
2467+ unexpected(err)
2468+ })
2469+ })
2470+
2471+ test('push-2', function(done) {
2472+ var inbox = new Inbox(this.db)
2473+ inbox.pushMessage('foo', {m: 42}, function(msg) {
2474+ assert.equal(msg._serial, 1)
2475+ inbox.pushMessage('foo', {m: 45}, function(msg) {
2476+ assert.equal(msg._serial, 2)
2477+ done()
2478+ }, function(err) {
2479+ unexpected(err)
2480+ })
2481+ }, function(err) {
2482+ unexpected(err)
2483+ })
2484+ })
2485+
2486+ test('push-3-drain', function(done) {
2487+ var inbox = new Inbox(this.db)
2488+ inbox.pushMessage('foo', {m: 42, _timestamp: 2000}, function(msg) {
2489+ inbox.pushMessage('foo', {m: 45, _timestamp: 3000}, function(msg) {
2490+ inbox.pushMessage('foo', {m: 47, _timestamp: 4000}, function(msg) {
2491+ inbox.drain('foo', 3000, function(msgs) {
2492+ assert.deepEqual(msgs, [
2493+ {m: 45, _timestamp: 3000, serial: 2},
2494+ {m: 47, _timestamp: 4000, serial: 3}
2495+ ])
2496+ inbox.drain('foo', 0, function(msgs) {
2497+ assert.equal(msgs.length, 2)
2498+ done()
2499+ }, done)
2500+ }, done)
2501+ }, done)
2502+ }, done)
2503+ }, done)
2504+ })
2505+
2506+ test('drain-nop', function(done) {
2507+ var inbox = new Inbox(this.db)
2508+ inbox.drain('foo', 3000, function(msgs) {
2509+ assert.deepEqual(msgs, [])
2510+ done()
2511+ }, done)
2512+ })
2513+
2514+})
2515\ No newline at end of file
2516
2517=== added file 'docs/example-server/test/notifier_test.js'
2518--- docs/example-server/test/notifier_test.js 1970-01-01 00:00:00 +0000
2519+++ docs/example-server/test/notifier_test.js 2014-09-08 16:59:38 +0000
2520@@ -0,0 +1,192 @@
2521+var assert = require('assert')
2522+ , http = require('http')
2523+
2524+
2525+var Notifier = require('../lib/notifier')
2526+
2527+var cfg = {
2528+ 'app_id': 'app1',
2529+ 'expire_mins': 10,
2530+ 'retry_secs': 0.05,
2531+ 'retry_batch': 1,
2532+ 'happy_retry_secs': 0.02
2533+}
2534+ , cfg_batch2 = {
2535+ 'app_id': 'app1',
2536+ 'expire_mins': 10,
2537+ 'retry_secs': 0.05,
2538+ 'retry_batch': 2,
2539+ 'happy_retry_secs': 0.02
2540+}
2541+
2542+suite('Notifier', function() {
2543+ setup(function(done) {
2544+ var self = this
2545+ self.s = http.createServer(function(req, resp) {
2546+ self.s.emit(req.method, req, resp)
2547+ })
2548+ self.s.listen(0, 'localhost', function() {
2549+ self.url = 'http://localhost:' + self.s.address().port
2550+ done()
2551+ })
2552+ })
2553+
2554+ teardown(function() {
2555+ this.s.close()
2556+ })
2557+
2558+ test('happy-notify', function(done) {
2559+ var b = ""
2560+ this.s.on('POST', function(req, resp) {
2561+ req.on('data', function(chunk) {
2562+ b += chunk
2563+ })
2564+ req.on('end', function() {
2565+ resp.writeHead(200, {"Content-Type": "application/json"})
2566+ resp.end('{}')
2567+ })
2568+ })
2569+ var n = new Notifier(this.url, cfg)
2570+ var approxExpire = new Date
2571+ approxExpire.setUTCMinutes(approxExpire.getUTCMinutes()+10)
2572+ n.notify("N", "T", {m: 42}, function() {
2573+ var reqObj = JSON.parse(b)
2574+ var expireOn = Date.parse(reqObj.expire_on)
2575+ delete reqObj.expire_on
2576+ assert.ok(expireOn >= approxExpire)
2577+ assert.deepEqual(reqObj, {
2578+ "token": "T",
2579+ "appid": "app1",
2580+ "data": {"m": 42}
2581+ })
2582+ done()
2583+ })
2584+ })
2585+
2586+ test('retry-notify', function(done) {
2587+ var b = ""
2588+ var fail = 1
2589+ this.s.on('POST', function(req, resp) {
2590+ if (fail) {
2591+ fail--
2592+ resp.writeHead(503, {"Content-Type": "application/json"})
2593+ resp.end('')
2594+ return
2595+ }
2596+ req.on('data', function(chunk) {
2597+ b += chunk
2598+ })
2599+ req.on('end', function() {
2600+ resp.writeHead(200, {"Content-Type": "application/json"})
2601+ resp.end('{}')
2602+ })
2603+ })
2604+ var n = new Notifier(this.url, cfg)
2605+ var approxExpire = new Date
2606+ approxExpire.setUTCMinutes(approxExpire.getUTCMinutes()+10)
2607+ n.notify("N", "T", {m: 42}, function() {
2608+ var reqObj = JSON.parse(b)
2609+ var expireOn = Date.parse(reqObj.expire_on)
2610+ delete reqObj.expire_on
2611+ assert.ok(expireOn >= approxExpire)
2612+ assert.deepEqual(reqObj, {
2613+ "token": "T",
2614+ "appid": "app1",
2615+ "data": {"m": 42}
2616+ })
2617+ done()
2618+ })
2619+ })
2620+
2621+ function flakyPOST(s, fail, tokens) {
2622+ s.on('POST', function(req, resp) {
2623+ var b = ""
2624+ req.on('data', function(chunk) {
2625+ b += chunk
2626+ })
2627+ req.on('end', function() {
2628+ var reqObj = JSON.parse(b)
2629+ if (fail) {
2630+ fail--
2631+ resp.writeHead(503, {"Content-Type": "application/json"})
2632+ resp.end('')
2633+ return
2634+ }
2635+ tokens[reqObj.token] = 1
2636+ resp.writeHead(200, {"Content-Type": "application/json"})
2637+ resp.end('{}')
2638+ })
2639+ })
2640+ }
2641+
2642+ test('retry-notify-2-retries', function(done) {
2643+ var tokens = {}
2644+ flakyPOST(this.s, 2, tokens)
2645+ var n = new Notifier(this.url, cfg)
2646+ function yay() {
2647+ assert.deepEqual(tokens, {"T1": 1})
2648+ done()
2649+ }
2650+ n.notify("N1", "T1", {m: 42}, yay)
2651+ })
2652+
2653+ test('retry-notify-2-batches', function(done) {
2654+ var tokens = {}
2655+ flakyPOST(this.s, 2, tokens)
2656+ var n = new Notifier(this.url, cfg)
2657+ var waiting = 2
2658+ function yay() {
2659+ waiting--
2660+ if (waiting == 0) {
2661+ assert.deepEqual(tokens, {"T1": 1, "T2": 1})
2662+ done()
2663+ }
2664+ }
2665+ n.notify("N1", "T1", {m: 42}, yay)
2666+ n.notify("N2", "T2", {m: 42}, yay)
2667+ })
2668+
2669+ test('retry-notify-expired', function(done) {
2670+ var tokens = {}
2671+ flakyPOST(this.s, 2, tokens)
2672+ var n = new Notifier(this.url, cfg_batch2)
2673+ var waiting = 1
2674+ function yay() {
2675+ waiting--
2676+ if (waiting == 0) {
2677+ assert.deepEqual(tokens, {"T2": 1})
2678+ done()
2679+ }
2680+ }
2681+ n.notify("N1", "T1", {m: 42}, yay, new Date)
2682+ n.notify("N2", "T2", {m: 42}, yay)
2683+ })
2684+
2685+ test('unknown-token-notify', function(done) {
2686+ this.s.on('POST', function(req, resp) {
2687+ resp.writeHead(400, {"Content-Type": "application/json"})
2688+ resp.end('{"error": "unknown-token"}')
2689+ })
2690+ var n = new Notifier(this.url, cfg)
2691+ n.on('unknownToken', function(nick, token) {
2692+ assert.equal(nick, "N")
2693+ assert.equal(token, "T")
2694+ done()
2695+ })
2696+ n.notify("N", "T", {m: 42})
2697+ })
2698+
2699+ test('error-notify', function(done) {
2700+ this.s.on('POST', function(req, resp) {
2701+ resp.writeHead(500)
2702+ resp.end('')
2703+ })
2704+ var n = new Notifier(this.url, cfg)
2705+ n.on('pushError', function(err, resp, body) {
2706+ assert.equal(resp.statusCode, 500)
2707+ done()
2708+ })
2709+ n.notify("N", "T", {m: 42})
2710+ })
2711+
2712+})
2713
2714=== added file 'docs/example-server/test/registry_test.js'
2715--- docs/example-server/test/registry_test.js 1970-01-01 00:00:00 +0000
2716+++ docs/example-server/test/registry_test.js 2014-09-08 16:59:38 +0000
2717@@ -0,0 +1,179 @@
2718+var assert = require('assert')
2719+
2720+var MongoClient = require('mongodb').MongoClient
2721+
2722+function unexpected(msg) {
2723+ assert.ok(false, "unexpected: "+msg)
2724+}
2725+
2726+var Registry = require('../lib/registry')
2727+
2728+suite('Registry', function(){
2729+ setup(function(done) {
2730+ var self = this
2731+ MongoClient.connect("mongodb://localhost:27017/pushAppTestDb", function(err, database) {
2732+ if(err) throw err
2733+ self.db = database
2734+ // cleanup
2735+ self.db.collection('registry').drop(function(err) {
2736+ if(err && err.errmsg != 'ns not found') throw err
2737+ done()
2738+ })
2739+ })
2740+ })
2741+
2742+ test('insert-and-find', function(done) {
2743+ var reg = new Registry(this.db)
2744+ reg.insertToken("N", "T", function() {
2745+ reg.findToken("N", function(token) {
2746+ assert.equal(token, "T")
2747+ done()
2748+ }, function() {
2749+ unexpected("not-found")
2750+ }, function(err) {
2751+ unexpected(err)
2752+ })
2753+ }, function() {
2754+ unexpected("dup")
2755+ }, function(err) {
2756+ unexpected(err)
2757+ })
2758+ })
2759+
2760+ test('find-not-found', function(done) {
2761+ var reg = new Registry(this.db)
2762+ reg.findToken("N", function() {
2763+ unexpected("found")
2764+ }, function() {
2765+ done()
2766+ }, function(err) {
2767+ unexpected(err)
2768+ })
2769+ })
2770+
2771+ test('insert-identical-dup', function(done) {
2772+ var reg = new Registry(this.db)
2773+ reg.insertToken("N", "T", function() {
2774+ reg.insertToken("N", "T", function() {
2775+ done()
2776+ }, function() {
2777+ unexpected("dup")
2778+ }, function(err) {
2779+ unexpected(err)
2780+ })
2781+ }, function() {
2782+ unexpected("dup")
2783+ }, function(err) {
2784+ unexpected(err)
2785+ })
2786+ })
2787+
2788+ test('insert-dup', function(done) {
2789+ var reg = new Registry(this.db)
2790+ reg.insertToken("N", "T1", function() {
2791+ reg.insertToken("N", "T2", function() {
2792+ unexpected("success")
2793+ }, function() {
2794+ done()
2795+ }, function(err) {
2796+ unexpected(err)
2797+ })
2798+ }, function() {
2799+ unexpected("dup")
2800+ }, function(err) {
2801+ unexpected(err)
2802+ })
2803+ })
2804+
2805+ test('insert-temp-dup', function(done) {
2806+ var reg = new Registry(this.db)
2807+ var findToken = reg.findToken
2808+ , insertToken = reg.insertToken
2809+ var notFoundOnce = 0
2810+ var insertInvocations = 0
2811+ reg.findToken = function(nick, foundCb, notFoundCb, errCb) {
2812+ if (notFoundOnce == 0) {
2813+ notFoundOnce++
2814+ notFoundCb()
2815+ return
2816+ }
2817+ findToken.call(reg, nick, foundCb, notFoundCb, errCb)
2818+ }
2819+ reg.insertToken = function(nick, token, doneCb, dupCb, errCb) {
2820+ insertInvocations++
2821+ insertToken.call(reg, nick, token, doneCb, dupCb, errCb)
2822+ }
2823+ reg.insertToken("N", "T1", function() {
2824+ reg.insertToken("N", "T2", function() {
2825+ unexpected("success")
2826+ }, function() {
2827+ assert.equal(insertInvocations, 3)
2828+ done()
2829+ }, function(err) {
2830+ unexpected(err)
2831+ })
2832+ }, function() {
2833+ unexpected("dup")
2834+ }, function(err) {
2835+ unexpected(err)
2836+ })
2837+ })
2838+
2839+ test('remove', function(done) {
2840+ var reg = new Registry(this.db)
2841+ reg.insertToken("N", "T", function() {
2842+ reg.removeToken("N", "T", function() {
2843+ reg.findToken("N", function(token) {
2844+ unexpected("found")
2845+ }, function() {
2846+ done()
2847+ }, function(err) {
2848+ unexpected(err)
2849+ })
2850+ }, function(err) {
2851+ unexpected(err)
2852+ })
2853+ }, function() {
2854+ unexpected("dup")
2855+ }, function(err) {
2856+ unexpected(err)
2857+ })
2858+ })
2859+
2860+ test('remove-exact', function(done) {
2861+ var reg = new Registry(this.db)
2862+ reg.insertToken("N", "T1", function() {
2863+ reg.removeToken("N", "T2", function() {
2864+ reg.findToken("N", function(token) {
2865+ assert.equal(token, "T1")
2866+ done()
2867+ }, function() {
2868+ unexpected("no-found")
2869+ }, function(err) {
2870+ unexpected(err)
2871+ })
2872+ }, function(err) {
2873+ unexpected(err)
2874+ })
2875+ }, function() {
2876+ unexpected("dup")
2877+ }, function(err) {
2878+ unexpected(err)
2879+ })
2880+ })
2881+
2882+ test('remove-nop', function(done) {
2883+ var reg = new Registry(this.db)
2884+ reg.removeToken("N1", "T1", function() {
2885+ reg.findToken("N1", function(token) {
2886+ unexpected("found")
2887+ }, function() {
2888+ done()
2889+ }, function(err) {
2890+ unexpected(err)
2891+ })
2892+ }, function(err) {
2893+ unexpected(err)
2894+ })
2895+ })
2896+})
2897
2898=== modified file 'docs/highlevel.txt'
2899--- docs/highlevel.txt 2014-08-08 09:09:39 +0000
2900+++ docs/highlevel.txt 2014-09-08 16:59:38 +0000
2901@@ -1,8 +1,10 @@
2902-Ubuntu Push Client Developer Guide
2903-==================================
2904+Ubuntu Push Client High Level Developer Guide
2905+============================================
2906
2907 :Version: 0.50+
2908
2909+.. contents::
2910+
2911 Introduction
2912 ------------
2913
2914@@ -11,52 +13,7 @@
2915
2916 ---------
2917
2918-Let's describe the push system by way of an example.
2919-
2920-Alice has written a chat application called Chatter. Using it, Bob can send messages to Carol and viceversa. Alice has a
2921-web application for it, so the way it works now is that Bob connects to the service, posts a message, and when Carol
2922-connects, she gets it. If Carol leaves the browser window open, it beeps when messages arrive.
2923-
2924-Now Alice wants to create an Ubuntu Touch app for Chatter, so she implements the same architecture using a client that
2925-does the same thing as the web browser. Sadly, since applications on Ubuntu Touch don't run continuously, messages are
2926-only delivered when Carol opens the app, and the user experience suffers.
2927-
2928-Using the Ubuntu Push Server, this problem is alleviated: the Chatter server will deliver the messages to the Ubuntu
2929-Push Server, which in turn will send it in an efficient manner to the Ubuntu Push Client running in Bob and Carol's
2930-devices. The user sees a notification (all without starting the app) and then can launch it if he's interested in
2931-reading messages at that point.
2932-
2933-Since the app is not started and messages are delivered oportunistically, this is both battery and bandwidth-efficient.
2934-
2935-.. figure:: push.svg
2936-
2937-The Ubuntu Push system provides:
2938-
2939-* A push server which receives **push messages** from the app servers, queues them and delivers them efficiently
2940- to the devices.
2941-* A push client which receives those messages, queues messages to the app and displays notifications to the user
2942-
2943-The full lifecycle of a push message is:
2944-
2945-* Created in a application-specific server
2946-* Sent to the Ubuntu Push server, targeted at a user or user+device pair
2947-* Delivered to one or more Ubuntu devices
2948-* Passed through the application helper for processing
2949-* Notification displayed to the user (via different mechanisms)
2950-* Application Message queued for the app's use
2951-
2952-If the user interacts with the notification, the application is launched and should check its queue for messages
2953-it has to process.
2954-
2955-For the app developer, there are several components needed:
2956-
2957-* A server that sends the **push messages** to the Ubuntu Push server
2958-* Support in the client app for registering with the Ubuntu Push client
2959-* Support in the client app to react to **notifications** displayed to the user and process **application messages**
2960-* A helper program with application-specific knowledge that transforms **push messages** as needed.
2961-
2962-In the following sections, we'll see how to implement all the client side parts. For the application server, see the
2963-`Ubuntu Push Server API section <#ubuntu-push-server-api>`__
2964+.. include:: _description.txt
2965
2966 The PushClient Component
2967 ------------------------
2968@@ -68,7 +25,7 @@
2969 PushClient {
2970 id: pushClient
2971 Component.onCompleted: {
2972- newNotifications.connect(messageList.handle_notifications)
2973+ notificationsChanged.connect(messageList.handle_notifications)
2974 error.connect(messageList.handle_error)
2975 }
2976 appId: "com.ubuntu.developer.push.hello_hello"
2977@@ -95,13 +52,13 @@
2978 ~~~~~~~~~~~~~~~~~~~~~~~
2979
2980 When a notification is received by the Push Client, it will be delivered to your application's push helper, and then
2981-placed in your application's mailbox. At that point, the PushClient will emit the ``newNotifications(QStringList)`` signal
2982+placed in your application's mailbox. At that point, the PushClient will emit the ``notificationsChanged(QStringList)`` signal
2983 containing your messages. You should probably connect to that signal and handle those messages.
2984
2985 Because of the application's lifecycle, there is no guarantee that it will be running when the signal is emitted. For that
2986 reason, apps should check for pending notifications whenever they are activated or started. To do that, use the
2987 ``getNotifications()`` slot. Triggering that slot will fetch notifications and trigger the
2988-``newNotifications(QStringList)`` signal.
2989+``notificationsChanged(QStringList)`` signal.
2990
2991 Error Handling
2992 ~~~~~~~~~~~~~~
2993@@ -111,8 +68,8 @@
2994 Persistent Notification Management
2995 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2996
2997-Some notifications are persistent, meaning they don't disappear automatically. For those notifications, there is an API that
2998-allows the app to manage them without having to know the underlying details of the platform.
2999+Some notifications are persistent, meaning that, after they are presented, they don't disappear automatically.
3000+This API allows the app to manage that type of notifications.
3001
3002 On each notification there's an optional ``tag`` field, used for this purpose.
3003
3004@@ -125,201 +82,4 @@
3005
3006 The ``count`` property sets the counter in the application's icon to the given value.
3007
3008-
3009-Application Helpers
3010--------------------
3011-
3012-The payload delivered to push-client will be passed onto a helper program that can modify it as needed before passing it onto
3013-the postal service (see `Helper Output Format <#helper-output-format>`__).
3014-
3015-The helper receives two arguments ``infile`` and ``outfile``. The message is delivered via ``infile`` and the transformed
3016-version is placed in ``outfile``.
3017-
3018-This is the simplest possible useful helper, which simply passes the message through unchanged::
3019-
3020- #!/usr/bin/python3
3021-
3022- import sys
3023- f1, f2 = sys.argv[1:3]
3024- open(f2, "w").write(open(f1).read())
3025-
3026-Helpers need to be added to the click package manifest::
3027-
3028- {
3029- "name": "com.ubuntu.developer.ralsina.hello",
3030- "description": "description of hello",
3031- "framework": "ubuntu-sdk-14.10-qml-dev2",
3032- "architecture": "all",
3033- "title": "hello",
3034- "hooks": {
3035- "hello": {
3036- "apparmor": "hello.json",
3037- "desktop": "hello.desktop"
3038- },
3039- "helloHelper": {
3040- "apparmor": "helloHelper-apparmor.json",
3041- "push-helper": "helloHelper.json"
3042- }
3043- },
3044- "version": "0.2",
3045- "maintainer": "Roberto Alsina <roberto.alsina@canonical.com>"
3046- }
3047-
3048-Here, we created a helloHelper entry in hooks that has an apparmor profile and an additional JSON file for the push-helper hook.
3049-
3050-helloHelper-apparmor.json must contain **only** the push-notification-client policy group::
3051-
3052- {
3053- "policy_groups": [
3054- "push-notification-client"
3055- ],
3056- "policy_version": 1.2
3057- }
3058-
3059-And helloHelper.json must have at least a exec key with the path to the helper executable relative to the json, and optionally
3060-an app_id key containing the short id of one of the apps in the package (in the format packagename_appname without a version).
3061-If the app_id is not specified, the helper will be used for all apps in the package::
3062-
3063- {
3064- "exec": "helloHelper",
3065- "app_id": "com.ubuntu.developer.ralsina.hello_hello"
3066- }
3067-
3068-.. note:: For deb packages, helpers should be installed into /usr/lib/ubuntu-push-client/legacy-helpers/ as part of the package.
3069-
3070-Helper Output Format
3071---------------------
3072-
3073-Helpers output has two parts, the postal message (in the "message" key) and a notification to be presented to the user (in the "notification" key).
3074-
3075-Here's a simple example::
3076-
3077- {
3078- "message": "foobar",
3079- "notification": {
3080- "tag": "foo",
3081- "card": {
3082- "summary": "yes",
3083- "body": "hello",
3084- "popup": true,
3085- "persist": true,
3086- "timestamp": 1407160197
3087- }
3088- "sound": "buzz.mp3",
3089- "vibrate": {
3090- "pattern": [200, 100],
3091- "repeat": 2
3092- }
3093- "emblem-counter": {
3094- "count": 12,
3095- "visible": true
3096- }
3097- }
3098- }
3099-
3100-The notification can contain a **tag** field, which can later be used by the `persistent notification management API. <#persistent-notification-management>`__
3101-
3102-:message: (optional) A JSON object that is passed as-is to the application via PopAll.
3103-:notification: (optional) Describes the user-facing notifications triggered by this push message.
3104-
3105-The notification can contain a **card**. A card describes a specific notification to be given to the user,
3106-and has the following fields:
3107-
3108-:summary: (required) a title. The card will not be presented if this is missing.
3109-:body: longer text, defaults to empty.
3110-:actions: If empty (the default), a bubble notification is non-clickable.
3111- If you add a URL, then bubble notifications are clickable and launch that URL. One use for this is using a URL like
3112- ``appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version`` which will switch to the app or launch
3113- it if it's not running. See `URLDispatcher <https://wiki.ubuntu.com/URLDispatcher>`__ for more information.
3114-
3115-:icon: An icon relating to the event being notified. Defaults to empty (no icon);
3116- a secondary icon relating to the application will be shown as well, regardless of this field.
3117-:timestamp: Seconds since the unix epoch, only used for persist (for now). If zero or unset, defaults to current timestamp.
3118-:persist: Whether to show in notification centre; defaults to false
3119-:popup: Whether to show in a bubble. Users can disable this, and can easily miss them, so don't rely on it exclusively. Defaults to false.
3120-
3121-.. note:: Keep in mind that the precise way in which each field is presented to the user depends on factors such as
3122- whether it's shown as a bubble or in the notification centre, or even the version of Ubuntu Touch the user
3123- has on their device.
3124-
3125-The notification can contain a **sound** field. This is either a boolean (play a predetermined sound) or the path to a sound file. The user can disable it, so don't rely on it exclusively.
3126-Defaults to empty (no sound). The path is relative, and will be looked up in (a) the application's .local/share/<pkgname>, and (b)
3127-standard xdg dirs.
3128-
3129-The notification can contain a **vibrate** field, causing haptic feedback, which can be either a boolean (if true, vibrate a predetermined way) or an object that has the following content:
3130-
3131-:pattern: a list of integers describing a vibration pattern (duration of alternating vibration/no vibration times, in milliseconds).
3132-:repeat: number of times the pattern has to be repeated (defaults to 1, 0 is the same as 1).
3133-
3134-The notification can contain a **emblem-counter** field, with the following content:
3135-
3136-:count: a number to be displayed over the application's icon in the launcher.
3137-:visible: set to true to show the counter, or false to hide it.
3138-
3139-.. note:: Unlike other notifications, emblem-counter needs to be cleaned by the app itself.
3140- Please see `the persistent notification management section. <#persistent-notification-management>`__
3141-
3142-.. FIXME crosslink to hello example app on each method
3143-
3144-Security
3145-~~~~~~~~
3146-
3147-To use the push API, applications need to request permission in their security profile, using something like this::
3148-
3149- {
3150- "policy_groups": [
3151- "networking",
3152- "push-notification-client"
3153- ],
3154- "policy_version": 1.2
3155- }
3156-
3157-
3158-Ubuntu Push Server API
3159-----------------------
3160-
3161-The Ubuntu Push server is located at https://push.ubuntu.com and has a single endpoint: ``/notify``.
3162-To notify a user, your application has to do a POST with ``Content-type: application/json``.
3163-
3164-Here is an example of the POST body using all the fields::
3165-
3166- {
3167- "appid": "com.ubuntu.music_music",
3168- "expire_on": "2014-10-08T14:48:00.000Z",
3169- "token": "LeA4tRQG9hhEkuhngdouoA==",
3170- "clear_pending": true,
3171- "replace_tag": "tagname",
3172- "data": {
3173- "message": "foobar",
3174- "notification": {
3175- "card": {
3176- "summary": "yes",
3177- "body": "hello",
3178- "popup": true,
3179- "persist": true,
3180- "timestamp": 1407160197
3181- }
3182- "sound": "buzz.mp3",
3183- "tag": "foo",
3184- "vibrate": {
3185- "pattern": [200, 100],
3186- "repeat": 2
3187- }
3188- "emblem-counter": {
3189- "count": 12,
3190- "visible": true
3191- }
3192- }
3193- }
3194- }
3195-
3196-
3197-:appid: ID of the application that will receive the notification, as described in the client side documentation.
3198-:expire_on: Expiration date/time for this message, in `ISO8601 Extendend format <http://en.wikipedia.org/wiki/ISO_8601>`__
3199-:token: The token identifying the user+device to which the message is directed, as described in the client side documentation.
3200-:clear_pending: Discards all previous pending notifications. Usually in response to getting a "too-many-pending" error.
3201-:replace_tag: If there's a pending notification with the same tag, delete it before queuing this new one.
3202-:data: A JSON object.
3203-
3204-In this example, data is `what a helper would output <#helper-output-format>`__ but that's not necessarily the case.
3205-The content of the data field will be passed to the helper application which **has** to produce output in that format.
3206+.. include:: _common.txt
3207\ No newline at end of file
3208
3209=== modified file 'docs/lowlevel.txt'
3210--- docs/lowlevel.txt 2014-08-08 09:09:39 +0000
3211+++ docs/lowlevel.txt 2014-09-08 16:59:38 +0000
3212@@ -1,8 +1,10 @@
3213-Ubuntu Push Client Developer Guide
3214-==================================
3215+Ubuntu Push Client Low Level Developer Guide
3216+============================================
3217
3218 :Version: 0.50+
3219
3220+.. contents::
3221+
3222 Introduction
3223 ------------
3224
3225@@ -14,52 +16,8 @@
3226
3227 ---------
3228
3229-Let's describe the push system by way of an example.
3230-
3231-Alice has written a chat application called Chatter. Using it, Bob can send messages to Carol and viceversa. Alice has a
3232-web application for it, so the way it works now is that Bob connects to the service, posts a message, and when Carol
3233-connects, she gets it. If Carol leaves the browser window open, it beeps when messages arrive.
3234-
3235-Now Alice wants to create an Ubuntu Touch app for Chatter, so she implements the same architecture using a client that
3236-does the same thing as the web browser. Sadly, since applications on Ubuntu Touch don't run continuously, messages are
3237-only delivered when Carol opens the app, and the user experience suffers.
3238-
3239-Using the Ubuntu Push Server, this problem is alleviated: the Chatter server will deliver the messages to the Ubuntu
3240-Push Server, which in turn will send it in an efficient manner to the Ubuntu Push Client running in Bob and Carol's
3241-devices. The user sees a notification (all without starting the app) and then can launch it if he's interested in
3242-reading messages at that point.
3243-
3244-Since the app is not started and messages are delivered oportunistically, this is both battery and bandwidth-efficient.
3245-
3246-.. figure:: push.svg
3247-
3248-The Ubuntu Push system provides:
3249-
3250-* A push server which receives **push messages** from the app servers, queues them and delivers them efficiently
3251- to the devices.
3252-* A push client which receives those messages, queues messages to the app and displays notifications to the user
3253-
3254-The full lifecycle of a push message is:
3255-
3256-* Created in a application-specific server
3257-* Sent to the Ubuntu Push server, targeted at a user or user+device pair
3258-* Delivered to one or more Ubuntu devices
3259-* Passed through the application helper for processing
3260-* Notification displayed to the user (via different mechanisms)
3261-* Application Message queued for the app's use
3262-
3263-If the user interacts with the notification, the application is launched and should check its queue for messages
3264-it has to process.
3265-
3266-For the app developer, there are several components needed:
3267-
3268-* A server that sends the **push messages** to the Ubuntu Push server
3269-* Support in the client app for registering with the Ubuntu Push client
3270-* Support in the client app to react to **notifications** displayed to the user and process **application messages**
3271-* A helper program with application-specific knowledge that transforms **push messages** as needed.
3272-
3273-In the following sections, we'll see how to implement all the client side parts. For the application server, see the
3274-`Ubuntu Push Server API section <#ubuntu-push-server-api>`__
3275+.. include:: _description.txt
3276+
3277
3278 The PushNotifications Service
3279 -----------------------------
3280@@ -202,8 +160,8 @@
3281 Persistent Notification Management
3282 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3283
3284-Some notifications are persistent, meaning they don't disappear automatically. For those notifications, there is an API that
3285-allows the app to manage them without having to know the underlying details of the platform.
3286+Some notifications are persistent, meaning that, after they are presented, they don't disappear automatically.
3287+This API allows the app to manage that type of notifications.
3288
3289 On each notification there's an optional ``tag`` field, used for this purpose.
3290
3291@@ -220,200 +178,4 @@
3292 Set the counter to the given values.
3293
3294
3295-Application Helpers
3296--------------------
3297-
3298-The payload delivered to push-client will be passed onto a helper program that can modify it as needed before passing it onto
3299-the postal service (see `Helper Output Format <#helper-output-format>`__).
3300-
3301-The helper receives two arguments ``infile`` and ``outfile``. The message is delivered via ``infile`` and the transformed
3302-version is placed in ``outfile``.
3303-
3304-This is the simplest possible useful helper, which simply passes the message through unchanged::
3305-
3306- #!/usr/bin/python3
3307-
3308- import sys
3309- f1, f2 = sys.argv[1:3]
3310- open(f2, "w").write(open(f1).read())
3311-
3312-Helpers need to be added to the click package manifest::
3313-
3314- {
3315- "name": "com.ubuntu.developer.ralsina.hello",
3316- "description": "description of hello",
3317- "framework": "ubuntu-sdk-14.10-qml-dev2",
3318- "architecture": "all",
3319- "title": "hello",
3320- "hooks": {
3321- "hello": {
3322- "apparmor": "hello.json",
3323- "desktop": "hello.desktop"
3324- },
3325- "helloHelper": {
3326- "apparmor": "helloHelper-apparmor.json",
3327- "push-helper": "helloHelper.json"
3328- }
3329- },
3330- "version": "0.2",
3331- "maintainer": "Roberto Alsina <roberto.alsina@canonical.com>"
3332- }
3333-
3334-Here, we created a helloHelper entry in hooks that has an apparmor profile and an additional JSON file for the push-helper hook.
3335-
3336-helloHelper-apparmor.json must contain **only** the push-notification-client policy group::
3337-
3338- {
3339- "policy_groups": [
3340- "push-notification-client"
3341- ],
3342- "policy_version": 1.2
3343- }
3344-
3345-And helloHelper.json must have at least a exec key with the path to the helper executable relative to the json, and optionally
3346-an app_id key containing the short id of one of the apps in the package (in the format packagename_appname without a version).
3347-If the app_id is not specified, the helper will be used for all apps in the package::
3348-
3349- {
3350- "exec": "helloHelper",
3351- "app_id": "com.ubuntu.developer.ralsina.hello_hello"
3352- }
3353-
3354-.. note:: For deb packages, helpers should be installed into /usr/lib/ubuntu-push-client/legacy-helpers/ as part of the package.
3355-
3356-Helper Output Format
3357---------------------
3358-
3359-Helpers output has two parts, the postal message (in the "message" key) and a notification to be presented to the user (in the "notification" key).
3360-
3361-Here's a simple example::
3362-
3363- {
3364- "message": "foobar",
3365- "notification": {
3366- "tag": "foo",
3367- "card": {
3368- "summary": "yes",
3369- "body": "hello",
3370- "popup": true,
3371- "persist": true,
3372- "timestamp": 1407160197
3373- }
3374- "sound": "buzz.mp3",
3375- "vibrate": {
3376- "pattern": [200, 100],
3377- "repeat": 2
3378- }
3379- "emblem-counter": {
3380- "count": 12,
3381- "visible": true
3382- }
3383- }
3384- }
3385-
3386-The notification can contain a **tag** field, which can later be used by the `persistent notification management API. <#persistent-notification-management>`__
3387-
3388-:message: (optional) A JSON object that is passed as-is to the application via PopAll.
3389-:notification: (optional) Describes the user-facing notifications triggered by this push message.
3390-
3391-The notification can contain a **card**. A card describes a specific notification to be given to the user,
3392-and has the following fields:
3393-
3394-:summary: (required) a title. The card will not be presented if this is missing.
3395-:body: longer text, defaults to empty.
3396-:actions: If empty (the default), a bubble notification is non-clickable.
3397- If you add a URL, then bubble notifications are clickable and launch that URL. One use for this is using a URL like
3398- ``appid://com.ubuntu.developer.ralsina.hello/hello/current-user-version`` which will switch to the app or launch
3399- it if it's not running. See `URLDispatcher <https://wiki.ubuntu.com/URLDispatcher>`__ for more information.
3400-
3401-:icon: An icon relating to the event being notified. Defaults to empty (no icon);
3402- a secondary icon relating to the application will be shown as well, regardless of this field.
3403-:timestamp: Seconds since the unix epoch, only used for persist (for now). If zero or unset, defaults to current timestamp.
3404-:persist: Whether to show in notification centre; defaults to false
3405-:popup: Whether to show in a bubble. Users can disable this, and can easily miss them, so don't rely on it exclusively. Defaults to false.
3406-
3407-.. note:: Keep in mind that the precise way in which each field is presented to the user depends on factors such as
3408- whether it's shown as a bubble or in the notification centre, or even the version of Ubuntu Touch the user
3409- has on their device.
3410-
3411-The notification can contain a **sound** field. This is either a boolean (play a predetermined sound) or the path to a sound file. The user can disable it, so don't rely on it exclusively.
3412-Defaults to empty (no sound). The path is relative, and will be looked up in (a) the application's .local/share/<pkgname>, and (b)
3413-standard xdg dirs.
3414-
3415-The notification can contain a **vibrate** field, causing haptic feedback, which can be either a boolean (if true, vibrate a predetermined way) or an object that has the following content:
3416-
3417-:pattern: a list of integers describing a vibration pattern (duration of alternating vibration/no vibration times, in milliseconds).
3418-:repeat: number of times the pattern has to be repeated (defaults to 1, 0 is the same as 1).
3419-
3420-The notification can contain a **emblem-counter** field, with the following content:
3421-
3422-:count: a number to be displayed over the application's icon in the launcher.
3423-:visible: set to true to show the counter, or false to hide it.
3424-
3425-.. note:: Unlike other notifications, emblem-counter needs to be cleaned by the app itself.
3426- Please see `the persistent notification management section. <#persistent-notification-management>`__
3427-
3428-.. FIXME crosslink to hello example app on each method
3429-
3430-Security
3431-~~~~~~~~
3432-
3433-To use the push API, applications need to request permission in their security profile, using something like this::
3434-
3435- {
3436- "policy_groups": [
3437- "networking",
3438- "push-notification-client"
3439- ],
3440- "policy_version": 1.2
3441- }
3442-
3443-
3444-Ubuntu Push Server API
3445-----------------------
3446-
3447-The Ubuntu Push server is located at https://push.ubuntu.com and has a single endpoint: ``/notify``.
3448-To notify a user, your application has to do a POST with ``Content-type: application/json``.
3449-
3450-Here is an example of the POST body using all the fields::
3451-
3452- {
3453- "appid": "com.ubuntu.music_music",
3454- "expire_on": "2014-10-08T14:48:00.000Z",
3455- "token": "LeA4tRQG9hhEkuhngdouoA==",
3456- "clear_pending": true,
3457- "replace_tag": "tagname",
3458- "data": {
3459- "message": "foobar",
3460- "notification": {
3461- "card": {
3462- "summary": "yes",
3463- "body": "hello",
3464- "popup": true,
3465- "persist": true,
3466- "timestamp": 1407160197
3467- }
3468- "sound": "buzz.mp3",
3469- "tag": "foo",
3470- "vibrate": {
3471- "pattern": [200, 100],
3472- "repeat": 2
3473- }
3474- "emblem-counter": {
3475- "count": 12,
3476- "visible": true
3477- }
3478- }
3479- }
3480- }
3481-
3482-
3483-:appid: ID of the application that will receive the notification, as described in the client side documentation.
3484-:expire_on: Expiration date/time for this message, in `ISO8601 Extendend format <http://en.wikipedia.org/wiki/ISO_8601>`__
3485-:token: The token identifying the user+device to which the message is directed, as described in the client side documentation.
3486-:clear_pending: Discards all previous pending notifications. Usually in response to getting a "too-many-pending" error.
3487-:replace_tag: If there's a pending notification with the same tag, delete it before queuing this new one.
3488-:data: A JSON object.
3489-
3490-In this example, data is `what a helper would output <#helper-output-format>`__ but that's not necessarily the case.
3491-The content of the data field will be passed to the helper application which **has** to produce output in that format.
3492+.. include:: _common.txt
3493\ No newline at end of file

Subscribers

People subscribed via source and target branches