Merge lp:~sergiusens/account-polld/refresh_accounts into lp:~phablet-team/account-polld/trunk

Proposed by Sergio Schvezov
Status: Merged
Merged at revision: 21
Proposed branch: lp:~sergiusens/account-polld/refresh_accounts
Merge into: lp:~phablet-team/account-polld/trunk
Diff against target: 634 lines (+231/-68)
12 files modified
accounts/account-watcher.c (+51/-27)
accounts/account-watcher.h (+3/-0)
accounts/accounts.c (+17/-2)
accounts/accounts.go (+43/-12)
cmd/account-polld/account_manager.go (+30/-18)
cmd/account-polld/main.go (+4/-2)
cmd/account-watcher-test/main.go (+1/-1)
plugins/facebook/facebook.go (+3/-0)
plugins/facebook/facebook_test.go (+25/-5)
plugins/plugins.go (+9/-1)
plugins/twitter/twitter.go (+12/-0)
plugins/twitter/twitter_test.go (+33/-0)
To merge this branch: bzr merge lp:~sergiusens/account-polld/refresh_accounts
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Roberto Alsina Pending
Review via email: mp+228201@code.launchpad.net

Commit message

Add support for handling token expiry

Description of the change

This is part of a series of commits from https://code.launchpad.net/~jamesh/account-polld/account-refresh/+merge/227759
I've taken a subset from it which would be good to have now and leave the other part for review.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'accounts/account-watcher.c'
2--- accounts/account-watcher.c 2014-07-11 12:01:42 +0000
3+++ accounts/account-watcher.c 2014-07-24 20:44:55 +0000
4@@ -82,7 +82,7 @@
5 g_free(info);
6 }
7
8-static void account_info_notify(AccountInfo *info) {
9+static void account_info_notify(AccountInfo *info, GError *error) {
10 AgService *service = ag_account_service_get_service(info->account_service);
11 const char *service_name = ag_service_get_name(service);
12 char *client_id = NULL;
13@@ -108,6 +108,7 @@
14 info->watcher->callback(info->watcher,
15 info->account_id,
16 service_name,
17+ error,
18 info->enabled,
19 client_id,
20 client_secret,
21@@ -124,32 +125,18 @@
22
23 GError *error = NULL;
24 info->session_data = signon_auth_session_process_finish(session, result, &error);
25+ account_info_notify(info, error);
26
27 if (error != NULL) {
28 fprintf(stderr, "Authentication failed: %s\n", error->message);
29 g_error_free(error);
30- return;
31 }
32- account_info_notify(info);
33 }
34
35-static void account_info_enabled_cb(
36- AgAccountService *account_service, gboolean enabled, AccountInfo *info) {
37- fprintf(stderr, "account_info_enabled_cb for %u, enabled=%d\n", info->account_id, enabled);
38- if (info->enabled == enabled) {
39- /* no change */
40- return;
41- }
42- info->enabled = enabled;
43-
44+static void account_info_login(AccountInfo *info) {
45 account_info_clear_login(info);
46- if (!enabled) {
47- // Send notification that account has been disabled */
48- account_info_notify(info);
49- return;
50- }
51
52- AgAuthData *auth_data = ag_account_service_get_auth_data(account_service);
53+ AgAuthData *auth_data = ag_account_service_get_auth_data(info->account_service);
54 GError *error = NULL;
55 fprintf(stderr, "Starting authentication session for account %u\n", info->account_id);
56 info->session = signon_auth_session_new(
57@@ -157,21 +144,15 @@
58 ag_auth_data_get_method(auth_data), &error);
59 if (error != NULL) {
60 fprintf(stderr, "Could not set up auth session: %s\n", error->message);
61+ account_info_notify(info, error);
62 g_error_free(error);
63 g_object_unref(auth_data);
64 return;
65 }
66
67- GVariantBuilder builder;
68- g_variant_builder_init(&builder, G_VARIANT_TYPE_VARDICT);
69- g_variant_builder_add(
70- &builder, "{sv}",
71- SIGNON_SESSION_DATA_UI_POLICY,
72- g_variant_new_int32(SIGNON_POLICY_NO_USER_INTERACTION));
73-
74 info->auth_params = g_variant_ref_sink(
75 ag_auth_data_get_login_parameters(
76- auth_data, g_variant_builder_end(&builder)));
77+ auth_data, NULL));
78
79 signon_auth_session_process_async(
80 info->session,
81@@ -182,6 +163,24 @@
82 ag_auth_data_unref(auth_data);
83 }
84
85+static void account_info_enabled_cb(
86+ AgAccountService *account_service, gboolean enabled, AccountInfo *info) {
87+ fprintf(stderr, "account_info_enabled_cb for %u, enabled=%d\n", info->account_id, enabled);
88+ if (info->enabled == enabled) {
89+ /* no change */
90+ return;
91+ }
92+ info->enabled = enabled;
93+
94+ if (enabled) {
95+ account_info_login(info);
96+ } else {
97+ account_info_clear_login(info);
98+ // Send notification that account has been disabled */
99+ account_info_notify(info, NULL);
100+ }
101+}
102+
103 static AccountInfo *account_info_new(AccountWatcher *watcher, AgAccountService *account_service) {
104 AccountInfo *info = g_new0(AccountInfo, 1);
105 info->watcher = watcher;
106@@ -258,7 +257,7 @@
107 }
108 ag_manager_list_free(enabled_accounts);
109
110- return FALSE;
111+ return G_SOURCE_REMOVE;
112 }
113
114 AccountWatcher *account_watcher_new(GHashTable *services_to_watch,
115@@ -277,3 +276,28 @@
116 g_idle_add(account_watcher_setup, watcher);
117 return watcher;
118 }
119+
120+struct refresh_info {
121+ AccountWatcher *watcher;
122+ AgAccountId account_id;
123+};
124+
125+static gboolean account_watcher_refresh_cb(void *user_data) {
126+ struct refresh_info *data = (struct refresh_info *)user_data;
127+
128+ AccountInfo *info = g_hash_table_lookup(
129+ data->watcher->services, GUINT_TO_POINTER(data->account_id));
130+ if (info != NULL) {
131+ account_info_login(info);
132+ }
133+
134+ return G_SOURCE_REMOVE;
135+}
136+
137+void account_watcher_refresh(AccountWatcher *watcher, unsigned int account_id) {
138+ struct refresh_info *data = g_new(struct refresh_info, 1);
139+ data->watcher = watcher;
140+ data->account_id = account_id;
141+ g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, account_watcher_refresh_cb,
142+ data, g_free);
143+}
144
145=== modified file 'accounts/account-watcher.h'
146--- accounts/account-watcher.h 2014-07-11 12:01:42 +0000
147+++ accounts/account-watcher.h 2014-07-24 20:44:55 +0000
148@@ -24,6 +24,7 @@
149 typedef void (*AccountEnabledCallback)(AccountWatcher *watcher,
150 unsigned int account_id,
151 const char *service_name,
152+ GError *error,
153 int enabled,
154 const char *client_id,
155 const char *client_secret,
156@@ -35,4 +36,6 @@
157 AccountEnabledCallback callback,
158 void *user_data);
159
160+void account_watcher_refresh(AccountWatcher *watcher, unsigned int account_id);
161+
162 #endif
163
164=== modified file 'accounts/accounts.c'
165--- accounts/accounts.c 2014-07-11 13:44:14 +0000
166+++ accounts/accounts.c 2014-07-24 20:44:55 +0000
167@@ -1,5 +1,4 @@
168 #include "_cgo_export.h"
169-#include "account-watcher.h"
170
171 AccountWatcher *watch_for_services(void *array_of_service_names, int length) {
172 /* Transfer service names to hash table */
173@@ -11,8 +10,24 @@
174 g_hash_table_insert(services_to_watch, g_strdup(service_names[i].p), NULL);
175 }
176
177+ if (FALSE) {
178+ /* The Go callback doesn't quite match the
179+ * AccountEnabledCallback function prototype, so we cast the
180+ * argument in the account_watcher_new() call below.
181+ *
182+ * This is just a check to see that the function still has the
183+ * prototype we expect.
184+ */
185+ void (*unused)(void *watcher,
186+ unsigned int account_id, char *service_name,
187+ GError *error, int enabled,
188+ char *client_id, char *client_secret,
189+ char *access_token, char *token_secret,
190+ void *user_data) = authCallback;
191+ }
192+
193 AccountWatcher *watcher = account_watcher_new(
194- services_to_watch, authCallback, NULL);
195+ services_to_watch, (AccountEnabledCallback)authCallback, NULL);
196 g_hash_table_unref(services_to_watch);
197 return watcher;
198 }
199
200=== modified file 'accounts/accounts.go'
201--- accounts/accounts.go 2014-07-11 12:14:35 +0000
202+++ accounts/accounts.go 2014-07-24 20:44:55 +0000
203@@ -21,17 +21,26 @@
204 #cgo pkg-config: glib-2.0 libaccounts-glib libsignon-glib
205 #include <stdlib.h>
206 #include <glib.h>
207-
208-typedef struct _AccountWatcher AccountWatcher;
209+#include "account-watcher.h"
210
211 AccountWatcher *watch_for_services(void *array_of_service_names, int length);
212 */
213 import "C"
214-import "unsafe"
215+import (
216+ "errors"
217+ "sync"
218+ "unsafe"
219+)
220+
221+type Watcher struct {
222+ C <-chan AuthData
223+ watcher *C.AccountWatcher
224+}
225
226 type AuthData struct {
227 AccountId uint
228 ServiceName string
229+ Error error
230 Enabled bool
231
232 ClientId string
233@@ -41,28 +50,47 @@
234 }
235
236 var (
237- mainLoop *C.GMainLoop
238+ mainLoopOnce sync.Once
239 authChannels = make(map[*C.AccountWatcher]chan<- AuthData)
240+ authChannelsLock sync.Mutex
241 )
242
243-func WatchForService(serviceNames... string) <-chan AuthData {
244- if mainLoop == nil {
245- mainLoop = C.g_main_loop_new(nil, C.gboolean(1))
246+func startMainLoop() {
247+ mainLoopOnce.Do(func() {
248+ mainLoop := C.g_main_loop_new(nil, C.gboolean(1))
249 go C.g_main_loop_run(mainLoop)
250- }
251+ })
252+}
253
254- watcher := C.watch_for_services(unsafe.Pointer(&serviceNames[0]), C.int(len(serviceNames)))
255+// NewWatcher creates a new account watcher for the given service names
256+func NewWatcher(serviceNames... string) *Watcher {
257+ w := new(Watcher)
258+ w.watcher = C.watch_for_services(unsafe.Pointer(&serviceNames[0]), C.int(len(serviceNames)))
259
260 ch := make(chan AuthData)
261- authChannels[watcher] = ch
262- return ch
263+ w.C = ch
264+ authChannelsLock.Lock()
265+ authChannels[w.watcher] = ch
266+ authChannelsLock.Unlock()
267+
268+ startMainLoop()
269+
270+ return w
271+}
272+
273+// Refresh requests that the token for the given account be refreshed.
274+// The new access token will be delivered over the watcher's channel.
275+func (w *Watcher) Refresh(accountId uint) {
276+ C.account_watcher_refresh(w.watcher, C.uint(accountId))
277 }
278
279 //export authCallback
280-func authCallback(watcher unsafe.Pointer, accountId C.uint, serviceName *C.char, enabled C.int, clientId, clientSecret, accessToken, tokenSecret *C.char, userData unsafe.Pointer) {
281+func authCallback(watcher unsafe.Pointer, accountId C.uint, serviceName *C.char, error *C.GError, enabled C.int, clientId, clientSecret, accessToken, tokenSecret *C.char, userData unsafe.Pointer) {
282 // Ideally the first argument would be of type
283 // *C.AccountWatcher, but that fails with Go 1.2.
284+ authChannelsLock.Lock()
285 ch := authChannels[(*C.AccountWatcher)(watcher)]
286+ authChannelsLock.Unlock()
287 if ch == nil {
288 // Log the error
289 return
290@@ -71,6 +99,9 @@
291 var data AuthData
292 data.AccountId = uint(accountId)
293 data.ServiceName = C.GoString(serviceName)
294+ if error != nil {
295+ data.Error = errors.New(C.GoString((*C.char)(error.message)))
296+ }
297 if enabled != 0 {
298 data.Enabled = true
299 }
300
301=== modified file 'cmd/account-polld/account_manager.go'
302--- cmd/account-polld/account_manager.go 2014-07-17 18:35:36 +0000
303+++ cmd/account-polld/account_manager.go 2014-07-24 20:44:55 +0000
304@@ -19,7 +19,6 @@
305
306 import (
307 "log"
308- "sync"
309 "time"
310
311 "launchpad.net/account-polld/accounts"
312@@ -27,59 +26,74 @@
313 )
314
315 type AccountManager struct {
316+ watcher *accounts.Watcher
317 authData accounts.AuthData
318- authMutex *sync.Mutex
319 plugin plugins.Plugin
320 interval time.Duration
321 postWatch chan *PostWatch
322- terminate chan bool
323+ authChan chan accounts.AuthData
324 }
325
326 const DEFAULT_INTERVAL = time.Duration(60 * time.Second)
327
328-func NewAccountManager(authData accounts.AuthData, postWatch chan *PostWatch, plugin plugins.Plugin) *AccountManager {
329+func NewAccountManager(watcher *accounts.Watcher, postWatch chan *PostWatch, plugin plugins.Plugin) *AccountManager {
330 return &AccountManager{
331+ watcher: watcher,
332 plugin: plugin,
333- authData: authData,
334- authMutex: &sync.Mutex{},
335+ interval: DEFAULT_INTERVAL,
336 postWatch: postWatch,
337- interval: DEFAULT_INTERVAL,
338- terminate: make(chan bool),
339+ authChan: make(chan accounts.AuthData, 1),
340 }
341 }
342
343 func (a *AccountManager) Delete() {
344- a.terminate <- true
345+ close(a.authChan)
346 }
347
348 func (a *AccountManager) Loop() {
349- defer close(a.terminate)
350+ var ok bool
351+ if a.authData, ok = <- a.authChan; !ok {
352+ return
353+ }
354 L:
355 for {
356 log.Println("Polling set to", a.interval, "for", a.authData.AccountId)
357 select {
358 case <-time.After(a.interval):
359 a.poll()
360- case <-a.terminate:
361- break L
362+ case a.authData, ok = <-a.authChan:
363+ if !ok {
364+ break L
365+ }
366+ a.poll()
367 }
368 }
369 log.Printf("Ending poll loop for account %d", a.authData.AccountId)
370 }
371
372 func (a *AccountManager) poll() {
373- a.authMutex.Lock()
374- defer a.authMutex.Unlock()
375-
376 if !a.authData.Enabled {
377 log.Println("Account", a.authData.AccountId, "no longer enabled")
378 return
379 }
380
381+ if a.authData.Error != nil {
382+ log.Println("Account", a.authData.AccountId, "failed to authenticate:", a.authData.Error)
383+ return
384+ }
385+
386 if n, err := a.plugin.Poll(&a.authData); err != nil {
387 log.Print("Error while polling ", a.authData.AccountId, ": ", err)
388 // penalizing the next poll
389 a.interval += DEFAULT_INTERVAL
390+
391+ // If the error indicates that the authentication
392+ // token has expired, request reauthentication and
393+ // mark data as disabled.
394+ if err == plugins.ErrTokenExpired {
395+ a.watcher.Refresh(a.authData.AccountId)
396+ a.authData.Enabled = false
397+ }
398 } else if len(n) > 0 {
399 // on success we reset the timeout to the default interval
400 a.interval = DEFAULT_INTERVAL
401@@ -88,7 +102,5 @@
402 }
403
404 func (a *AccountManager) updateAuthData(authData accounts.AuthData) {
405- a.authMutex.Lock()
406- defer a.authMutex.Unlock()
407- a.authData = authData
408+ a.authChan <- authData
409 }
410
411=== modified file 'cmd/account-polld/main.go'
412--- cmd/account-polld/main.go 2014-07-18 11:42:52 +0000
413+++ cmd/account-polld/main.go 2014-07-24 20:44:55 +0000
414@@ -69,9 +69,10 @@
415 }
416
417 func monitorAccounts(postWatch chan *PostWatch) {
418+ watcher := accounts.NewWatcher(SERVICENAME_GMAIL, SERVICENAME_FACEBOOK, SERVICENAME_TWITTER)
419 mgr := make(map[uint]*AccountManager)
420 L:
421- for data := range accounts.WatchForService(SERVICENAME_GMAIL, SERVICENAME_FACEBOOK, SERVICENAME_TWITTER) {
422+ for data := range watcher.C {
423 if account, ok := mgr[data.AccountId]; ok {
424 if data.Enabled {
425 log.Printf("New account data for %d - was %#v, now is %#v", data.AccountId, account.authData, data)
426@@ -98,7 +99,8 @@
427 log.Println("Unhandled account with id", data.AccountId, "for", data.ServiceName)
428 continue L
429 }
430- mgr[data.AccountId] = NewAccountManager(data, postWatch, plugin)
431+ mgr[data.AccountId] = NewAccountManager(watcher, postWatch, plugin)
432+ mgr[data.AccountId].updateAuthData(data)
433 go mgr[data.AccountId].Loop()
434 }
435 }
436
437=== modified file 'cmd/account-watcher-test/main.go'
438--- cmd/account-watcher-test/main.go 2014-07-11 12:07:47 +0000
439+++ cmd/account-watcher-test/main.go 2014-07-24 20:44:55 +0000
440@@ -9,7 +9,7 @@
441
442 func main() {
443 // Expects a list of service names as command line arguments
444- for data := range accounts.WatchForService(os.Args[1:]...) {
445+ for data := range accounts.NewWatcher(os.Args[1:]...).C {
446 fmt.Printf("%#v\n", data)
447 }
448 }
449
450=== modified file 'plugins/facebook/facebook.go'
451--- plugins/facebook/facebook.go 2014-07-17 18:35:36 +0000
452+++ plugins/facebook/facebook.go 2014-07-24 20:44:55 +0000
453@@ -62,6 +62,9 @@
454 if err := decoder.Decode(&result); err != nil {
455 return nil, err
456 }
457+ if result.Error.Code == 190 {
458+ return nil, plugins.ErrTokenExpired
459+ }
460 return nil, &result.Error
461 }
462
463
464=== modified file 'plugins/facebook/facebook_test.go'
465--- plugins/facebook/facebook_test.go 2014-07-18 11:38:36 +0000
466+++ plugins/facebook/facebook_test.go 2014-07-24 20:44:55 +0000
467@@ -23,6 +23,8 @@
468 "testing"
469
470 . "launchpad.net/gocheck"
471+
472+ "launchpad.net/account-polld/plugins"
473 )
474
475 type S struct{}
476@@ -48,10 +50,18 @@
477 errorBody = `
478 {
479 "error": {
480- "message": "Message describing the error",
481+ "message": "Unknown path components: /xyz",
482+ "type": "OAuthException",
483+ "code": 2500
484+ }
485+}`
486+ tokenExpiredErrorBody = `
487+{
488+ "error": {
489+ "message": "Error validating access token: Session has expired",
490 "type": "OAuthException",
491 "code": 190 ,
492- "error_subcode": 460
493+ "error_subcode": 463
494 }
495 }`
496 notificationsBody = `
497@@ -151,7 +161,17 @@
498 c.Check(notifications, IsNil)
499 c.Assert(err, Not(IsNil))
500 graphErr := err.(*GraphError)
501- c.Check(graphErr.Message, Equals, "Message describing the error")
502- c.Check(graphErr.Code, Equals, 190)
503- c.Check(graphErr.Subcode, Equals, 460)
504+ c.Check(graphErr.Message, Equals, "Unknown path components: /xyz")
505+ c.Check(graphErr.Code, Equals, 2500)
506+}
507+
508+func (s S) TestTokenExpiredErrorResponse(c *C) {
509+ resp := &http.Response{
510+ StatusCode: http.StatusBadRequest,
511+ Body: closeWrapper{bytes.NewReader([]byte(tokenExpiredErrorBody))},
512+ }
513+ p := &fbPlugin{}
514+ notifications, err := p.parseResponse(resp)
515+ c.Check(notifications, IsNil)
516+ c.Assert(err, Equals, plugins.ErrTokenExpired)
517 }
518
519=== modified file 'plugins/plugins.go'
520--- plugins/plugins.go 2014-07-17 18:35:36 +0000
521+++ plugins/plugins.go 2014-07-24 20:44:55 +0000
522@@ -17,7 +17,11 @@
523
524 package plugins
525
526-import "launchpad.net/account-polld/accounts"
527+import (
528+ "errors"
529+
530+ "launchpad.net/account-polld/accounts"
531+)
532
533 // Plugin is an interface which the plugins will adhere to for the poll
534 // daemon to interact with.
535@@ -122,3 +126,7 @@
536 PLUGIN_EMAIL = 0
537 PLUGIN_SOCIAL
538 )
539+
540+// ErrTokenExpired is the error returned by a plugin to indicate that
541+// the web service reported that the authentication token has expired.
542+var ErrTokenExpired = errors.New("Token expired")
543
544=== modified file 'plugins/twitter/twitter.go'
545--- plugins/twitter/twitter.go 2014-07-23 10:35:13 +0000
546+++ plugins/twitter/twitter.go 2014-07-24 20:44:55 +0000
547@@ -77,6 +77,12 @@
548 if err := decoder.Decode(&result); err != nil {
549 return nil, err
550 }
551+ // Error code 89 is used for invalid or expired tokens.
552+ for _, e := range result.Errors {
553+ if e.Code == 89 {
554+ return nil, plugins.ErrTokenExpired
555+ }
556+ }
557 return nil, &result
558 }
559
560@@ -113,6 +119,12 @@
561 if err := decoder.Decode(&result); err != nil {
562 return nil, err
563 }
564+ // Error code 89 is used for invalid or expired tokens.
565+ for _, e := range result.Errors {
566+ if e.Code == 89 {
567+ return nil, plugins.ErrTokenExpired
568+ }
569+ }
570 return nil, &result
571 }
572
573
574=== modified file 'plugins/twitter/twitter_test.go'
575--- plugins/twitter/twitter_test.go 2014-07-18 11:37:04 +0000
576+++ plugins/twitter/twitter_test.go 2014-07-24 20:44:55 +0000
577@@ -23,6 +23,8 @@
578 "testing"
579
580 . "launchpad.net/gocheck"
581+
582+ "launchpad.net/account-polld/plugins"
583 )
584
585 type S struct{}
586@@ -54,6 +56,15 @@
587 }
588 ]
589 }`
590+ tokenExpiredErrorBody = `
591+{
592+ "errors": [
593+ {
594+ "message":"Invalid or expired token",
595+ "code":89
596+ }
597+ ]
598+}`
599 statusesBody = `
600 [
601 {
602@@ -425,6 +436,17 @@
603 c.Check(twErr.Errors[0].Code, Equals, 34)
604 }
605
606+func (s S) TestParseStatusesTokenExpiredError(c *C) {
607+ resp := &http.Response{
608+ StatusCode: http.StatusBadRequest,
609+ Body: closeWrapper{bytes.NewReader([]byte(tokenExpiredErrorBody))},
610+ }
611+ p := &twitterPlugin{}
612+ messages, err := p.parseStatuses(resp)
613+ c.Check(messages, IsNil)
614+ c.Assert(err, Equals, plugins.ErrTokenExpired)
615+}
616+
617 func (s S) TestParseDirectMessages(c *C) {
618 resp := &http.Response{
619 StatusCode: http.StatusOK,
620@@ -453,3 +475,14 @@
621 c.Check(twErr.Errors[0].Message, Equals, "Sorry, that page does not exist")
622 c.Check(twErr.Errors[0].Code, Equals, 34)
623 }
624+
625+func (s S) TestParseDirectMessagesTokenExpiredError(c *C) {
626+ resp := &http.Response{
627+ StatusCode: http.StatusBadRequest,
628+ Body: closeWrapper{bytes.NewReader([]byte(tokenExpiredErrorBody))},
629+ }
630+ p := &twitterPlugin{}
631+ messages, err := p.parseDirectMessages(resp)
632+ c.Check(messages, IsNil)
633+ c.Assert(err, Equals, plugins.ErrTokenExpired)
634+}

Subscribers

People subscribed via source and target branches