Merge lp:~sergiusens/account-polld/facebook_tweaks into lp:~ubuntu-push-hackers/account-polld/trunk

Proposed by Sergio Schvezov
Status: Merged
Approved by: Sergio Schvezov
Approved revision: 59
Merged at revision: 54
Proposed branch: lp:~sergiusens/account-polld/facebook_tweaks
Merge into: lp:~ubuntu-push-hackers/account-polld/trunk
Prerequisite: lp:~sergiusens/account-polld/gmail_persist
Diff against target: 570 lines (+226/-105)
6 files modified
cmd/account-polld/main.go (+2/-2)
plugins/facebook/facebook.go (+125/-27)
plugins/facebook/facebook_test.go (+8/-5)
plugins/gmail/gmail.go (+20/-61)
plugins/plugins.go (+58/-4)
po/account-polld.pot (+13/-6)
To merge this branch: bzr merge lp:~sergiusens/account-polld/facebook_tweaks
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
PS Jenkins bot continuous-integration Approve
Review via email: mp+230174@code.launchpad.net

Commit message

Facebook pretty notifications, persistence (with multiple account support)

Description of the change

This is not just facebook tweaks, but also some changes to have multi account logging and persistance per plugin

To post a comment you must log in.
59. By Sergio Schvezov

Updated translation template

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Roberto Alsina (ralsina) :
review: Approve
60. By Sergio Schvezov

Filter notifications for facebook at start, don't do a consolidated message if no usernames where collected log more for facebook for now

61. By Sergio Schvezov

Updated translation template

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/account-polld/main.go'
2--- cmd/account-polld/main.go 2014-08-01 19:27:53 +0000
3+++ cmd/account-polld/main.go 2014-08-11 18:35:47 +0000
4@@ -94,11 +94,11 @@
5 switch data.ServiceName {
6 case SERVICENAME_GMAIL:
7 log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
8- plugin = gmail.New()
9+ plugin = gmail.New(data.AccountId)
10 case SERVICENAME_FACEBOOK:
11 // This is just stubbed until the plugin exists.
12 log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
13- plugin = facebook.New()
14+ plugin = facebook.New(data.AccountId)
15 case SERVICENAME_TWITTER:
16 // This is just stubbed until the plugin exists.
17 log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
18
19=== modified file 'plugins/facebook/facebook.go'
20--- plugins/facebook/facebook.go 2014-08-01 20:30:06 +0000
21+++ plugins/facebook/facebook.go 2014-08-11 18:35:47 +0000
22@@ -1,6 +1,7 @@
23 /*
24 Copyright 2014 Canonical Ltd.
25 Authors: James Henstridge <james.henstridge@canonical.com>
26+ Sergio Schvezov <sergio.schvezov@canonical.com>
27
28 This program is free software: you can redistribute it and/or modify it
29 under the terms of the GNU General Public License version 3, as published
30@@ -19,24 +20,63 @@
31
32 import (
33 "encoding/json"
34+ "fmt"
35 "net/http"
36 "net/url"
37+ "os"
38+ "strings"
39 "time"
40
41+ "log"
42+
43 "launchpad.net/account-polld/accounts"
44+ "launchpad.net/account-polld/gettext"
45 "launchpad.net/account-polld/plugins"
46 )
47
48-const facebookTime = "2006-01-02T15:04:05-0700"
49+const (
50+ facebookTime = "2006-01-02T15:04:05-0700"
51+ maxIndividualNotifications = 4
52+ consolidatedNotificationsIndexStart = maxIndividualNotifications
53+ pluginName = "facebook"
54+)
55
56 var baseUrl, _ = url.Parse("https://graph.facebook.com/v2.0/")
57
58+type timeStamp string
59+
60+func (stamp timeStamp) persist(accountId uint) (err error) {
61+ err = plugins.Persist(pluginName, accountId, stamp)
62+ if err != nil {
63+ log.Print("facebook plugin", accountId, ": failed to save state: ", err)
64+ }
65+ return nil
66+}
67+
68+func timeStampFromStorage(accountId uint) (stamp timeStamp, err error) {
69+ err = plugins.FromPersist(pluginName, accountId, &stamp)
70+ if err != nil {
71+ return stamp, err
72+ }
73+ if _, err := time.Parse(facebookTime, string(stamp)); err == nil {
74+ return stamp, err
75+ }
76+ return stamp, nil
77+}
78+
79 type fbPlugin struct {
80- lastUpdate string
81+ lastUpdate timeStamp
82+ accountId uint
83 }
84
85-func New() plugins.Plugin {
86- return &fbPlugin{}
87+func New(accountId uint) plugins.Plugin {
88+ stamp, err := timeStampFromStorage(accountId)
89+ if err != nil {
90+ log.Print("facebook plugin ", accountId, ": cannot load previous state from storage: ", err)
91+ } else {
92+ log.Print("facebook plugin ", accountId, ": last state loaded from storage")
93+ }
94+ return &fbPlugin{lastUpdate: stamp, accountId: accountId}
95 }
96
97 func (p *fbPlugin) ApplicationId() plugins.ApplicationId {
98@@ -75,29 +115,75 @@
99 // page full of notifications. The default limit seems to be
100 // 5000 though, which we are unlikely to hit, since
101 // notifications are deleted once read.
102+ // TODO filter out of date messages before operating
103 var result notificationDoc
104 if err := decoder.Decode(&result); err != nil {
105 return nil, err
106 }
107- pushMsg := []plugins.PushMessage{}
108- latestUpdate := ""
109+
110+ var validNotifications []notification
111+ latestUpdate := p.lastUpdate
112 for _, n := range result.Data {
113 if n.UpdatedTime <= p.lastUpdate {
114- continue
115+ log.Println("facebook plugin: skipping notification", n.Id, "as", n.UpdatedTime, "is older than", p.lastUpdate)
116+ } else if n.Unread != 1 {
117+ log.Println("facebook plugin: skipping notification", n.Id, "as it's read:", n.Unread)
118+ } else {
119+ log.Println("facebook plugin: valid notification", n.Id, "dated:", n.UpdatedTime, "and read status:", n.Unread)
120+ validNotifications = append(validNotifications, n)
121+ if n.UpdatedTime > latestUpdate {
122+ latestUpdate = n.UpdatedTime
123+ }
124 }
125- // TODO proper action needed
126- action := "https://m.facebook.com"
127+ }
128+ p.lastUpdate = latestUpdate
129+ p.lastUpdate.persist(p.accountId)
130+
131+ pushMsg := []plugins.PushMessage{}
132+ for _, n := range validNotifications {
133 epoch := toEpoch(n.UpdatedTime)
134- pushMsg = append(pushMsg, *plugins.NewStandardPushMessage(n.Title, "", action, "", epoch))
135- if n.UpdatedTime > latestUpdate {
136- latestUpdate = n.UpdatedTime
137- }
138- }
139- p.lastUpdate = latestUpdate
140+ pushMsg = append(pushMsg, *plugins.NewStandardPushMessage(n.From.Name, n.Title, n.Link, n.picture(), epoch))
141+ if len(pushMsg) == maxIndividualNotifications {
142+ break
143+ }
144+ }
145+
146+ // Now we consolidate the remaining statuses
147+ if len(validNotifications) > len(pushMsg) && len(validNotifications) >= consolidatedNotificationsIndexStart {
148+ usernamesMap := make(map[string]bool)
149+ for _, n := range result.Data[consolidatedNotificationsIndexStart:] {
150+ if _, ok := usernamesMap[n.From.Name]; !ok {
151+ usernamesMap[n.From.Name] = true
152+ }
153+ }
154+ usernames := []string{}
155+ for k, _ := range usernamesMap {
156+ usernames = append(usernames, k)
157+ // we don't too many usernames listed, this is a hard number
158+ if len(usernames) > 10 {
159+ usernames = append(usernames, "...")
160+ break
161+ }
162+ }
163+ if len(usernames) > 0 {
164+ // TRANSLATORS: This represents a notification summary about more facebook notifications
165+ summary := gettext.Gettext("Multiple more notifications")
166+ // TRANSLATORS: This represents a notification body with the comma separated facebook usernames
167+ body := fmt.Sprintf(gettext.Gettext("From %s"), strings.Join(usernames, ", "))
168+ action := "https://m.facebook.com"
169+ epoch := time.Now().Unix()
170+ pushMsg = append(pushMsg, *plugins.NewStandardPushMessage(summary, body, action, "", epoch))
171+ }
172+ }
173+
174 return pushMsg, nil
175 }
176
177 func (p *fbPlugin) Poll(authData *accounts.AuthData) ([]plugins.PushMessage, error) {
178+ // This envvar check is to ease testing.
179+ if token := os.Getenv("ACCOUNT_POLLD_TOKEN_FACEBOOK"); token != "" {
180+ authData.AccessToken = token
181+ }
182 resp, err := p.request(authData, "me/notifications")
183 if err != nil {
184 return nil, err
185@@ -105,8 +191,8 @@
186 return p.parseResponse(resp)
187 }
188
189-func toEpoch(timestamp string) int64 {
190- if t, err := time.Parse(facebookTime, timestamp); err == nil {
191+func toEpoch(stamp timeStamp) int64 {
192+ if t, err := time.Parse(facebookTime, string(stamp)); err == nil {
193 return t.Unix()
194 }
195 return time.Now().Unix()
196@@ -123,16 +209,28 @@
197 }
198
199 type notification struct {
200- Id string `json:"id"`
201- From object `json:"from"`
202- To object `json:"to"`
203- CreatedTime string `json:"created_time"`
204- UpdatedTime string `json:"updated_time"`
205- Title string `json:"title"`
206- Link string `json:"link"`
207- Application object `json:"application"`
208- Unread int `json:"unread"`
209- Object object `json:"object"`
210+ Id string `json:"id"`
211+ From object `json:"from"`
212+ To object `json:"to"`
213+ CreatedTime timeStamp `json:"created_time"`
214+ UpdatedTime timeStamp `json:"updated_time"`
215+ Title string `json:"title"`
216+ Link string `json:"link"`
217+ Application object `json:"application"`
218+ Unread int `json:"unread"`
219+ Object object `json:"object"`
220+}
221+
222+func (n notification) picture() string {
223+ u, err := baseUrl.Parse(fmt.Sprintf("%s/picture", n.From.Id))
224+ if err != nil {
225+ log.Println("facebook plugin: cannot get picture for", n.Id)
226+ return ""
227+ }
228+ query := u.Query()
229+ query.Add("redirect", "true")
230+ u.RawQuery = query.Encode()
231+ return u.String()
232 }
233
234 type object struct {
235
236=== modified file 'plugins/facebook/facebook_test.go'
237--- plugins/facebook/facebook_test.go 2014-07-23 10:45:24 +0000
238+++ plugins/facebook/facebook_test.go 2014-08-11 18:35:47 +0000
239@@ -133,9 +133,11 @@
240 messages, err := p.parseResponse(resp)
241 c.Assert(err, IsNil)
242 c.Assert(len(messages), Equals, 2)
243- c.Check(messages[0].Notification.Card.Summary, Equals, "Sender posted on your timeline: \"The message...\"")
244- c.Check(messages[1].Notification.Card.Summary, Equals, "Sender2's birthday was on July 7.")
245- c.Check(p.lastUpdate, Equals, "2014-07-12T09:51:57+0000")
246+ c.Check(messages[0].Notification.Card.Summary, Equals, "Sender")
247+ c.Check(messages[0].Notification.Card.Body, Equals, "Sender posted on your timeline: \"The message...\"")
248+ c.Check(messages[1].Notification.Card.Summary, Equals, "Sender2")
249+ c.Check(messages[1].Notification.Card.Body, Equals, "Sender2's birthday was on July 7.")
250+ c.Check(p.lastUpdate, Equals, timeStamp("2014-07-12T09:51:57+0000"))
251 }
252
253 func (s S) TestIgnoreOldNotifications(c *C) {
254@@ -147,8 +149,9 @@
255 messages, err := p.parseResponse(resp)
256 c.Assert(err, IsNil)
257 c.Assert(len(messages), Equals, 1)
258- c.Check(messages[0].Notification.Card.Summary, Equals, "Sender posted on your timeline: \"The message...\"")
259- c.Check(p.lastUpdate, Equals, "2014-07-12T09:51:57+0000")
260+ c.Check(messages[0].Notification.Card.Summary, Equals, "Sender")
261+ c.Check(messages[0].Notification.Card.Body, Equals, "Sender posted on your timeline: \"The message...\"")
262+ c.Check(p.lastUpdate, Equals, timeStamp("2014-07-12T09:51:57+0000"))
263 }
264
265 func (s S) TestErrorResponse(c *C) {
266
267=== modified file 'plugins/gmail/gmail.go'
268--- plugins/gmail/gmail.go 2014-08-11 18:35:47 +0000
269+++ plugins/gmail/gmail.go 2014-08-11 18:35:47 +0000
270@@ -18,14 +18,12 @@
271 package gmail
272
273 import (
274- "bufio"
275 "encoding/json"
276 "fmt"
277 "net/http"
278 "net/mail"
279 "net/url"
280 "os"
281- "path/filepath"
282 "sort"
283 "time"
284
285@@ -41,6 +39,7 @@
286 gmailDispatchUrl = "https://mail.google.com/mail/mu/mp/#cv/priority/^smartlabel_%s/%s"
287 // this means 3 individual messages + 1 bundled notification.
288 individualNotificationsLimit = 2
289+ pluginName = "gmail"
290 )
291
292 type reportedIdMap map[string]time.Time
293@@ -53,87 +52,47 @@
294 // trackDelta defines how old messages can be before removed from tracking
295 var trackDelta = time.Duration(time.Hour * 24 * 7)
296
297-var idStoreSubPath = filepath.Join("gmail", "reportedIds.json")
298-
299 type GmailPlugin struct {
300 // reportedIds holds the messages that have already been notified. This
301 // approach is taken against timestamps as it avoids needing to call
302 // get on the message.
303 reportedIds reportedIdMap
304+ accountId uint
305 }
306
307-func idsFromStorage() (ids reportedIdMap, err error) {
308- var p string
309- defer func() {
310- if err != nil {
311- if p != "" {
312- os.Remove(p)
313- }
314- }
315- }()
316- p, err = plugins.DataFind(idStoreSubPath)
317- if err != nil {
318- return nil, err
319- }
320- file, err := os.Open(p)
321- if err != nil {
322- return nil, err
323- }
324- defer file.Close()
325- jsonReader := json.NewDecoder(file)
326- if err := jsonReader.Decode(&ids); err != nil {
327- return nil, err
328- }
329-
330+func idsFromPersist(accountId uint) (ids reportedIdMap, err error) {
331+ err = plugins.FromPersist(pluginName, accountId, &ids)
332+ if err != nil {
333+ return nil, err
334+ }
335 // discard old ids
336 timestamp := time.Now()
337 for k, v := range ids {
338 delta := timestamp.Sub(v)
339 if delta > trackDelta {
340- log.Println("gmail plugin: deleting", k, "as", delta, "is greater than", trackDelta)
341+ log.Print("gmail plugin ", accountId, ": deleting ", k, " as ", delta, " is greater than ", trackDelta)
342 delete(ids, k)
343 }
344 }
345 return ids, nil
346 }
347
348-func (ids reportedIdMap) persist() (err error) {
349- var p string
350- defer func() {
351- if err != nil {
352- log.Println("gmail plugin: failed to save state:", err)
353- if p != "" {
354- os.Remove(p)
355- }
356- }
357- }()
358- p, err = plugins.DataEnsure(idStoreSubPath)
359- if err != nil {
360- return err
361- }
362- file, err := os.Create(p)
363- if err != nil {
364- return err
365- }
366- defer file.Close()
367- w := bufio.NewWriter(file)
368- defer w.Flush()
369- jsonWriter := json.NewEncoder(w)
370- if err := jsonWriter.Encode(ids); err != nil {
371- return err
372- }
373-
374+func (ids reportedIdMap) persist(accountId uint) (err error) {
375+ err = plugins.Persist(pluginName, accountId, ids)
376+ if err != nil {
377+ log.Print("gmail plugin ", accountId, ": failed to save state: ", err)
378+ }
379 return nil
380 }
381
382-func New() *GmailPlugin {
383- reportedIds, err := idsFromStorage()
384+func New(accountId uint) *GmailPlugin {
385+ reportedIds, err := idsFromPersist(accountId)
386 if err != nil {
387- log.Println("gmail plugin: cannot load previous state from storage:", err)
388+ log.Print("gmail plugin ", accountId, ": cannot load previous state from storage: ", err)
389 } else {
390- log.Println("gmail plugin: report state loaded from storage")
391+ log.Print("gmail plugin ", accountId, ": last state loaded from storage")
392 }
393- return &GmailPlugin{reportedIds}
394+ return &GmailPlugin{reportedIds: reportedIds, accountId: accountId}
395 }
396
397 func (p *GmailPlugin) ApplicationId() plugins.ApplicationId {
398@@ -202,7 +161,7 @@
399 epoch := hdr.getEpoch()
400 pushMsgMap[msg.ThreadId] = *plugins.NewStandardPushMessage(summary, body, action, "", epoch)
401 } else {
402- log.Println("gmail: skipping message id", msg.Id, "with date", msgStamp, "older than", timeDelta)
403+ log.Print("gmail plugin ", p.accountId, ": skipping message id ", msg.Id, " with date ", msgStamp, " older than ", timeDelta)
404 }
405 }
406 var pushMsg []plugins.PushMessage
407@@ -268,7 +227,7 @@
408 ids[msg.Id] = time.Now()
409 }
410 p.reportedIds = ids
411- p.reportedIds.persist()
412+ p.reportedIds.persist(p.accountId)
413 return reportMsg
414 }
415
416
417=== modified file 'plugins/plugins.go'
418--- plugins/plugins.go 2014-08-11 18:35:47 +0000
419+++ plugins/plugins.go 2014-08-11 18:35:47 +0000
420@@ -18,9 +18,13 @@
421 package plugins
422
423 import (
424+ "bufio"
425+ "encoding/json"
426 "errors"
427+ "fmt"
428 "os"
429 "path/filepath"
430+ "reflect"
431
432 "launchpad.net/account-polld/accounts"
433 "launchpad.net/go-xdg/v0"
434@@ -166,12 +170,62 @@
435
436 var cmdName string
437
438-func DataFind(path string) (string, error) {
439- return xdg.Data.Find(filepath.Join(cmdName, path))
440+// Persist stores the plugins data in a common location to a json file
441+// from which it can recover later
442+func Persist(pluginName string, accountId uint, data interface{}) (err error) {
443+ var p string
444+ defer func() {
445+ if err != nil && p != "" {
446+ os.Remove(p)
447+ }
448+ }()
449+ p, err = xdg.Data.Ensure(filepath.Join(cmdName, fmt.Sprintf("%s-%d.json", pluginName, accountId)))
450+ if err != nil {
451+ return err
452+ }
453+ file, err := os.Create(p)
454+ if err != nil {
455+ return err
456+ }
457+ defer file.Close()
458+ w := bufio.NewWriter(file)
459+ defer w.Flush()
460+ jsonWriter := json.NewEncoder(w)
461+ if err := jsonWriter.Encode(data); err != nil {
462+ return err
463+ }
464+ return nil
465 }
466
467-func DataEnsure(path string) (string, error) {
468- return xdg.Data.Ensure(filepath.Join(cmdName, path))
469+// FromPersist restores the plugins data from a common location which
470+// was stored in a json file
471+func FromPersist(pluginName string, accountId uint, data interface{}) (err error) {
472+ if reflect.ValueOf(data).Kind() != reflect.Ptr {
473+ return errors.New("decode target is not a pointer")
474+ }
475+ var p string
476+ defer func() {
477+ if err != nil {
478+ if p != "" {
479+ os.Remove(p)
480+ }
481+ }
482+ }()
483+ p, err = xdg.Data.Find(filepath.Join(cmdName, fmt.Sprintf("%s-%d.json", pluginName, accountId)))
484+ if err != nil {
485+ return err
486+ }
487+ file, err := os.Open(p)
488+ if err != nil {
489+ return err
490+ }
491+ defer file.Close()
492+ jsonReader := json.NewDecoder(file)
493+ if err := jsonReader.Decode(&data); err != nil {
494+ return err
495+ }
496+
497+ return nil
498 }
499
500 // DefaultSound returns the path to the default sound for a Notification
501
502=== modified file 'po/account-polld.pot'
503--- po/account-polld.pot 2014-08-06 13:04:18 +0000
504+++ po/account-polld.pot 2014-08-11 18:35:47 +0000
505@@ -8,7 +8,7 @@
506 msgstr ""
507 "Project-Id-Version: account-polld\n"
508 "Report-Msgid-Bugs-To: \n"
509-"POT-Creation-Date: 2014-08-06 10:04-0300\n"
510+"POT-Creation-Date: 2014-08-11 15:35-0300\n"
511 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
512 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
513 "Language-Team: LANGUAGE <LL@li.org>\n"
514@@ -29,7 +29,9 @@
515 msgstr ""
516
517 #. TRANSLATORS: This represents a notification body with the comma separated twitter usernames
518+#. TRANSLATORS: This represents a notification body with the comma separated facebook usernames
519 #: plugins/twitter/twitter.go:130 plugins/twitter/twitter.go:187
520+#: plugins/facebook/facebook.go:172
521 #, c-format
522 msgid "From %s"
523 msgstr ""
524@@ -40,19 +42,19 @@
525 msgstr ""
526
527 #. TRANSLATORS: the %s is an appended "from" corresponding to an specific email thread
528-#: plugins/gmail/gmail.go:116
529+#: plugins/gmail/gmail.go:153
530 #, c-format
531 msgid ", %s"
532 msgstr ""
533
534 #. TRANSLATORS: the %s is the "from" header corresponding to a specific email
535-#: plugins/gmail/gmail.go:119
536+#: plugins/gmail/gmail.go:156
537 #, c-format
538 msgid "%s"
539 msgstr ""
540
541 #. TRANSLATORS: the first %s refers to the email "subject", the second %s refers "from"
542-#: plugins/gmail/gmail.go:121
543+#: plugins/gmail/gmail.go:158
544 #, c-format
545 msgid ""
546 "%s\n"
547@@ -60,16 +62,21 @@
548 msgstr ""
549
550 #. TRANSLATORS: This represents a notification summary about more unread emails
551-#: plugins/gmail/gmail.go:139
552+#: plugins/gmail/gmail.go:176
553 msgid "More unread emails available"
554 msgstr ""
555
556 #. TRANSLATORS: the first %d refers to approximate additionl email message count
557-#: plugins/gmail/gmail.go:143
558+#: plugins/gmail/gmail.go:180
559 #, c-format
560 msgid "You have an approximate of %d additional unread messages"
561 msgstr ""
562
563+#. TRANSLATORS: This represents a notification summary about more facebook notifications
564+#: plugins/facebook/facebook.go:170
565+msgid "Multiple more notifications"
566+msgstr ""
567+
568 #: data/account-polld.desktop.tr.h:1
569 msgid "Notifications"
570 msgstr ""

Subscribers

People subscribed via source and target branches