Merge lp:~jamesh/account-polld/facebook-plugin into lp:~phablet-team/account-polld/trunk

Proposed by James Henstridge
Status: Merged
Approved by: Sergio Schvezov
Approved revision: 17
Merged at revision: 8
Proposed branch: lp:~jamesh/account-polld/facebook-plugin
Merge into: lp:~phablet-team/account-polld/trunk
Diff against target: 404 lines (+323/-6) (has conflicts)
6 files modified
cmd/account-polld/account_manager.go (+13/-0)
cmd/account-polld/main.go (+5/-4)
plugins/facebook/facebook.go (+146/-0)
plugins/facebook/facebook_test.go (+157/-0)
plugins/gmail/plugin.go (+1/-1)
plugins/plugins.go (+1/-1)
Text conflict in cmd/account-polld/account_manager.go
To merge this branch: bzr merge lp:~jamesh/account-polld/facebook-plugin
Reviewer Review Type Date Requested Status
Sergio Schvezov Approve
Review via email: mp+226966@code.launchpad.net

Commit message

Add Facebook plugin. It currently doesn't support paginated results, and requires a token with "manage_notifications" permission.

Description of the change

First cut at the Facebook plugin. There are still a few issues to fix:

1. the facebook-microblog online-accounts token we are using does not have the "manage_notifications" permission, which is necessary to read the user's notifications list. I have been testing this using a program that drives the plugin directly using a token generated with Facebook's Graph Explorer app.

2. It isn't currently handling pagination of results. Notifications are automatically deleted when the user reads them though and it looks like it defaults to returning 5000 notifications, so in most cases it won't matter. The fix is to follow the "paging.next" links when we think there is more.

There is some simple tests for the response parsing logic using Go's test infrastructure. If it's okay to use Gustavo's gocheck, I'd be happy to update the tests accordingly.

The actual notification variables I'm sending are quite bare at the moment: I wasn't sure what to put in there, so I've just filled in the summary.

To post a comment you must log in.
12. By James Henstridge

Make use a pointer for the error values.

13. By James Henstridge

Wire in FB plugin.

14. By James Henstridge

Don't bother passing around pointers to slices unnecessarily.

Revision history for this message
Sergio Schvezov (sergiusens) wrote :

I've added a couple of inline minor comments.

wrt

I'll ping alex about the extra permission (I have a similar problem for gmail) and add you to an email I sent him (he promised to look into it tomorrow morning)

If you don't mind to use the "still in" bzr gocheck, feel free to do so; it's packaged as golang-gocheck-dev and it's import path is "launchpad.net/gocheck". If you have reasons to believe the github one has things that are direly needed we should start packaging it for ubuntu now (I haven't since I didn't find an urgency for it).

I have an MP with updates to the notifications API with descriptions of what everything is; just take the "Actions" one with a grain of salt as it's subject to change on the Post side.

review: Needs Fixing
Revision history for this message
Sergio Schvezov (sergiusens) wrote :

Oh, can you add a TODO comment for the pagination stuff?

15. By James Henstridge

Fix plugin's app ID, and a few other small issues brought up in review.

16. By James Henstridge

Run code through gofmt.

17. By James Henstridge

Use gocheck for the tests.

Revision history for this message
Sergio Schvezov (sergiusens) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'cmd/account-polld/account_manager.go'
2--- cmd/account-polld/account_manager.go 2014-07-15 10:40:13 +0000
3+++ cmd/account-polld/account_manager.go 2014-07-17 10:25:40 +0000
4@@ -59,7 +59,20 @@
5 log.Println("Polling set to", a.interval, "for", a.authData.AccountId)
6 select {
7 case <-time.After(a.interval):
8+<<<<<<< TREE
9 a.poll()
10+=======
11+ if n, err := a.plugin.Poll(&a.authData); err != nil {
12+ log.Print("Error while polling ", a.authData.AccountId, ": ", err)
13+ // penalizing the next poll
14+ a.interval += DEFAULT_INTERVAL
15+ continue
16+ } else if len(n) > 0 {
17+ // on success we reset the timeout to the default interval
18+ a.interval = DEFAULT_INTERVAL
19+ postWatch <- &PostWatch{notifications: n, appId: a.plugin.ApplicationId()}
20+ }
21+>>>>>>> MERGE-SOURCE
22 case <-a.terminate:
23 break L
24 }
25
26=== modified file 'cmd/account-polld/main.go'
27--- cmd/account-polld/main.go 2014-07-15 10:40:13 +0000
28+++ cmd/account-polld/main.go 2014-07-17 10:25:40 +0000
29@@ -24,13 +24,14 @@
30
31 "launchpad.net/account-polld/accounts"
32 "launchpad.net/account-polld/plugins"
33+ "launchpad.net/account-polld/plugins/facebook"
34 "launchpad.net/account-polld/plugins/gmail"
35 "launchpad.net/go-dbus/v1"
36 )
37
38 type PostWatch struct {
39 appId plugins.ApplicationId
40- notifications *[]plugins.Notification
41+ notifications []plugins.Notification
42 }
43
44 const (
45@@ -78,8 +79,8 @@
46 plugin = gmail.New()
47 case SERVICENAME_FACEBOOK:
48 // This is just stubbed until the plugin exists.
49- log.Println("Unhandled account with id", data.AccountId, "for", data.ServiceName)
50- continue L
51+ log.Println("Creating account with id", data.AccountId, "for", data.ServiceName)
52+ plugin = facebook.New()
53 case SERVICENAME_TWITTER:
54 // This is just stubbed until the plugin exists.
55 log.Println("Unhandled account with id", data.AccountId, "for", data.ServiceName)
56@@ -96,7 +97,7 @@
57
58 func postOffice(bus *dbus.Connection, postWatch chan *PostWatch) {
59 for post := range postWatch {
60- for _, n := range *post.notifications {
61+ for _, n := range post.notifications {
62 fmt.Println("Should be dispatching", n, "to the post office using", bus.UniqueName, "for", post.appId)
63 }
64 }
65
66=== added directory 'plugins/facebook'
67=== added file 'plugins/facebook/facebook.go'
68--- plugins/facebook/facebook.go 1970-01-01 00:00:00 +0000
69+++ plugins/facebook/facebook.go 2014-07-17 10:25:40 +0000
70@@ -0,0 +1,146 @@
71+/*
72+ Copyright 2014 Canonical Ltd.
73+ Authors: James Henstridge <james.henstridge@canonical.com>
74+
75+ This program is free software: you can redistribute it and/or modify it
76+ under the terms of the GNU General Public License version 3, as published
77+ by the Free Software Foundation.
78+
79+ This program is distributed in the hope that it will be useful, but
80+ WITHOUT ANY WARRANTY; without even the implied warranties of
81+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
82+ PURPOSE. See the GNU General Public License for more details.
83+
84+ You should have received a copy of the GNU General Public License along
85+ with this program. If not, see <http://www.gnu.org/licenses/>.
86+*/
87+
88+package facebook
89+
90+import (
91+ "encoding/json"
92+ "net/http"
93+ "net/url"
94+
95+ "launchpad.net/account-polld/accounts"
96+ "launchpad.net/account-polld/plugins"
97+)
98+
99+var baseUrl, _ = url.Parse("https://graph.facebook.com/v2.0/")
100+
101+type fbPlugin struct {
102+ lastUpdate string
103+}
104+
105+func New() plugins.Plugin {
106+ return &fbPlugin{}
107+}
108+
109+func (p *fbPlugin) ApplicationId() plugins.ApplicationId {
110+ return "com.ubuntu.developer.webapps.webapp-facebook_webapp-facebook"
111+}
112+
113+func (p *fbPlugin) request(authData *accounts.AuthData, path string) (*http.Response, error) {
114+ // Resolve path relative to Graph API base URL, and add access token
115+ u, err := baseUrl.Parse(path)
116+ if err != nil {
117+ return nil, err
118+ }
119+ query := u.Query()
120+ query.Add("access_token", authData.AccessToken)
121+ u.RawQuery = query.Encode()
122+
123+ return http.Get(u.String())
124+}
125+
126+func (p *fbPlugin) parseResponse(resp *http.Response) ([]plugins.Notification, error) {
127+ defer resp.Body.Close()
128+ decoder := json.NewDecoder(resp.Body)
129+
130+ if resp.StatusCode != http.StatusOK {
131+ var result errorDoc
132+ if err := decoder.Decode(&result); err != nil {
133+ return nil, err
134+ }
135+ return nil, &result.Error
136+ }
137+
138+ // TODO: Follow the "paging.next" link if we get more than one
139+ // page full of notifications. The default limit seems to be
140+ // 5000 though, which we are unlikely to hit, since
141+ // notifications are deleted once read.
142+ var result notificationDoc
143+ if err := decoder.Decode(&result); err != nil {
144+ return nil, err
145+ }
146+ notifications := []plugins.Notification{}
147+ latestUpdate := ""
148+ for _, n := range result.Data {
149+ if n.UpdatedTime <= p.lastUpdate {
150+ continue
151+ }
152+ notifications = append(notifications, plugins.Notification{
153+ Card: plugins.Card{
154+ Summary: n.Title,
155+ },
156+ })
157+ if n.UpdatedTime > latestUpdate {
158+ latestUpdate = n.UpdatedTime
159+ }
160+ }
161+ p.lastUpdate = latestUpdate
162+ return notifications, nil
163+}
164+
165+func (p *fbPlugin) Poll(authData *accounts.AuthData) ([]plugins.Notification, error) {
166+ resp, err := p.request(authData, "me/notifications")
167+ if err != nil {
168+ return nil, err
169+ }
170+ return p.parseResponse(resp)
171+}
172+
173+// The notifications response format is described here:
174+// https://developers.facebook.com/docs/graph-api/reference/v2.0/user/notifications/
175+type notificationDoc struct {
176+ Data []notification `json:"data"`
177+ Paging struct {
178+ Previous string `json:"previous"`
179+ Next string `json:"next"`
180+ } `json:"paging"`
181+}
182+
183+type notification struct {
184+ Id string `json:"id"`
185+ From object `json:"from"`
186+ To object `json:"to"`
187+ CreatedTime string `json:"created_time"`
188+ UpdatedTime string `json:"updated_time"`
189+ Title string `json:"title"`
190+ Link string `json:"link"`
191+ Application object `json:"application"`
192+ Unread int `json:"unread"`
193+ Object object `json:"object"`
194+}
195+
196+type object struct {
197+ Id string `json:"id"`
198+ Name string `json:"name"`
199+}
200+
201+// The error response format is described here:
202+// https://developers.facebook.com/docs/graph-api/using-graph-api/v2.0#errors
203+type errorDoc struct {
204+ Error GraphError `json:"error"`
205+}
206+
207+type GraphError struct {
208+ Message string `json:"message"`
209+ Type string `json:"type"`
210+ Code int `json:"code"`
211+ Subcode int `json:"error_subcode"`
212+}
213+
214+func (err *GraphError) Error() string {
215+ return err.Message
216+}
217
218=== added file 'plugins/facebook/facebook_test.go'
219--- plugins/facebook/facebook_test.go 1970-01-01 00:00:00 +0000
220+++ plugins/facebook/facebook_test.go 2014-07-17 10:25:40 +0000
221@@ -0,0 +1,157 @@
222+/*
223+ Copyright 2014 Canonical Ltd.
224+ Authors: James Henstridge <james.henstridge@canonical.com>
225+
226+ This program is free software: you can redistribute it and/or modify it
227+ under the terms of the GNU General Public License version 3, as published
228+ by the Free Software Foundation.
229+
230+ This program is distributed in the hope that it will be useful, but
231+ WITHOUT ANY WARRANTY; without even the implied warranties of
232+ MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
233+ PURPOSE. See the GNU General Public License for more details.
234+
235+ You should have received a copy of the GNU General Public License along
236+ with this program. If not, see <http://www.gnu.org/licenses/>.
237+*/
238+package facebook
239+
240+import (
241+ "bytes"
242+ "io"
243+ "net/http"
244+ "testing"
245+
246+ . "launchpad.net/gocheck"
247+)
248+
249+type S struct{}
250+
251+func init() {
252+ Suite(S{})
253+}
254+
255+func TestAll(t *testing.T) {
256+ TestingT(t)
257+}
258+
259+// closeWraper adds a dummy Close() method to a reader
260+type closeWrapper struct {
261+ io.Reader
262+}
263+
264+func (r closeWrapper) Close() error {
265+ return nil
266+}
267+
268+const (
269+ errorBody = `
270+{
271+ "error": {
272+ "message": "Message describing the error",
273+ "type": "OAuthException",
274+ "code": 190 ,
275+ "error_subcode": 460
276+ }
277+}`
278+ notificationsBody = `
279+{
280+ "data": [
281+ {
282+ "id": "notif_id",
283+ "from": {
284+ "id": "sender_id",
285+ "name": "Sender"
286+ },
287+ "to": {
288+ "id": "recipient_id",
289+ "name": "Recipient"
290+ },
291+ "created_time": "2014-07-12T09:51:57+0000",
292+ "updated_time": "2014-07-12T09:51:57+0000",
293+ "title": "Sender posted on your timeline: \"The message...\"",
294+ "link": "http://www.facebook.com/recipient/posts/id",
295+ "application": {
296+ "name": "Wall",
297+ "namespace": "wall",
298+ "id": "2719290516"
299+ },
300+ "unread": 1
301+ },
302+ {
303+ "id": "notif_1105650586_80600069",
304+ "from": {
305+ "id": "sender2_id",
306+ "name": "Sender2"
307+ },
308+ "to": {
309+ "id": "recipient_id",
310+ "name": "Recipient"
311+ },
312+ "created_time": "2014-07-08T06:17:52+0000",
313+ "updated_time": "2014-07-08T06:17:52+0000",
314+ "title": "Sender2's birthday was on July 7.",
315+ "link": "http://www.facebook.com/profile.php?id=xxx&ref=brem",
316+ "application": {
317+ "name": "Gifts",
318+ "namespace": "superkarma",
319+ "id": "329122197162272"
320+ },
321+ "unread": 1,
322+ "object": {
323+ "id": "sender2_id",
324+ "name": "Sender2"
325+ }
326+ }
327+ ],
328+ "paging": {
329+ "previous": "https://graph.facebook.com/v2.0/recipient/notifications?limit=5000&since=1405158717&__paging_token=enc_AewDzwIQmWOwPNO-36GaZsaJAog8l93HQ7uLEO-gp1Tb6KCiolXfzMCcGY2KjrJJsDJXdDmNJObICr5dewfMZgGs",
330+ "next": "https://graph.facebook.com/v2.0/recipient/notifications?limit=5000&until=1404705077&__paging_token=enc_Aewlhut5DQyhqtLNr7pLCMlYU012t4XY7FOt7cooz4wsWIWi-Jqz0a0IDnciJoeLu2vNNQkbtOpCmEmsVsN4hkM4"
331+ },
332+ "summary": [
333+ ]
334+}
335+`
336+)
337+
338+func (s S) TestParseNotifications(c *C) {
339+ resp := &http.Response{
340+ StatusCode: http.StatusOK,
341+ Body: closeWrapper{bytes.NewReader([]byte(notificationsBody))},
342+ }
343+ p := &fbPlugin{}
344+ notifications, err := p.parseResponse(resp)
345+ c.Assert(err, IsNil)
346+ c.Assert(len(notifications), Equals, 2)
347+ c.Check(notifications[0].Card.Summary, Equals, "Sender posted on your timeline: \"The message...\"")
348+ c.Check(notifications[1].Card.Summary, Equals, "Sender2's birthday was on July 7.")
349+ c.Check(p.lastUpdate, Equals, "2014-07-12T09:51:57+0000")
350+}
351+
352+func (s S) TestIgnoreOldNotifications(c *C) {
353+ resp := &http.Response{
354+ StatusCode: http.StatusOK,
355+ Body: closeWrapper{bytes.NewReader([]byte(notificationsBody))},
356+ }
357+ p := &fbPlugin{lastUpdate: "2014-07-08T06:17:52+0000"}
358+ notifications, err := p.parseResponse(resp)
359+ c.Assert(err, IsNil)
360+ c.Assert(len(notifications), Equals, 1)
361+ c.Check(notifications[0].Card.Summary, Equals, "Sender posted on your timeline: \"The message...\"")
362+ c.Check(p.lastUpdate, Equals, "2014-07-12T09:51:57+0000")
363+}
364+
365+func (s S) TestErrorResponse(c *C) {
366+ resp := &http.Response{
367+ StatusCode: http.StatusBadRequest,
368+ Body: closeWrapper{bytes.NewReader([]byte(errorBody))},
369+ }
370+ p := &fbPlugin{}
371+ notifications, err := p.parseResponse(resp)
372+ c.Check(notifications, IsNil)
373+ c.Assert(err, Not(IsNil))
374+ graphErr := err.(*GraphError)
375+ c.Check(graphErr.Message, Equals, "Message describing the error")
376+ c.Check(graphErr.Code, Equals, 190)
377+ c.Check(graphErr.Subcode, Equals, 460)
378+}
379
380=== modified file 'plugins/gmail/plugin.go'
381--- plugins/gmail/plugin.go 2014-07-11 20:23:49 +0000
382+++ plugins/gmail/plugin.go 2014-07-17 10:25:40 +0000
383@@ -31,7 +31,7 @@
384 return &GmailPlugin{}
385 }
386
387-func (p *GmailPlugin) Poll(authData *accounts.AuthData) (*[]plugins.Notification, error) {
388+func (p *GmailPlugin) Poll(authData *accounts.AuthData) ([]plugins.Notification, error) {
389 return nil, nil
390 }
391
392
393=== modified file 'plugins/plugins.go'
394--- plugins/plugins.go 2014-07-11 20:23:49 +0000
395+++ plugins/plugins.go 2014-07-17 10:25:40 +0000
396@@ -31,7 +31,7 @@
397 // ApplicationId returns the APP_ID of the delivery target for Post Office.
398 type Plugin interface {
399 ApplicationId() ApplicationId
400- Poll(*accounts.AuthData) (*[]Notification, error)
401+ Poll(*accounts.AuthData) ([]Notification, error)
402 }
403
404 // AuthTokens is a map with tokens the plugins are to use to make requests.

Subscribers

People subscribed via source and target branches