Merge lp:~renatofilho/account-polld/account-polld-caldav into lp:~ubuntu-push-hackers/account-polld/trunk
- account-polld-caldav
- Merge into trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~renatofilho/account-polld/account-polld-caldav |
Merge into: | lp:~ubuntu-push-hackers/account-polld/trunk |
Prerequisite: | lp:~renatofilho/account-polld/query-calendars |
Diff against target: |
1606 lines (+978/-244) 14 files modified
accounts/account-watcher.c (+79/-90) accounts/account-watcher.h (+9/-3) accounts/accounts.c (+3/-2) accounts/accounts.go (+25/-9) cmd/account-polld/account_service.go (+11/-13) cmd/account-polld/main.go (+24/-28) cmd/account-watcher-test/main.go (+1/-3) plugins/caldav/api.go (+54/-0) plugins/caldav/caldav.go (+203/-0) plugins/dekko/api.go (+127/-0) plugins/dekko/dekko.go (+346/-0) plugins/gcalendar/gcalendar.go (+3/-2) plugins/gcalendar/syncmonitor.go (+0/-94) syncmonitor/syncmonitor.go (+93/-0) |
To merge this branch: | bzr merge lp:~renatofilho/account-polld/account-polld-caldav |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
system-apps-ci-bot | continuous-integration | Approve | |
PS Jenkins bot | continuous-integration | Pending | |
Ubuntu Push Hackers | Pending | ||
Review via email: mp+299868@code.launchpad.net |
This proposal supersedes a proposal from 2016-07-12.
This proposal has been superseded by a proposal from 2016-08-02.
Commit message
Implement the caldav plugin to query for changes on caldav sources.
Description of the change
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:180
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 180. By Renato Araujo Oliveira Filho
-
Do not use hard coded urls.
- 181. By Renato Araujo Oliveira Filho
-
print debug message.
- 182. By Renato Araujo Oliveira Filho
-
Trunk merged.
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:182
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 183. By Renato Araujo Oliveira Filho
-
Use UTC date on end date.
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
FAILED: Continuous integration, rev:183
https:/
Executed test runs:
FAILURE: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
FAILURE: https:/
Click here to trigger a rebuild:
https:/
- 184. By Renato Araujo Oliveira Filho
-
Fix utc conversion.
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:184
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 185. By Renato Araujo Oliveira Filho
-
Retrieve changed events one miture before last sync.
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:185
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 186. By Renato Araujo Oliveira Filho
-
Fix startDate.
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:186
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 187. By Renato Araujo Oliveira Filho
-
Query for changes in localtime format if UTC returns empty values.
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:187
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 188. By Renato Araujo Oliveira Filho
-
Query events using UTC time.
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:188
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 189. By Renato Araujo Oliveira Filho
-
Rever changes on account_manager.go
- 190. By Renato Araujo Oliveira Filho
-
Format code.
- 191. By Renato Araujo Oliveira Filho
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
PASSED: Continuous integration, rev:190
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild:
https:/
- 192. By Renato Araujo Oliveira Filho
-
Remove debug messages.
Unmerged revisions
Preview Diff
1 | === modified file 'accounts/account-watcher.c' |
2 | --- accounts/account-watcher.c 2016-04-19 19:58:25 +0000 |
3 | +++ accounts/account-watcher.c 2016-08-02 14:37:44 +0000 |
4 | @@ -31,12 +31,12 @@ |
5 | struct _AccountWatcher { |
6 | AgManager *manager; |
7 | /* A hash table of the enabled accounts we know of. |
8 | - * Keys are account ID integers, and AccountInfo structs as values. |
9 | + * Keys are "<accountId>/<serviceName>", and AccountInfo structs as values. |
10 | */ |
11 | GHashTable *services; |
12 | |
13 | - gulong enabled_event_signal_id; |
14 | - gulong account_deleted_signal_id; |
15 | + /* List of supported services' IDs */ |
16 | + GSList *supported_services; |
17 | |
18 | AccountEnabledCallback callback; |
19 | void *user_data; |
20 | @@ -51,9 +51,7 @@ |
21 | GVariant *auth_params; |
22 | GVariant *session_data; |
23 | |
24 | - gulong enabled_signal_id; |
25 | AgAccountId account_id; |
26 | - gboolean enabled; /* the last known state of the account */ |
27 | }; |
28 | |
29 | static void account_info_clear_login(AccountInfo *info) { |
30 | @@ -74,11 +72,6 @@ |
31 | |
32 | static void account_info_free(AccountInfo *info) { |
33 | account_info_clear_login(info); |
34 | - if (info->enabled_signal_id != 0) { |
35 | - g_signal_handler_disconnect( |
36 | - info->account_service, info->enabled_signal_id); |
37 | - } |
38 | - info->enabled_signal_id = 0; |
39 | if (info->account_service) { |
40 | g_object_unref(info->account_service); |
41 | info->account_service = NULL; |
42 | @@ -94,6 +87,8 @@ |
43 | char *client_secret = NULL; |
44 | char *access_token = NULL; |
45 | char *token_secret = NULL; |
46 | + char *secret = NULL; |
47 | + char *user_name = NULL; |
48 | |
49 | if (info->auth_params != NULL) { |
50 | /* Look up OAuth 2 parameters, falling back to OAuth 1 names */ |
51 | @@ -109,6 +104,8 @@ |
52 | if (info->session_data != NULL) { |
53 | g_variant_lookup(info->session_data, "AccessToken", "&s", &access_token); |
54 | g_variant_lookup(info->session_data, "TokenSecret", "&s", &token_secret); |
55 | + g_variant_lookup(info->session_data, "Secret", "&s", &secret); |
56 | + g_variant_lookup(info->session_data, "UserName", "&s", &user_name); |
57 | } |
58 | |
59 | info->watcher->callback(info->watcher, |
60 | @@ -116,11 +113,13 @@ |
61 | service_type, |
62 | service_name, |
63 | error, |
64 | - info->enabled, |
65 | + TRUE, |
66 | client_id, |
67 | client_secret, |
68 | access_token, |
69 | token_secret, |
70 | + user_name, |
71 | + secret, |
72 | info->watcher->user_data); |
73 | } |
74 | |
75 | @@ -178,24 +177,6 @@ |
76 | ag_auth_data_unref(auth_data); |
77 | } |
78 | |
79 | -static void account_info_enabled_cb( |
80 | - AgAccountService *account_service, gboolean enabled, AccountInfo *info) { |
81 | - trace("account_info_enabled_cb for %u, enabled=%d\n", info->account_id, enabled); |
82 | - if (info->enabled == enabled) { |
83 | - /* no change */ |
84 | - return; |
85 | - } |
86 | - info->enabled = enabled; |
87 | - |
88 | - if (enabled) { |
89 | - account_info_login(info); |
90 | - } else { |
91 | - account_info_clear_login(info); |
92 | - // Send notification that account has been disabled */ |
93 | - account_info_notify(info, NULL); |
94 | - } |
95 | -} |
96 | - |
97 | static AccountInfo *account_info_new(AccountWatcher *watcher, AgAccountService *account_service) { |
98 | AccountInfo *info = g_new0(AccountInfo, 1); |
99 | info->watcher = watcher; |
100 | @@ -204,99 +185,105 @@ |
101 | AgAccount *account = ag_account_service_get_account(account_service); |
102 | g_object_get(account, "id", &info->account_id, NULL); |
103 | |
104 | - info->enabled_signal_id = g_signal_connect( |
105 | - account_service, "enabled", |
106 | - G_CALLBACK(account_info_enabled_cb), info); |
107 | - // Set initial state |
108 | - account_info_enabled_cb(account_service, ag_account_service_get_enabled(account_service), info); |
109 | - |
110 | return info; |
111 | } |
112 | |
113 | -static void account_watcher_enabled_event_cb( |
114 | - AgManager *manager, AgAccountId account_id, AccountWatcher *watcher) { |
115 | - trace("enabled-event for %u\n", account_id); |
116 | - if (g_hash_table_contains(watcher->services, GUINT_TO_POINTER(account_id))) { |
117 | - /* We are already tracking this account */ |
118 | - return; |
119 | - } |
120 | - AgAccount *account = ag_manager_get_account(manager, account_id); |
121 | - if (account == NULL) { |
122 | - /* There was a problem looking up the account */ |
123 | - return; |
124 | - } |
125 | - /* Since our AgManager is restricted to a particular service type, |
126 | - * pick the first service for the account. */ |
127 | - GList *services = ag_account_list_services(account); |
128 | - if (services != NULL) { |
129 | - AgService *service = services->data; |
130 | - AgAccountService *account_service = ag_account_service_new( |
131 | - account, service); |
132 | - AccountInfo *info = account_info_new(watcher, account_service); |
133 | - g_object_unref(account_service); |
134 | - g_hash_table_insert(watcher->services, GUINT_TO_POINTER(account_id), info); |
135 | - } |
136 | - ag_service_list_free(services); |
137 | - g_object_unref(account); |
138 | -} |
139 | - |
140 | -static void account_watcher_account_deleted_cb( |
141 | - AgManager *manager, AgAccountId account_id, AccountWatcher *watcher) { |
142 | - trace("account-deleted for %u\n", account_id); |
143 | - /* A disabled event should have been sent prior to this, so no |
144 | - * need to send any notification. */ |
145 | - g_hash_table_remove(watcher->services, GUINT_TO_POINTER(account_id)); |
146 | +static gboolean service_is_supported(AccountWatcher *watcher, |
147 | + const char *service_id) |
148 | +{ |
149 | + GSList *node = g_slist_find_custom(watcher->supported_services, |
150 | + service_id, |
151 | + (GCompareFunc)g_strcmp0); |
152 | + return node != NULL; |
153 | } |
154 | |
155 | static gboolean account_watcher_setup(void *user_data) { |
156 | AccountWatcher *watcher = (AccountWatcher *)user_data; |
157 | |
158 | - /* Track changes to accounts */ |
159 | - watcher->enabled_event_signal_id = g_signal_connect( |
160 | - watcher->manager, "enabled-event", |
161 | - G_CALLBACK(account_watcher_enabled_event_cb), watcher); |
162 | - watcher->account_deleted_signal_id = g_signal_connect( |
163 | - watcher->manager, "account-deleted", |
164 | - G_CALLBACK(account_watcher_account_deleted_cb), watcher); |
165 | - |
166 | /* Now check initial state */ |
167 | - GList *enabled_accounts = ag_manager_list(watcher->manager); |
168 | + GList *enabled_accounts = |
169 | + ag_manager_get_enabled_account_services(watcher->manager); |
170 | + GList *old_services = g_hash_table_get_keys(watcher->services); |
171 | + |
172 | + /* Update the services table */ |
173 | GList *l; |
174 | for (l = enabled_accounts; l != NULL; l = l->next) { |
175 | - AgAccountId account_id = GPOINTER_TO_UINT(l->data); |
176 | - account_watcher_enabled_event_cb(watcher->manager, account_id, watcher); |
177 | - } |
178 | - ag_manager_list_free(enabled_accounts); |
179 | + AgAccountService *account_service = l->data; |
180 | + AgAccountId id = ag_account_service_get_account(account_service)->id; |
181 | + AgService *service = ag_account_service_get_service(account_service); |
182 | + const char *service_id = ag_service_get_name(service); |
183 | + |
184 | + if (!service_is_supported(watcher, service_id)) continue; |
185 | + |
186 | + char *key = g_strdup_printf("%d/%s", id, service_id); |
187 | + |
188 | + AccountInfo *info = g_hash_table_lookup(watcher->services, key); |
189 | + if (info) { |
190 | + GList *node = g_list_find_custom(old_services, key, |
191 | + (GCompareFunc)g_strcmp0); |
192 | + old_services = g_list_remove_link(old_services, node); |
193 | + g_free(key); |
194 | + } else { |
195 | + trace("adding account %s\n", key); |
196 | + info = account_info_new(watcher, account_service); |
197 | + g_hash_table_insert(watcher->services, key, info); |
198 | + } |
199 | + account_info_login(info); |
200 | + } |
201 | + g_list_free_full(enabled_accounts, g_object_unref); |
202 | + |
203 | + /* Remove from the table the accounts which are no longer enabled */ |
204 | + for (l = old_services; l != NULL; l = l->next) { |
205 | + char *key = l->data; |
206 | + trace("removing account %s\n", key); |
207 | + g_hash_table_remove(watcher->services, key); |
208 | + } |
209 | + g_list_free(old_services); |
210 | |
211 | return G_SOURCE_REMOVE; |
212 | } |
213 | |
214 | -AccountWatcher *account_watcher_new(const char *service_type, |
215 | - AccountEnabledCallback callback, |
216 | +AccountWatcher *account_watcher_new(AccountEnabledCallback callback, |
217 | void *user_data) { |
218 | AccountWatcher *watcher = g_new0(AccountWatcher, 1); |
219 | |
220 | - watcher->manager = ag_manager_new_for_service_type(service_type); |
221 | + watcher->manager = ag_manager_new(); |
222 | watcher->services = g_hash_table_new_full( |
223 | - g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)account_info_free); |
224 | + g_str_hash, g_str_equal, g_free, (GDestroyNotify)account_info_free); |
225 | + watcher->supported_services = NULL; |
226 | watcher->callback = callback; |
227 | watcher->user_data = user_data; |
228 | |
229 | + return watcher; |
230 | +} |
231 | + |
232 | +void account_watcher_add_service(AccountWatcher *watcher, |
233 | + char *serviceId) { |
234 | + watcher->supported_services = |
235 | + g_slist_prepend(watcher->supported_services, serviceId); |
236 | +} |
237 | + |
238 | +void account_watcher_run(AccountWatcher *watcher) { |
239 | /* Make sure main setup occurs within the mainloop thread */ |
240 | g_idle_add(account_watcher_setup, watcher); |
241 | - return watcher; |
242 | } |
243 | |
244 | struct refresh_info { |
245 | AccountWatcher *watcher; |
246 | AgAccountId account_id; |
247 | + char *service_name; |
248 | }; |
249 | |
250 | +static void refresh_info_free(struct refresh_info *data) { |
251 | + g_free(data->service_name); |
252 | + g_free(data); |
253 | +} |
254 | + |
255 | static gboolean account_watcher_refresh_cb(void *user_data) { |
256 | struct refresh_info *data = (struct refresh_info *)user_data; |
257 | |
258 | - AccountInfo *info = g_hash_table_lookup( |
259 | - data->watcher->services, GUINT_TO_POINTER(data->account_id)); |
260 | + char *key = g_strdup_printf("%d/%s", data->account_id, data->service_name); |
261 | + AccountInfo *info = g_hash_table_lookup(data->watcher->services, key); |
262 | if (info != NULL) { |
263 | account_info_login(info); |
264 | } |
265 | @@ -304,10 +291,12 @@ |
266 | return G_SOURCE_REMOVE; |
267 | } |
268 | |
269 | -void account_watcher_refresh(AccountWatcher *watcher, unsigned int account_id) { |
270 | +void account_watcher_refresh(AccountWatcher *watcher, unsigned int account_id, |
271 | + const char *service_name) { |
272 | struct refresh_info *data = g_new(struct refresh_info, 1); |
273 | data->watcher = watcher; |
274 | data->account_id = account_id; |
275 | + data->service_name = g_strdup(service_name); |
276 | g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, account_watcher_refresh_cb, |
277 | - data, g_free); |
278 | + data, (GDestroyNotify)refresh_info_free); |
279 | } |
280 | |
281 | === modified file 'accounts/account-watcher.h' |
282 | --- accounts/account-watcher.h 2016-04-19 19:58:25 +0000 |
283 | +++ accounts/account-watcher.h 2016-08-02 14:37:44 +0000 |
284 | @@ -30,12 +30,18 @@ |
285 | const char *client_secret, |
286 | const char *access_token, |
287 | const char *token_secret, |
288 | + const char *user_name, |
289 | + const char *secret, |
290 | void *user_data); |
291 | |
292 | -AccountWatcher *account_watcher_new(const char *service_type, |
293 | - AccountEnabledCallback callback, |
294 | +AccountWatcher *account_watcher_new(AccountEnabledCallback callback, |
295 | void *user_data); |
296 | +void account_watcher_add_service(AccountWatcher *watcher, |
297 | + char *serviceId); |
298 | +void account_watcher_run(AccountWatcher *watcher); |
299 | |
300 | -void account_watcher_refresh(AccountWatcher *watcher, unsigned int account_id); |
301 | +void account_watcher_refresh(AccountWatcher *watcher, |
302 | + unsigned int account_id, |
303 | + const char *service_name); |
304 | |
305 | #endif |
306 | |
307 | === modified file 'accounts/accounts.c' |
308 | --- accounts/accounts.c 2016-04-19 19:58:25 +0000 |
309 | +++ accounts/accounts.c 2016-08-02 14:37:44 +0000 |
310 | @@ -1,6 +1,6 @@ |
311 | #include "_cgo_export.h" |
312 | |
313 | -AccountWatcher *watch_for_service_type(const char *service_type) { |
314 | +AccountWatcher *watch() { |
315 | /* Transfer service names to hash table */ |
316 | if (FALSE) { |
317 | /* The Go callback doesn't quite match the |
318 | @@ -16,10 +16,11 @@ |
319 | GError *error, int enabled, |
320 | char *client_id, char *client_secret, |
321 | char *access_token, char *token_secret, |
322 | + char *user_name, char *secret, |
323 | void *user_data) = authCallback; |
324 | } |
325 | |
326 | AccountWatcher *watcher = account_watcher_new( |
327 | - service_type, (AccountEnabledCallback)authCallback, NULL); |
328 | + (AccountEnabledCallback)authCallback, NULL); |
329 | return watcher; |
330 | } |
331 | |
332 | === modified file 'accounts/accounts.go' |
333 | --- accounts/accounts.go 2016-04-19 19:58:25 +0000 |
334 | +++ accounts/accounts.go 2016-08-02 14:37:44 +0000 |
335 | @@ -22,7 +22,7 @@ |
336 | #include <glib.h> |
337 | #include "account-watcher.h" |
338 | |
339 | -AccountWatcher *watch_for_service_type(const char *service_type); |
340 | +AccountWatcher *watch(); |
341 | */ |
342 | import "C" |
343 | import ( |
344 | @@ -47,6 +47,8 @@ |
345 | ClientSecret string |
346 | AccessToken string |
347 | TokenSecret string |
348 | + Secret string |
349 | + UserName string |
350 | } |
351 | |
352 | var ( |
353 | @@ -54,12 +56,10 @@ |
354 | authChannelsLock sync.Mutex |
355 | ) |
356 | |
357 | -// NewWatcher creates a new account watcher for the given service names |
358 | -func NewWatcher(serviceType string) *Watcher { |
359 | +// NewWatcher creates a new account watcher |
360 | +func NewWatcher() *Watcher { |
361 | w := new(Watcher) |
362 | - cServiceType := C.CString(serviceType) |
363 | - defer C.free(unsafe.Pointer(cServiceType)) |
364 | - w.watcher = C.watch_for_service_type(cServiceType) |
365 | + w.watcher = C.watch() |
366 | |
367 | ch := make(chan AuthData) |
368 | w.C = ch |
369 | @@ -70,14 +70,24 @@ |
370 | return w |
371 | } |
372 | |
373 | +func (w *Watcher) AddService(serviceId string) { |
374 | + C.account_watcher_add_service(w.watcher, C.CString(serviceId)) |
375 | +} |
376 | + |
377 | +// Walk through the enabled accounts, and get auth tokens for each of them. |
378 | +// The new access token will be delivered over the watcher's channel. |
379 | +func (w *Watcher) Run() { |
380 | + C.account_watcher_run(w.watcher) |
381 | +} |
382 | + |
383 | // Refresh requests that the token for the given account be refreshed. |
384 | // The new access token will be delivered over the watcher's channel. |
385 | -func (w *Watcher) Refresh(accountId uint) { |
386 | - C.account_watcher_refresh(w.watcher, C.uint(accountId)) |
387 | +func (w *Watcher) Refresh(accountId uint, serviceName string) { |
388 | + C.account_watcher_refresh(w.watcher, C.uint(accountId), C.CString(serviceName)) |
389 | } |
390 | |
391 | //export authCallback |
392 | -func authCallback(watcher unsafe.Pointer, accountId C.uint, serviceType *C.char, serviceName *C.char, error *C.GError, enabled C.int, clientId, clientSecret, accessToken, tokenSecret *C.char, userData unsafe.Pointer) { |
393 | +func authCallback(watcher unsafe.Pointer, accountId C.uint, serviceType *C.char, serviceName *C.char, error *C.GError, enabled C.int, clientId, clientSecret, accessToken, tokenSecret *C.char, userName *C.char, secret *C.char, userData unsafe.Pointer) { |
394 | // Ideally the first argument would be of type |
395 | // *C.AccountWatcher, but that fails with Go 1.2. |
396 | authChannelsLock.Lock() |
397 | @@ -110,5 +120,11 @@ |
398 | if tokenSecret != nil { |
399 | data.TokenSecret = C.GoString(tokenSecret) |
400 | } |
401 | + if secret != nil { |
402 | + data.Secret = C.GoString(secret) |
403 | + } |
404 | + if userName != nil { |
405 | + data.UserName = C.GoString(userName) |
406 | + } |
407 | ch <- data |
408 | } |
409 | |
410 | === renamed file 'cmd/account-polld/account_manager.go' => 'cmd/account-polld/account_service.go' |
411 | --- cmd/account-polld/account_manager.go 2016-06-30 19:21:46 +0000 |
412 | +++ cmd/account-polld/account_service.go 2016-08-02 14:37:44 +0000 |
413 | @@ -26,7 +26,7 @@ |
414 | "launchpad.net/ubuntu-push/click" |
415 | ) |
416 | |
417 | -type AccountManager struct { |
418 | +type AccountService struct { |
419 | watcher *accounts.Watcher |
420 | authData accounts.AuthData |
421 | plugin plugins.Plugin |
422 | @@ -51,8 +51,8 @@ |
423 | clickNotInstalledError = errors.New("Click not installed") |
424 | ) |
425 | |
426 | -func NewAccountManager(watcher *accounts.Watcher, postWatch chan *PostWatch, plugin plugins.Plugin) *AccountManager { |
427 | - return &AccountManager{ |
428 | +func NewAccountService(watcher *accounts.Watcher, postWatch chan *PostWatch, plugin plugins.Plugin) *AccountService { |
429 | + return &AccountService{ |
430 | watcher: watcher, |
431 | plugin: plugin, |
432 | postWatch: postWatch, |
433 | @@ -61,20 +61,18 @@ |
434 | } |
435 | } |
436 | |
437 | -func (a *AccountManager) Delete() { |
438 | +func (a *AccountService) Delete() { |
439 | close(a.authChan) |
440 | close(a.doneChan) |
441 | } |
442 | |
443 | // Poll() always needs to be called asynchronously as otherwise qtcontacs' GetAvatar() |
444 | // will raise an error: "QSocketNotifier: Can only be used with threads started with QThread" |
445 | -func (a *AccountManager) Poll(bootstrap bool) { |
446 | +func (a *AccountService) Poll(bootstrap bool) { |
447 | gotNewAuthData := false |
448 | - if !a.authData.Enabled { |
449 | - if a.authData, gotNewAuthData = <-a.authChan; !gotNewAuthData { |
450 | - log.Println("Account", a.authData.AccountId, "no longer enabled") |
451 | - return |
452 | - } |
453 | + if a.authData, gotNewAuthData = <-a.authChan; !gotNewAuthData { |
454 | + log.Println("Account", a.authData.AccountId, "no longer enabled") |
455 | + return |
456 | } |
457 | |
458 | if a.penaltyCount > 0 { |
459 | @@ -126,7 +124,7 @@ |
460 | // and mark the data as disabled. |
461 | // Do not refresh immediately when we just got new (faulty) auth data as immediately trying |
462 | // again is probably not going to help. Instead, we wait for the next poll cycle. |
463 | - a.watcher.Refresh(a.authData.AccountId) |
464 | + a.watcher.Refresh(a.authData.AccountId, a.authData.ServiceName) |
465 | a.authData.Enabled = false |
466 | a.authData.Error = err |
467 | } |
468 | @@ -139,7 +137,7 @@ |
469 | log.Printf("Ending poll for account %d", a.authData.AccountId) |
470 | } |
471 | |
472 | -func (a *AccountManager) poll() { |
473 | +func (a *AccountService) poll() { |
474 | log.Println("Polling account", a.authData.AccountId) |
475 | if !isClickInstalled(a.plugin.ApplicationId()) { |
476 | log.Println( |
477 | @@ -167,7 +165,7 @@ |
478 | } |
479 | } |
480 | |
481 | -func (a *AccountManager) updateAuthData(authData accounts.AuthData) { |
482 | +func (a *AccountService) updateAuthData(authData accounts.AuthData) { |
483 | a.authChan <- authData |
484 | } |
485 | |
486 | |
487 | === modified file 'cmd/account-polld/main.go' |
488 | --- cmd/account-polld/main.go 2016-04-21 23:33:48 +0000 |
489 | +++ cmd/account-polld/main.go 2016-08-02 14:37:44 +0000 |
490 | @@ -27,6 +27,8 @@ |
491 | "launchpad.net/account-polld/accounts" |
492 | "launchpad.net/account-polld/gettext" |
493 | "launchpad.net/account-polld/plugins" |
494 | + "launchpad.net/account-polld/plugins/caldav" |
495 | + "launchpad.net/account-polld/plugins/dekko" |
496 | "launchpad.net/account-polld/plugins/gcalendar" |
497 | "launchpad.net/account-polld/plugins/gmail" |
498 | "launchpad.net/account-polld/plugins/twitter" |
499 | @@ -41,19 +43,18 @@ |
500 | } |
501 | |
502 | type AccountKey struct { |
503 | - serviceType string |
504 | + serviceId string |
505 | accountId uint |
506 | } |
507 | |
508 | /* Use identifiers and API keys provided by the respective webapps which are the official |
509 | end points for the notifications */ |
510 | const ( |
511 | - SERVICETYPE_WEBAPPS = "webapps" |
512 | - SERVICETYPE_CALENDAR = "calendar" |
513 | - |
514 | + SERVICENAME_DEKKO = "dekko.dekkoproject_dekko" |
515 | SERVICENAME_GMAIL = "com.ubuntu.developer.webapps.webapp-gmail_webapp-gmail" |
516 | SERVICENAME_TWITTER = "com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter" |
517 | SERVICENAME_GCALENDAR = "google-caldav" |
518 | + SERVICENAME_OCALENDAR = "owncloud-caldav" |
519 | ) |
520 | |
521 | const ( |
522 | @@ -101,16 +102,18 @@ |
523 | } |
524 | |
525 | func monitorAccounts(postWatch chan *PostWatch, pollBus *pollbus.PollBus) { |
526 | - watchers := make(map[string]*accounts.Watcher) |
527 | - watchers[SERVICETYPE_WEBAPPS] = accounts.NewWatcher(SERVICETYPE_WEBAPPS) |
528 | - watchers[SERVICETYPE_CALENDAR] = accounts.NewWatcher(SERVICETYPE_CALENDAR) |
529 | + watcher := accounts.NewWatcher() |
530 | + watcher.AddService(SERVICENAME_DEKKO) |
531 | + watcher.AddService(SERVICENAME_GMAIL) |
532 | + watcher.AddService(SERVICENAME_GCALENDAR) |
533 | + watcher.AddService(SERVICENAME_TWITTER) |
534 | |
535 | - mgr := make(map[AccountKey]*AccountManager) |
536 | + mgr := make(map[AccountKey]*AccountService) |
537 | |
538 | var wg sync.WaitGroup |
539 | |
540 | pullAccount := func(data accounts.AuthData) bool { |
541 | - accountKey := AccountKey{data.ServiceType, data.AccountId} |
542 | + accountKey := AccountKey{data.ServiceName, data.AccountId} |
543 | if account, ok := mgr[accountKey]; ok { |
544 | if data.Enabled { |
545 | log.Println("New account data for existing account with id", data.AccountId) |
546 | @@ -131,8 +134,11 @@ |
547 | } |
548 | } else if data.Enabled { |
549 | var plugin plugins.Plugin |
550 | - log.Println("Creat plugin for service: ", data.ServiceName) |
551 | + log.Println("Creating plugin for service: ", data.ServiceName) |
552 | switch data.ServiceName { |
553 | + case SERVICENAME_DEKKO: |
554 | + log.Println("Creating account with id", data.AccountId, "for", data.ServiceName) |
555 | + plugin = dekko.New(data.AccountId) |
556 | case SERVICENAME_GMAIL: |
557 | log.Println("Creating account with id", data.AccountId, "for", data.ServiceName) |
558 | plugin = gmail.New(data.AccountId) |
559 | @@ -143,11 +149,14 @@ |
560 | // This is just stubbed until the plugin exists. |
561 | log.Println("Creating account with id", data.AccountId, "for", data.ServiceName) |
562 | plugin = twitter.New() |
563 | + case SERVICENAME_OCALENDAR: |
564 | + log.Println("Creating account with id", data.AccountId, "for", data.ServiceName) |
565 | + plugin = caldav.New(data.AccountId) |
566 | default: |
567 | log.Println("Unhandled account with id", data.AccountId, "for", data.ServiceName) |
568 | return false |
569 | } |
570 | - mgr[accountKey] = NewAccountManager(watchers[data.ServiceType], postWatch, plugin) |
571 | + mgr[accountKey] = NewAccountService(watcher, postWatch, plugin) |
572 | mgr[accountKey].updateAuthData(data) |
573 | wg.Add(1) |
574 | go func() { |
575 | @@ -165,27 +174,14 @@ |
576 | L: |
577 | for { |
578 | select { |
579 | - case data := <-watchers[SERVICETYPE_CALENDAR].C: |
580 | - if pullAccount(data) == false { |
581 | - continue L |
582 | - } |
583 | - case data := <-watchers[SERVICETYPE_WEBAPPS].C: |
584 | - if pullAccount(data) == false { |
585 | + case data := <-watcher.C: |
586 | + if pullAccount(data) == false { |
587 | + log.Println("pullAccount returned false, continuing") |
588 | continue L |
589 | } |
590 | case <-pollBus.PollChan: |
591 | wg.Wait() // Finish all running Poll() calls before potentially polling the same accounts again |
592 | - for _, v := range mgr { |
593 | - if v.authData.Error != plugins.ErrTokenExpired { // Do not poll if the new token hasn't been loaded yet |
594 | - wg.Add(1) |
595 | - go func(accountManager *AccountManager) { |
596 | - defer wg.Done() |
597 | - accountManager.Poll(false) |
598 | - }(v) |
599 | - } else { |
600 | - log.Println("Skipping account with id", v.authData.AccountId, "as it is refreshing its token") |
601 | - } |
602 | - } |
603 | + watcher.Run() |
604 | wg.Wait() |
605 | pollBus.SignalDone() |
606 | } |
607 | |
608 | === modified file 'cmd/account-watcher-test/main.go' |
609 | --- cmd/account-watcher-test/main.go 2014-07-24 05:38:51 +0000 |
610 | +++ cmd/account-watcher-test/main.go 2016-08-02 14:37:44 +0000 |
611 | @@ -2,14 +2,12 @@ |
612 | |
613 | import ( |
614 | "fmt" |
615 | - "os" |
616 | |
617 | "launchpad.net/account-polld/accounts" |
618 | ) |
619 | |
620 | func main() { |
621 | - // Expects a list of service names as command line arguments |
622 | - for data := range accounts.NewWatcher(os.Args[1]).C { |
623 | + for data := range accounts.NewWatcher().C { |
624 | if data.Error != nil { |
625 | fmt.Println("Failed to authenticate account", data.AccountId, ":", data.Error) |
626 | } else { |
627 | |
628 | === added directory 'plugins/caldav' |
629 | === added file 'plugins/caldav/api.go' |
630 | --- plugins/caldav/api.go 1970-01-01 00:00:00 +0000 |
631 | +++ plugins/caldav/api.go 2016-08-02 14:37:44 +0000 |
632 | @@ -0,0 +1,54 @@ |
633 | +/* |
634 | + Copyright 2014 Canonical Ltd. |
635 | + |
636 | + This program is free software: you can redistribute it and/or modify it |
637 | + under the terms of the GNU General Public License version 3, as published |
638 | + by the Free Software Foundation. |
639 | + |
640 | + This program is distributed in the hope that it will be useful, but |
641 | + WITHOUT ANY WARRANTY; without even the implied warranties of |
642 | + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
643 | + PURPOSE. See the GNU General Public License for more details. |
644 | + |
645 | + You should have received a copy of the GNU General Public License along |
646 | + with this program. If not, see <http://www.gnu.org/licenses/>. |
647 | +*/ |
648 | + |
649 | +package caldav |
650 | + |
651 | +import ( |
652 | + "fmt" |
653 | +) |
654 | + |
655 | +// eventList holds a response to call to Calendar.events: list |
656 | +// defined in https://developers.google.com/google-apps/calendar/v3/reference/events/list#response |
657 | +type eventList struct { |
658 | + // Messages holds a list of message. |
659 | + Events []event `json:"items"` |
660 | +} |
661 | + |
662 | +// event holds the event data response for a Calendar.event. |
663 | +// The full definition of a message is defined in |
664 | +// https://developers.google.com/google-apps/calendar/v3/reference/events#resource-representations |
665 | +type event struct { |
666 | + // Id is the immutable ID of the message. |
667 | + Etag string `json:"etag"` |
668 | + // ThreadId is the ID of the thread the message belongs to. |
669 | + Summary string `json:"summary"` |
670 | +} |
671 | + |
672 | +func (e event) String() string { |
673 | + return fmt.Sprintf("Id: %s, snippet: '%s'\n", e.Etag, e.Summary) |
674 | +} |
675 | + |
676 | +type errorResp struct { |
677 | + Err struct { |
678 | + Code uint64 `json:"code"` |
679 | + Message string `json:"message"` |
680 | + Errors []struct { |
681 | + Domain string `json:"domain"` |
682 | + Reason string `json:"reason"` |
683 | + Message string `json:"message"` |
684 | + } `json:"errors"` |
685 | + } `json:"error"` |
686 | +} |
687 | |
688 | === added file 'plugins/caldav/caldav.go' |
689 | --- plugins/caldav/caldav.go 1970-01-01 00:00:00 +0000 |
690 | +++ plugins/caldav/caldav.go 2016-08-02 14:37:44 +0000 |
691 | @@ -0,0 +1,203 @@ |
692 | +/* |
693 | + Copyright 2016 Canonical Ltd. |
694 | + |
695 | + This program is free software: you can redistribute it and/or modify it |
696 | + under the terms of the GNU General Public License version 3, as published |
697 | + by the Free Software Foundation. |
698 | + |
699 | + This program is distributed in the hope that it will be useful, but |
700 | + WITHOUT ANY WARRANTY; without even the implied warranties of |
701 | + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
702 | + PURPOSE. See the GNU General Public License for more details. |
703 | + |
704 | + You should have received a copy of the GNU General Public License along |
705 | + with this program. If not, see <http://www.gnu.org/licenses/>. |
706 | +*/ |
707 | + |
708 | +package caldav |
709 | + |
710 | +import ( |
711 | + "bytes" |
712 | + "fmt" |
713 | + "io/ioutil" |
714 | + "log" |
715 | + "net/http" |
716 | + "net/url" |
717 | + "os" |
718 | + "strings" |
719 | + "time" |
720 | + |
721 | + "launchpad.net/account-polld/accounts" |
722 | + "launchpad.net/account-polld/plugins" |
723 | + "launchpad.net/account-polld/syncmonitor" |
724 | +) |
725 | + |
726 | +const ( |
727 | + APP_ID = "com.ubuntu.calendar_calendar" |
728 | + pluginName = "caldav" |
729 | +) |
730 | + |
731 | +type CalDavPlugin struct { |
732 | + accountId uint |
733 | +} |
734 | + |
735 | +func New(accountId uint) *CalDavPlugin { |
736 | + return &CalDavPlugin{accountId: accountId} |
737 | +} |
738 | + |
739 | +func (p *CalDavPlugin) ApplicationId() plugins.ApplicationId { |
740 | + return plugins.ApplicationId(APP_ID) |
741 | +} |
742 | + |
743 | +func (p *CalDavPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) { |
744 | + // This envvar check is to ease testing. |
745 | + if token := os.Getenv("ACCOUNT_POLLD_TOKEN_CALDAV"); token != "" { |
746 | + log.Print("Using token from: ACCOUNT_POLLD_TOKEN_CALDAV env var") |
747 | + authData.AccessToken = token |
748 | + } |
749 | + |
750 | + log.Print("Check calendar changes for account:", p.accountId) |
751 | + |
752 | + syncMonitor := syncmonitor.NewSyncMonitor() |
753 | + if syncMonitor == nil { |
754 | + log.Print("Sync monitor not available yet.") |
755 | + return nil, nil |
756 | + } |
757 | + |
758 | + state, err := syncMonitor.State() |
759 | + if err != nil { |
760 | + log.Print("Fail to retrieve sync monitor state ", err) |
761 | + return nil, nil |
762 | + } |
763 | + if state != "idle" { |
764 | + log.Print("Sync monitor is not on 'idle' state, try later!") |
765 | + return nil, nil |
766 | + } |
767 | + |
768 | + calendars, err := syncMonitor.ListCalendarsByAccount(p.accountId) |
769 | + if err != nil { |
770 | + log.Print("Calendar plugin ", p.accountId, ": cannot load calendars: ", err) |
771 | + return nil, nil |
772 | + } |
773 | + |
774 | + var calendarsToSync []string |
775 | + log.Print("Number of calendars for account:", p.accountId, " size:", len(calendars)) |
776 | + |
777 | + for id, calendar := range calendars { |
778 | + lastSyncDate, err := syncMonitor.LastSyncDate(p.accountId, id) |
779 | + if err != nil { |
780 | + log.Print("\tcalendar: ", id, ", cannot load previous sync date: ", err, ". Try next time.") |
781 | + continue |
782 | + } else { |
783 | + log.Print("\tcalendar: ", id, " Url: ", calendar, " last sync date: ", lastSyncDate) |
784 | + } |
785 | + |
786 | + var needSync bool |
787 | + needSync = (len(lastSyncDate) == 0) |
788 | + |
789 | + if !needSync { |
790 | + resp, err := p.requestChanges(authData, calendar, lastSyncDate) |
791 | + if err != nil { |
792 | + log.Print("\tERROR: Fail to query for changes: ", err) |
793 | + continue |
794 | + } |
795 | + |
796 | + needSync, err = p.containEvents(resp) |
797 | + if err != nil { |
798 | + log.Print("\tERROR: Fail to parse changes: ", err) |
799 | + if err == plugins.ErrTokenExpired { |
800 | + log.Print("\t\tAbort poll") |
801 | + return nil, err |
802 | + } else { |
803 | + continue |
804 | + } |
805 | + } |
806 | + } |
807 | + |
808 | + if needSync { |
809 | + log.Print("\tCalendar needs sync: ", id) |
810 | + calendarsToSync = append(calendarsToSync, id) |
811 | + } else { |
812 | + log.Print("\tFound no calendar updates for account: ", p.accountId, " calendar: ", id) |
813 | + } |
814 | + } |
815 | + |
816 | + if len(calendarsToSync) > 0 { |
817 | + log.Print("Request account sync") |
818 | + err = syncMonitor.SyncAccount(p.accountId, calendarsToSync) |
819 | + if err != nil { |
820 | + log.Print("ERROR: Fail to start account sync ", p.accountId, " message: ", err) |
821 | + } |
822 | + } |
823 | + |
824 | + return nil, nil |
825 | +} |
826 | + |
827 | +func (p *CalDavPlugin) containEvents(resp *http.Response) (bool, error) { |
828 | + defer resp.Body.Close() |
829 | + log.Print("RESPONSE CODE ----:", resp.StatusCode) |
830 | + |
831 | + if resp.StatusCode != 207 { |
832 | + var errResp errorResp |
833 | + log.Print("Invalid response:", errResp.Err.Code) |
834 | + return false, nil |
835 | + } else { |
836 | + data, err := ioutil.ReadAll(resp.Body) |
837 | + if err != nil { |
838 | + return false, err |
839 | + } |
840 | + fmt.Printf("DATA: %s", data) |
841 | + return strings.Contains(string(data), "BEGIN:VEVENT"), nil |
842 | + } |
843 | + |
844 | + return false, nil |
845 | +} |
846 | + |
847 | +func (p *CalDavPlugin) requestChanges(authData *accounts.AuthData, calendar string, lastSyncDate string) (*http.Response, error) { |
848 | + u, err := url.Parse(calendar) |
849 | + if err != nil { |
850 | + return nil, err |
851 | + } |
852 | + startDate, err := time.Parse(time.RFC3339, lastSyncDate) |
853 | + if err != nil { |
854 | + log.Print("Fail to parse date: ", lastSyncDate) |
855 | + return nil, err |
856 | + } |
857 | + |
858 | + // Start date will be one minute before last sync |
859 | + startDate = startDate.Add(time.Duration(-1) * time.Minute) |
860 | + |
861 | + // End Date will be one year in the future from now |
862 | + endDate := time.Now().AddDate(1, 0, 0).UTC() |
863 | + |
864 | + log.Print("Calendar Url:", calendar) |
865 | + //u.Path += "/remote.php/caldav/calendars/renatox@gmail.com/" + calendar |
866 | + |
867 | + //GET https://my.owndrive.com:443/remote.php/caldav/calendars/renatox%40gmail.com/teste/ |
868 | + query := "<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">\n" |
869 | + query += "<d:prop>\n" |
870 | + query += "<d:getetag />\n" |
871 | + query += "<c:calendar-data />\n" |
872 | + query += "</d:prop>\n" |
873 | + query += "<c:filter>\n" |
874 | + query += "<c:comp-filter name=\"VCALENDAR\">\n" |
875 | + query += "<c:comp-filter name=\"VEVENT\">\n" |
876 | + query += "<c:prop-filter name=\"LAST-MODIFIED\">\n" |
877 | + query += "<c:time-range start=\"" + startDate.Format("20060102T150405Z") + "\" end=\"" + endDate.Format("20060102T150405Z") + "\"/>\n" |
878 | + query += "</c:prop-filter>\n" |
879 | + query += "</c:comp-filter>\n" |
880 | + query += "</c:comp-filter>\n" |
881 | + query += "</c:filter>\n" |
882 | + query += "</c:calendar-query>\n" |
883 | + log.Print("Query: ", query) |
884 | + req, err := http.NewRequest("REPORT", u.String(), bytes.NewBufferString(query)) |
885 | + if err != nil { |
886 | + return nil, err |
887 | + } |
888 | + req.Header.Set("Depth", "1") |
889 | + req.Header.Set("Prefer", "return-minimal") |
890 | + req.Header.Set("Content-Type", "application/xml; charset=utf-8") |
891 | + req.SetBasicAuth(authData.UserName, authData.Secret) |
892 | + |
893 | + return http.DefaultClient.Do(req) |
894 | +} |
895 | |
896 | === added directory 'plugins/dekko' |
897 | === added file 'plugins/dekko/api.go' |
898 | --- plugins/dekko/api.go 1970-01-01 00:00:00 +0000 |
899 | +++ plugins/dekko/api.go 2016-08-02 14:37:44 +0000 |
900 | @@ -0,0 +1,127 @@ |
901 | +/* |
902 | + Copyright 2014 Canonical Ltd. |
903 | + |
904 | + This program is free software: you can redistribute it and/or modify it |
905 | + under the terms of the GNU General Public License version 3, as published |
906 | + by the Free Software Foundation. |
907 | + |
908 | + This program is distributed in the hope that it will be useful, but |
909 | + WITHOUT ANY WARRANTY; without even the implied warranties of |
910 | + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
911 | + PURPOSE. See the GNU General Public License for more details. |
912 | + |
913 | + You should have received a copy of the GNU General Public License along |
914 | + with this program. If not, see <http://www.gnu.org/licenses/>. |
915 | +*/ |
916 | + |
917 | +package dekko |
918 | + |
919 | +import ( |
920 | + "fmt" |
921 | + "time" |
922 | + |
923 | + "launchpad.net/account-polld/plugins" |
924 | +) |
925 | + |
926 | +const gmailTime = "Mon, 2 Jan 2006 15:04:05 -0700" |
927 | + |
928 | +type pushes map[string]*plugins.PushMessage |
929 | +type headers map[string]string |
930 | + |
931 | +// messageList holds a response to call to Users.messages: list |
932 | +// defined in https://developers.google.com/gmail/api/v1/reference/users/messages/list |
933 | +type messageList struct { |
934 | + // Messages holds a list of message. |
935 | + Messages []message `json:"messages"` |
936 | + // NextPageToken is used to retrieve the next page of results in the list. |
937 | + NextPageToken string `json:"nextPageToken"` |
938 | + // ResultSizeEstimage is the estimated total number of results. |
939 | + ResultSizeEstimage uint64 `json:"resultSizeEstimate"` |
940 | +} |
941 | + |
942 | +// message holds a partial response for a Users.messages. |
943 | +// The full definition of a message is defined in |
944 | +// https://developers.google.com/gmail/api/v1/reference/users/messages#resource |
945 | +type message struct { |
946 | + // Id is the immutable ID of the message. |
947 | + Id string `json:"id"` |
948 | + // ThreadId is the ID of the thread the message belongs to. |
949 | + ThreadId string `json:"threadId"` |
950 | + // HistoryId is the ID of the last history record that modified |
951 | + // this message. |
952 | + HistoryId string `json:"historyId"` |
953 | + // Snippet is a short part of the message text. This text is |
954 | + // used for the push message summary. |
955 | + Snippet string `json:"snippet"` |
956 | + // Payload represents the message payload. |
957 | + Payload payload `json:"payload"` |
958 | +} |
959 | + |
960 | +func (m message) String() string { |
961 | + return fmt.Sprintf("Id: %d, snippet: '%s'\n", m.Id, m.Snippet[:10]) |
962 | +} |
963 | + |
964 | +// ById implements sort.Interface for []message based on |
965 | +// the Id field. |
966 | +type byId []message |
967 | + |
968 | +func (m byId) Len() int { return len(m) } |
969 | +func (m byId) Swap(i, j int) { m[i], m[j] = m[j], m[i] } |
970 | +func (m byId) Less(i, j int) bool { return m[i].Id < m[j].Id } |
971 | + |
972 | +// payload represents the message payload. |
973 | +type payload struct { |
974 | + Headers []messageHeader `json:"headers"` |
975 | +} |
976 | + |
977 | +func (p *payload) mapHeaders() headers { |
978 | + headers := make(map[string]string) |
979 | + for _, hdr := range p.Headers { |
980 | + headers[hdr.Name] = hdr.Value |
981 | + } |
982 | + return headers |
983 | +} |
984 | + |
985 | +func (hdr headers) getTimestamp() time.Time { |
986 | + timestamp, ok := hdr[hdrDATE] |
987 | + if !ok { |
988 | + return time.Now() |
989 | + } |
990 | + |
991 | + if t, err := time.Parse(gmailTime, timestamp); err == nil { |
992 | + return t |
993 | + } |
994 | + return time.Now() |
995 | +} |
996 | + |
997 | +func (hdr headers) getEpoch() int64 { |
998 | + return hdr.getTimestamp().Unix() |
999 | +} |
1000 | + |
1001 | +// messageHeader represents the message headers. |
1002 | +type messageHeader struct { |
1003 | + Name string `json:"name"` |
1004 | + Value string `json:"value"` |
1005 | +} |
1006 | + |
1007 | +type errorResp struct { |
1008 | + Err struct { |
1009 | + Code uint64 `json:"code"` |
1010 | + Message string `json:"message"` |
1011 | + Errors []struct { |
1012 | + Domain string `json:"domain"` |
1013 | + Reason string `json:"reason"` |
1014 | + Message string `json:"message"` |
1015 | + } `json:"errors"` |
1016 | + } `json:"error"` |
1017 | +} |
1018 | + |
1019 | +func (err *errorResp) Error() string { |
1020 | + return fmt.Sprint("backend response:", err.Err.Message) |
1021 | +} |
1022 | + |
1023 | +const ( |
1024 | + hdrDATE = "Date" |
1025 | + hdrSUBJECT = "Subject" |
1026 | + hdrFROM = "From" |
1027 | +) |
1028 | |
1029 | === added file 'plugins/dekko/dekko.go' |
1030 | --- plugins/dekko/dekko.go 1970-01-01 00:00:00 +0000 |
1031 | +++ plugins/dekko/dekko.go 2016-08-02 14:37:44 +0000 |
1032 | @@ -0,0 +1,346 @@ |
1033 | +/* |
1034 | + Copyright 2014 Canonical Ltd. |
1035 | + |
1036 | + This program is free software: you can redistribute it and/or modify it |
1037 | + under the terms of the GNU General Public License version 3, as published |
1038 | + by the Free Software Foundation. |
1039 | + |
1040 | + This program is distributed in the hope that it will be useful, but |
1041 | + WITHOUT ANY WARRANTY; without even the implied warranties of |
1042 | + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1043 | + PURPOSE. See the GNU General Public License for more details. |
1044 | + |
1045 | + You should have received a copy of the GNU General Public License along |
1046 | + with this program. If not, see <http://www.gnu.org/licenses/>. |
1047 | +*/ |
1048 | + |
1049 | +package dekko |
1050 | + |
1051 | +import ( |
1052 | + "encoding/json" |
1053 | + "fmt" |
1054 | + "net/http" |
1055 | + "net/mail" |
1056 | + "net/url" |
1057 | + "os" |
1058 | + "regexp" |
1059 | + "sort" |
1060 | + "strings" |
1061 | + "time" |
1062 | + |
1063 | + "log" |
1064 | + |
1065 | + "launchpad.net/account-polld/accounts" |
1066 | + "launchpad.net/account-polld/gettext" |
1067 | + "launchpad.net/account-polld/plugins" |
1068 | + "launchpad.net/account-polld/qtcontact" |
1069 | +) |
1070 | + |
1071 | +const ( |
1072 | + APP_ID = "dekko.dekkoproject_dekko" |
1073 | + dekkoDispatchUrl = "dekko://notify/%d/%s/%s" |
1074 | + // If there's more than 10 emails in one batch, we don't show 10 notification |
1075 | + // bubbles, but instead show one summary. We always show all notifications in the |
1076 | + // indicator. |
1077 | + individualNotificationsLimit = 10 |
1078 | + pluginName = "dekko" |
1079 | +) |
1080 | + |
1081 | +type reportedIdMap map[string]time.Time |
1082 | + |
1083 | +var baseUrl, _ = url.Parse("https://www.googleapis.com/gmail/v1/users/me/") |
1084 | + |
1085 | +// timeDelta defines how old messages can be to be reported. |
1086 | +var timeDelta = time.Duration(time.Hour * 24) |
1087 | + |
1088 | +// trackDelta defines how old messages can be before removed from tracking |
1089 | +var trackDelta = time.Duration(time.Hour * 24 * 7) |
1090 | + |
1091 | +// relativeTimeDelta is the same as timeDelta |
1092 | +var relativeTimeDelta string = "1d" |
1093 | + |
1094 | +// regexp for identifying non-ascii characters |
1095 | +var nonAsciiChars, _ = regexp.Compile("[^\x00-\x7F]") |
1096 | + |
1097 | +type GmailPlugin struct { |
1098 | + // reportedIds holds the messages that have already been notified. This |
1099 | + // approach is taken against timestamps as it avoids needing to call |
1100 | + // get on the message. |
1101 | + reportedIds reportedIdMap |
1102 | + accountId uint |
1103 | +} |
1104 | + |
1105 | +func idsFromPersist(accountId uint) (ids reportedIdMap, err error) { |
1106 | + err = plugins.FromPersist(pluginName, accountId, &ids) |
1107 | + if err != nil { |
1108 | + return nil, err |
1109 | + } |
1110 | + // discard old ids |
1111 | + timestamp := time.Now() |
1112 | + for k, v := range ids { |
1113 | + delta := timestamp.Sub(v) |
1114 | + if delta > trackDelta { |
1115 | + log.Print("gmail plugin ", accountId, ": deleting ", k, " as ", delta, " is greater than ", trackDelta) |
1116 | + delete(ids, k) |
1117 | + } |
1118 | + } |
1119 | + return ids, nil |
1120 | +} |
1121 | + |
1122 | +func (ids reportedIdMap) persist(accountId uint) (err error) { |
1123 | + err = plugins.Persist(pluginName, accountId, ids) |
1124 | + if err != nil { |
1125 | + log.Print("gmail plugin ", accountId, ": failed to save state: ", err) |
1126 | + } |
1127 | + return nil |
1128 | +} |
1129 | + |
1130 | +func New(accountId uint) *GmailPlugin { |
1131 | + reportedIds, err := idsFromPersist(accountId) |
1132 | + if err != nil { |
1133 | + log.Print("gmail plugin ", accountId, ": cannot load previous state from storage: ", err) |
1134 | + } else { |
1135 | + log.Print("gmail plugin ", accountId, ": last state loaded from storage") |
1136 | + } |
1137 | + return &GmailPlugin{reportedIds: reportedIds, accountId: accountId} |
1138 | +} |
1139 | + |
1140 | +func (p *GmailPlugin) ApplicationId() plugins.ApplicationId { |
1141 | + return plugins.ApplicationId(APP_ID) |
1142 | +} |
1143 | + |
1144 | +func (p *GmailPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) { |
1145 | + // This envvar check is to ease testing. |
1146 | + if token := os.Getenv("ACCOUNT_POLLD_TOKEN_GMAIL"); token != "" { |
1147 | + authData.AccessToken = token |
1148 | + } |
1149 | + |
1150 | + resp, err := p.requestMessageList(authData.AccessToken) |
1151 | + if err != nil { |
1152 | + return nil, err |
1153 | + } |
1154 | + messages, err := p.parseMessageListResponse(resp) |
1155 | + if err != nil { |
1156 | + return nil, err |
1157 | + } |
1158 | + |
1159 | + // TODO use the batching API defined in https://developers.google.com/gmail/api/guides/batch |
1160 | + for i := range messages { |
1161 | + resp, err := p.requestMessage(messages[i].Id, authData.AccessToken) |
1162 | + if err != nil { |
1163 | + return nil, err |
1164 | + } |
1165 | + messages[i], err = p.parseMessageResponse(resp) |
1166 | + if err != nil { |
1167 | + return nil, err |
1168 | + } |
1169 | + } |
1170 | + notif, err := p.createNotifications(messages) |
1171 | + if err != nil { |
1172 | + return nil, err |
1173 | + } |
1174 | + return []*plugins.PushMessageBatch{ |
1175 | + &plugins.PushMessageBatch{ |
1176 | + Messages: notif, |
1177 | + Limit: individualNotificationsLimit, |
1178 | + OverflowHandler: p.handleOverflow, |
1179 | + Tag: "dekko", |
1180 | + }}, nil |
1181 | + |
1182 | +} |
1183 | + |
1184 | +func (p *GmailPlugin) reported(id string) bool { |
1185 | + _, ok := p.reportedIds[id] |
1186 | + return ok |
1187 | +} |
1188 | + |
1189 | +func (p *GmailPlugin) createNotifications(messages []message) ([]*plugins.PushMessage, error) { |
1190 | + timestamp := time.Now() |
1191 | + pushMsgMap := make(pushes) |
1192 | + |
1193 | + for _, msg := range messages { |
1194 | + hdr := msg.Payload.mapHeaders() |
1195 | + |
1196 | + from := hdr[hdrFROM] |
1197 | + var avatarPath string |
1198 | + |
1199 | + emailAddress, err := mail.ParseAddress(from) |
1200 | + if err != nil { |
1201 | + // If the email address contains non-ascii characters, we get an |
1202 | + // error so we're going to try again, this time mangling the name |
1203 | + // by removing all non-ascii characters. We only care about the email |
1204 | + // address here anyway. |
1205 | + // XXX: We can't check the error message due to [1]: the error |
1206 | + // message is different in go < 1.3 and > 1.5. |
1207 | + // [1] https://github.com/golang/go/issues/12492 |
1208 | + mangledAddr := nonAsciiChars.ReplaceAllString(from, "") |
1209 | + mangledEmail, mangledParseError := mail.ParseAddress(mangledAddr) |
1210 | + if mangledParseError == nil { |
1211 | + emailAddress = mangledEmail |
1212 | + } |
1213 | + } else if emailAddress.Name != "" { |
1214 | + // We only want the Name if the first ParseAddress |
1215 | + // call was successful. I.e. we do not want the name |
1216 | + // from a mangled email address. |
1217 | + from = emailAddress.Name |
1218 | + } |
1219 | + |
1220 | + if emailAddress != nil { |
1221 | + avatarPath = qtcontact.GetAvatar(emailAddress.Address) |
1222 | + // If icon path starts with a path separator, assume local file path, |
1223 | + // encode it and prepend file scheme defined in RFC 1738. |
1224 | + if strings.HasPrefix(avatarPath, string(os.PathSeparator)) { |
1225 | + avatarPath = url.QueryEscape(avatarPath) |
1226 | + avatarPath = "file://" + avatarPath |
1227 | + } |
1228 | + } |
1229 | + |
1230 | + msgStamp := hdr.getTimestamp() |
1231 | + |
1232 | + if _, ok := pushMsgMap[msg.ThreadId]; ok { |
1233 | + // TRANSLATORS: the %s is an appended "from" corresponding to an specific email thread |
1234 | + pushMsgMap[msg.ThreadId].Notification.Card.Summary += fmt.Sprintf(gettext.Gettext(", %s"), from) |
1235 | + } else if timestamp.Sub(msgStamp) < timeDelta { |
1236 | + // TRANSLATORS: the %s is the "from" header corresponding to a specific email |
1237 | + summary := fmt.Sprintf(gettext.Gettext("%s"), from) |
1238 | + // TRANSLATORS: the first %s refers to the email "subject", the second %s refers "from" |
1239 | + body := fmt.Sprintf(gettext.Gettext("%s\n%s"), hdr[hdrSUBJECT], msg.Snippet) |
1240 | + // fmt with label personal and threadId |
1241 | + action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX", msg.Id) |
1242 | + epoch := hdr.getEpoch() |
1243 | + pushMsgMap[msg.ThreadId] = plugins.NewStandardPushMessage(summary, body, action, avatarPath, epoch) |
1244 | + } else { |
1245 | + log.Print("gmail plugin ", p.accountId, ": skipping message id ", msg.Id, " with date ", msgStamp, " older than ", timeDelta) |
1246 | + } |
1247 | + } |
1248 | + pushMsg := make([]*plugins.PushMessage, 0, len(pushMsgMap)) |
1249 | + for _, v := range pushMsgMap { |
1250 | + pushMsg = append(pushMsg, v) |
1251 | + } |
1252 | + return pushMsg, nil |
1253 | + |
1254 | +} |
1255 | +func (p *GmailPlugin) handleOverflow(pushMsg []*plugins.PushMessage) *plugins.PushMessage { |
1256 | + // TODO it would probably be better to grab the estimate that google returns in the message list. |
1257 | + approxUnreadMessages := len(pushMsg) |
1258 | + |
1259 | + // TRANSLATORS: the %d refers to the number of new email messages. |
1260 | + summary := fmt.Sprintf(gettext.Gettext("You have %d new messages"), approxUnreadMessages) |
1261 | + |
1262 | + body := "" |
1263 | + |
1264 | + // fmt with label personal and no threadId |
1265 | + action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX") |
1266 | + epoch := time.Now().Unix() |
1267 | + |
1268 | + return plugins.NewStandardPushMessage(summary, body, action, "", epoch) |
1269 | +} |
1270 | + |
1271 | +func (p *GmailPlugin) parseMessageListResponse(resp *http.Response) ([]message, error) { |
1272 | + defer resp.Body.Close() |
1273 | + decoder := json.NewDecoder(resp.Body) |
1274 | + |
1275 | + if resp.StatusCode != http.StatusOK { |
1276 | + var errResp errorResp |
1277 | + if err := decoder.Decode(&errResp); err != nil { |
1278 | + return nil, err |
1279 | + } |
1280 | + if errResp.Err.Code == 401 { |
1281 | + return nil, plugins.ErrTokenExpired |
1282 | + } |
1283 | + return nil, &errResp |
1284 | + } |
1285 | + |
1286 | + var messages messageList |
1287 | + if err := decoder.Decode(&messages); err != nil { |
1288 | + return nil, err |
1289 | + } |
1290 | + |
1291 | + filteredMsg := p.messageListFilter(messages.Messages) |
1292 | + |
1293 | + return filteredMsg, nil |
1294 | +} |
1295 | + |
1296 | +// messageListFilter returns a subset of unread messages where the subset |
1297 | +// depends on not being in reportedIds. Before returning, reportedIds is |
1298 | +// updated with the new list of unread messages. |
1299 | +func (p *GmailPlugin) messageListFilter(messages []message) []message { |
1300 | + sort.Sort(byId(messages)) |
1301 | + var reportMsg []message |
1302 | + var ids = make(reportedIdMap) |
1303 | + |
1304 | + for _, msg := range messages { |
1305 | + if !p.reported(msg.Id) { |
1306 | + reportMsg = append(reportMsg, msg) |
1307 | + } |
1308 | + ids[msg.Id] = time.Now() |
1309 | + } |
1310 | + p.reportedIds = ids |
1311 | + p.reportedIds.persist(p.accountId) |
1312 | + return reportMsg |
1313 | +} |
1314 | + |
1315 | +func (p *GmailPlugin) parseMessageResponse(resp *http.Response) (message, error) { |
1316 | + defer resp.Body.Close() |
1317 | + decoder := json.NewDecoder(resp.Body) |
1318 | + |
1319 | + if resp.StatusCode != http.StatusOK { |
1320 | + var errResp errorResp |
1321 | + if err := decoder.Decode(&errResp); err != nil { |
1322 | + return message{}, err |
1323 | + } |
1324 | + return message{}, &errResp |
1325 | + } |
1326 | + |
1327 | + var msg message |
1328 | + if err := decoder.Decode(&msg); err != nil { |
1329 | + return message{}, err |
1330 | + } |
1331 | + |
1332 | + return msg, nil |
1333 | +} |
1334 | + |
1335 | +func (p *GmailPlugin) requestMessage(id, accessToken string) (*http.Response, error) { |
1336 | + u, err := baseUrl.Parse("messages/" + id) |
1337 | + if err != nil { |
1338 | + return nil, err |
1339 | + } |
1340 | + |
1341 | + query := u.Query() |
1342 | + // only request specific fields |
1343 | + query.Add("fields", "snippet,threadId,id,payload/headers") |
1344 | + // get the full message to get From and Subject from headers |
1345 | + query.Add("format", "full") |
1346 | + u.RawQuery = query.Encode() |
1347 | + |
1348 | + req, err := http.NewRequest("GET", u.String(), nil) |
1349 | + if err != nil { |
1350 | + return nil, err |
1351 | + } |
1352 | + req.Header.Set("Authorization", "Bearer "+accessToken) |
1353 | + |
1354 | + return http.DefaultClient.Do(req) |
1355 | +} |
1356 | + |
1357 | +func (p *GmailPlugin) requestMessageList(accessToken string) (*http.Response, error) { |
1358 | + u, err := baseUrl.Parse("messages") |
1359 | + if err != nil { |
1360 | + return nil, err |
1361 | + } |
1362 | + |
1363 | + query := u.Query() |
1364 | + |
1365 | + // get all unread inbox emails received after |
1366 | + // the last time we checked. If this is the first |
1367 | + // time we check, get unread emails after timeDelta |
1368 | + query.Add("q", fmt.Sprintf("is:unread in:inbox newer_than:%s", relativeTimeDelta)) |
1369 | + u.RawQuery = query.Encode() |
1370 | + |
1371 | + req, err := http.NewRequest("GET", u.String(), nil) |
1372 | + if err != nil { |
1373 | + return nil, err |
1374 | + } |
1375 | + req.Header.Set("Authorization", "Bearer "+accessToken) |
1376 | + |
1377 | + return http.DefaultClient.Do(req) |
1378 | +} |
1379 | |
1380 | === modified file 'plugins/gcalendar/gcalendar.go' |
1381 | --- plugins/gcalendar/gcalendar.go 2016-07-12 14:36:52 +0000 |
1382 | +++ plugins/gcalendar/gcalendar.go 2016-08-02 14:37:44 +0000 |
1383 | @@ -25,6 +25,7 @@ |
1384 | |
1385 | "launchpad.net/account-polld/accounts" |
1386 | "launchpad.net/account-polld/plugins" |
1387 | + "launchpad.net/account-polld/syncmonitor" |
1388 | ) |
1389 | |
1390 | const ( |
1391 | @@ -32,7 +33,7 @@ |
1392 | pluginName = "gcalendar" |
1393 | ) |
1394 | |
1395 | -var baseUrl,_ = url.Parse("https://www.googleapis.com/calendar/v3/calendars/") |
1396 | +var baseUrl, _ = url.Parse("https://www.googleapis.com/calendar/v3/calendars/") |
1397 | |
1398 | type GCalendarPlugin struct { |
1399 | accountId uint |
1400 | @@ -55,7 +56,7 @@ |
1401 | |
1402 | log.Print("calendar: Check calendar changes for account:", p.accountId) |
1403 | |
1404 | - syncMonitor := NewSyncMonitor() |
1405 | + syncMonitor := syncmonitor.NewSyncMonitor() |
1406 | if syncMonitor == nil { |
1407 | log.Print("calendar: Sync monitor not available yet.") |
1408 | return nil, nil |
1409 | |
1410 | === removed file 'plugins/gcalendar/syncmonitor.go' |
1411 | --- plugins/gcalendar/syncmonitor.go 2016-06-23 18:06:35 +0000 |
1412 | +++ plugins/gcalendar/syncmonitor.go 1970-01-01 00:00:00 +0000 |
1413 | @@ -1,94 +0,0 @@ |
1414 | -/* |
1415 | - Copyright 2016 Canonical Ltd. |
1416 | - |
1417 | - This program is free software: you can redistribute it and/or modify it |
1418 | - under the terms of the GNU General Public License version 3, as published |
1419 | - by the Free Software Foundation. |
1420 | - |
1421 | - This program is distributed in the hope that it will be useful, but |
1422 | - WITHOUT ANY WARRANTY; without even the implied warranties of |
1423 | - MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1424 | - PURPOSE. See the GNU General Public License for more details. |
1425 | - |
1426 | - You should have received a copy of the GNU General Public License along |
1427 | - with this program. If not, see <http://www.gnu.org/licenses/>. |
1428 | -*/ |
1429 | - |
1430 | -package gcalendar |
1431 | - |
1432 | -import ( |
1433 | - "log" |
1434 | - "runtime" |
1435 | - |
1436 | - "launchpad.net/go-dbus/v1" |
1437 | -) |
1438 | - |
1439 | -const ( |
1440 | - busInterface = "com.canonical.SyncMonitor" |
1441 | - busPath = "/com/canonical/SyncMonitor" |
1442 | - busName = "com.canonical.SyncMonitor" |
1443 | -) |
1444 | - |
1445 | -type SyncMonitor struct { |
1446 | - conn *dbus.Connection |
1447 | - obj *dbus.ObjectProxy |
1448 | -} |
1449 | - |
1450 | -func NewSyncMonitor() *SyncMonitor { |
1451 | - conn, err := dbus.Connect(dbus.SessionBus) |
1452 | - if err != nil { |
1453 | - log.Print("Fail to connect with session bus: ", err) |
1454 | - return nil |
1455 | - } |
1456 | - |
1457 | - p := &SyncMonitor{ |
1458 | - conn: conn, |
1459 | - obj: conn.Object(busName, busPath), |
1460 | - } |
1461 | - runtime.SetFinalizer(p, clean) |
1462 | - return p |
1463 | -} |
1464 | - |
1465 | -func clean(p *SyncMonitor) { |
1466 | - if p.conn != nil { |
1467 | - p.conn.Close() |
1468 | - } |
1469 | -} |
1470 | - |
1471 | -func (p *SyncMonitor) ListCalendarsByAccount(accountId uint) (calendars map[string]string, err error) { |
1472 | - message, err := p.obj.Call(busInterface, "listCalendarsByAccount", uint32(accountId)) |
1473 | - if err != nil { |
1474 | - var calendars map[string]string |
1475 | - return calendars, err |
1476 | - } else { |
1477 | - err = message.Args(&calendars) |
1478 | - return calendars, err |
1479 | - } |
1480 | -} |
1481 | - |
1482 | -func (p *SyncMonitor) LastSyncDate(accountId uint, sourceId string) (lastSyncDate string, err error) { |
1483 | - message, err := p.obj.Call(busInterface, "lastSuccessfulSyncDate", uint32(accountId), sourceId) |
1484 | - if err != nil { |
1485 | - return "", err |
1486 | - } else { |
1487 | - var lastSyncDate string |
1488 | - err = message.Args(&lastSyncDate) |
1489 | - return lastSyncDate, err |
1490 | - } |
1491 | -} |
1492 | - |
1493 | -func (p *SyncMonitor) SyncAccount(accountId uint, sources []string) (err error) { |
1494 | - _, err = p.obj.Call(busInterface, "syncAccount", uint32(accountId), sources) |
1495 | - return err |
1496 | -} |
1497 | - |
1498 | -func (p *SyncMonitor) State() (state string, err error) { |
1499 | - message, err := p.obj.Call(busInterface, "state") |
1500 | - if err != nil { |
1501 | - return "", err |
1502 | - } else { |
1503 | - err = message.Args(&state) |
1504 | - return state, err |
1505 | - } |
1506 | -} |
1507 | - |
1508 | |
1509 | === added directory 'syncmonitor' |
1510 | === added file 'syncmonitor/syncmonitor.go' |
1511 | --- syncmonitor/syncmonitor.go 1970-01-01 00:00:00 +0000 |
1512 | +++ syncmonitor/syncmonitor.go 2016-08-02 14:37:44 +0000 |
1513 | @@ -0,0 +1,93 @@ |
1514 | +/* |
1515 | + Copyright 2016 Canonical Ltd. |
1516 | + |
1517 | + This program is free software: you can redistribute it and/or modify it |
1518 | + under the terms of the GNU General Public License version 3, as published |
1519 | + by the Free Software Foundation. |
1520 | + |
1521 | + This program is distributed in the hope that it will be useful, but |
1522 | + WITHOUT ANY WARRANTY; without even the implied warranties of |
1523 | + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
1524 | + PURPOSE. See the GNU General Public License for more details. |
1525 | + |
1526 | + You should have received a copy of the GNU General Public License along |
1527 | + with this program. If not, see <http://www.gnu.org/licenses/>. |
1528 | +*/ |
1529 | + |
1530 | +package syncmonitor |
1531 | + |
1532 | +import ( |
1533 | + "log" |
1534 | + "runtime" |
1535 | + |
1536 | + "launchpad.net/go-dbus/v1" |
1537 | +) |
1538 | + |
1539 | +const ( |
1540 | + busInterface = "com.canonical.SyncMonitor" |
1541 | + busPath = "/com/canonical/SyncMonitor" |
1542 | + busName = "com.canonical.SyncMonitor" |
1543 | +) |
1544 | + |
1545 | +type SyncMonitor struct { |
1546 | + conn *dbus.Connection |
1547 | + obj *dbus.ObjectProxy |
1548 | +} |
1549 | + |
1550 | +func NewSyncMonitor() *SyncMonitor { |
1551 | + conn, err := dbus.Connect(dbus.SessionBus) |
1552 | + if err != nil { |
1553 | + log.Print("Fail to connect with session bus: ", err) |
1554 | + return nil |
1555 | + } |
1556 | + |
1557 | + p := &SyncMonitor{ |
1558 | + conn: conn, |
1559 | + obj: conn.Object(busName, busPath), |
1560 | + } |
1561 | + runtime.SetFinalizer(p, clean) |
1562 | + return p |
1563 | +} |
1564 | + |
1565 | +func clean(p *SyncMonitor) { |
1566 | + if p.conn != nil { |
1567 | + p.conn.Close() |
1568 | + } |
1569 | +} |
1570 | + |
1571 | +func (p *SyncMonitor) ListCalendarsByAccount(accountId uint) (calendars map[string]string, err error) { |
1572 | + message, err := p.obj.Call(busInterface, "listCalendarsByAccount", uint32(accountId)) |
1573 | + if err != nil { |
1574 | + var calendars map[string]string |
1575 | + return calendars, err |
1576 | + } else { |
1577 | + err = message.Args(&calendars) |
1578 | + return calendars, err |
1579 | + } |
1580 | +} |
1581 | + |
1582 | +func (p *SyncMonitor) LastSyncDate(accountId uint, sourceId string) (lastSyncDate string, err error) { |
1583 | + message, err := p.obj.Call(busInterface, "lastSuccessfulSyncDate", uint32(accountId), sourceId) |
1584 | + if err != nil { |
1585 | + return "", err |
1586 | + } else { |
1587 | + var lastSyncDate string |
1588 | + err = message.Args(&lastSyncDate) |
1589 | + return lastSyncDate, err |
1590 | + } |
1591 | +} |
1592 | + |
1593 | +func (p *SyncMonitor) SyncAccount(accountId uint, sources []string) (err error) { |
1594 | + _, err = p.obj.Call(busInterface, "syncAccount", uint32(accountId), sources) |
1595 | + return err |
1596 | +} |
1597 | + |
1598 | +func (p *SyncMonitor) State() (state string, err error) { |
1599 | + message, err := p.obj.Call(busInterface, "state") |
1600 | + if err != nil { |
1601 | + return "", err |
1602 | + } else { |
1603 | + err = message.Args(&state) |
1604 | + return state, err |
1605 | + } |
1606 | +} |
FAILED: Continuous integration, rev:179 /code.launchpad .net/~renatofil ho/account- polld/account- polld-caldav/ +merge/ 299868/ +edit-commit- message
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https:/
https:/ /jenkins. canonical. com/system- apps/job/ lp-account- polld-ci/ 11/ /jenkins. canonical. com/system- apps/job/ build/953 /jenkins. canonical. com/system- apps/job/ test-0- autopkgtest/ label=phone- armhf,release= vivid+overlay, testname= default/ 169 /jenkins. canonical. com/system- apps/job/ build-0- fetch/953 /jenkins. canonical. com/system- apps/job/ build-1- sourcepkg/ release= vivid+overlay/ 858 /jenkins. canonical. com/system- apps/job/ build-1- sourcepkg/ release= xenial+ overlay/ 858 /jenkins. canonical. com/system- apps/job/ build-1- sourcepkg/ release= yakkety/ 858 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= vivid+overlay/ 855 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= vivid+overlay/ 855/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= xenial+ overlay/ 855 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= xenial+ overlay/ 855/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= yakkety/ 855 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= yakkety/ 855/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= vivid+overlay/ 855 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= vivid+overlay/ 855/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= xenial+ overlay/ 855 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= xenial+ overlay/ 855/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= yakkety/ 855 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= yakkety/ 855/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= vivid+overlay/ 855 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= vivid+overlay/ 855/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= xenial+ overlay/ 855 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= xenial+ overlay/ 855/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= yakkety/ 855 /jenkins. canonical. com/system- apps/job/ build-2- binpkg...
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/