Merge lp:~mardy/account-polld/dekko-gmail into lp:~ubuntu-push-hackers/account-polld/trunk
- dekko-gmail
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jonas G. Drange | ||||
Approved revision: | 170 | ||||
Merged at revision: | 174 | ||||
Proposed branch: | lp:~mardy/account-polld/dekko-gmail | ||||
Merge into: | lp:~ubuntu-push-hackers/account-polld/trunk | ||||
Prerequisite: | lp:~mardy/account-polld/skip-unsupported | ||||
Diff against target: |
521 lines (+479/-0) 3 files modified
cmd/account-polld/main.go (+6/-0) plugins/dekko/api.go (+127/-0) plugins/dekko/dekko.go (+346/-0) |
||||
To merge this branch: | bzr merge lp:~mardy/account-polld/dekko-gmail | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jonas G. Drange (community) | Approve | ||
system-apps-ci-bot | continuous-integration | Approve | |
Review via email: mp+300873@code.launchpad.net |
Commit message
Add Dekko GMail plugin
Description of the change
Add Dekko GMail plugin
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
Jonas G. Drange (jonas-drange) wrote : | # |
Everything looks okay, except for the duplication of some code from the gmail plugin. Have you looked at the possibility of sharing some of the code?
Alberto Mardegan (mardy) wrote : | # |
> Everything looks okay, except for the duplication of some code from the gmail
> plugin. Have you looked at the possibility of sharing some of the code?
I haven't. :-) The reason is that the plan is to extend account-polld to support out-of-process plugins, after which we could reuse Dekko's own IMAP/POP3 client implementation and drop this plugin altogether.
You can read more about this in bug 1421923, from comment #18 on.
Jonas G. Drange (jonas-drange) wrote : | # |
Then let's not invest too much time and energy in introducing some sort of code sharing between plugins—that may benefit from being completely separate packages anyway.
Preview Diff
1 | === modified file 'cmd/account-polld/main.go' |
2 | --- cmd/account-polld/main.go 2016-07-22 11:44:01 +0000 |
3 | +++ cmd/account-polld/main.go 2016-07-22 11:44:01 +0000 |
4 | @@ -27,6 +27,7 @@ |
5 | "launchpad.net/account-polld/accounts" |
6 | "launchpad.net/account-polld/gettext" |
7 | "launchpad.net/account-polld/plugins" |
8 | + "launchpad.net/account-polld/plugins/dekko" |
9 | "launchpad.net/account-polld/plugins/gcalendar" |
10 | "launchpad.net/account-polld/plugins/gmail" |
11 | "launchpad.net/account-polld/plugins/twitter" |
12 | @@ -48,6 +49,7 @@ |
13 | /* Use identifiers and API keys provided by the respective webapps which are the official |
14 | end points for the notifications */ |
15 | const ( |
16 | + SERVICENAME_DEKKO = "dekko.dekkoproject_dekko" |
17 | SERVICENAME_GMAIL = "com.ubuntu.developer.webapps.webapp-gmail_webapp-gmail" |
18 | SERVICENAME_TWITTER = "com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter" |
19 | SERVICENAME_GCALENDAR = "google-caldav" |
20 | @@ -99,6 +101,7 @@ |
21 | |
22 | func monitorAccounts(postWatch chan *PostWatch, pollBus *pollbus.PollBus) { |
23 | watcher := accounts.NewWatcher() |
24 | + watcher.AddService(SERVICENAME_DEKKO) |
25 | watcher.AddService(SERVICENAME_GMAIL) |
26 | watcher.AddService(SERVICENAME_GCALENDAR) |
27 | watcher.AddService(SERVICENAME_TWITTER) |
28 | @@ -131,6 +134,9 @@ |
29 | var plugin plugins.Plugin |
30 | log.Println("Creating plugin for service: ", data.ServiceName) |
31 | switch data.ServiceName { |
32 | + case SERVICENAME_DEKKO: |
33 | + log.Println("Creating account with id", data.AccountId, "for", data.ServiceName) |
34 | + plugin = dekko.New(data.AccountId) |
35 | case SERVICENAME_GMAIL: |
36 | log.Println("Creating account with id", data.AccountId, "for", data.ServiceName) |
37 | plugin = gmail.New(data.AccountId) |
38 | |
39 | === added directory 'plugins/dekko' |
40 | === added file 'plugins/dekko/api.go' |
41 | --- plugins/dekko/api.go 1970-01-01 00:00:00 +0000 |
42 | +++ plugins/dekko/api.go 2016-07-22 11:44:01 +0000 |
43 | @@ -0,0 +1,127 @@ |
44 | +/* |
45 | + Copyright 2014 Canonical Ltd. |
46 | + |
47 | + This program is free software: you can redistribute it and/or modify it |
48 | + under the terms of the GNU General Public License version 3, as published |
49 | + by the Free Software Foundation. |
50 | + |
51 | + This program is distributed in the hope that it will be useful, but |
52 | + WITHOUT ANY WARRANTY; without even the implied warranties of |
53 | + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
54 | + PURPOSE. See the GNU General Public License for more details. |
55 | + |
56 | + You should have received a copy of the GNU General Public License along |
57 | + with this program. If not, see <http://www.gnu.org/licenses/>. |
58 | +*/ |
59 | + |
60 | +package dekko |
61 | + |
62 | +import ( |
63 | + "fmt" |
64 | + "time" |
65 | + |
66 | + "launchpad.net/account-polld/plugins" |
67 | +) |
68 | + |
69 | +const gmailTime = "Mon, 2 Jan 2006 15:04:05 -0700" |
70 | + |
71 | +type pushes map[string]*plugins.PushMessage |
72 | +type headers map[string]string |
73 | + |
74 | +// messageList holds a response to call to Users.messages: list |
75 | +// defined in https://developers.google.com/gmail/api/v1/reference/users/messages/list |
76 | +type messageList struct { |
77 | + // Messages holds a list of message. |
78 | + Messages []message `json:"messages"` |
79 | + // NextPageToken is used to retrieve the next page of results in the list. |
80 | + NextPageToken string `json:"nextPageToken"` |
81 | + // ResultSizeEstimage is the estimated total number of results. |
82 | + ResultSizeEstimage uint64 `json:"resultSizeEstimate"` |
83 | +} |
84 | + |
85 | +// message holds a partial response for a Users.messages. |
86 | +// The full definition of a message is defined in |
87 | +// https://developers.google.com/gmail/api/v1/reference/users/messages#resource |
88 | +type message struct { |
89 | + // Id is the immutable ID of the message. |
90 | + Id string `json:"id"` |
91 | + // ThreadId is the ID of the thread the message belongs to. |
92 | + ThreadId string `json:"threadId"` |
93 | + // HistoryId is the ID of the last history record that modified |
94 | + // this message. |
95 | + HistoryId string `json:"historyId"` |
96 | + // Snippet is a short part of the message text. This text is |
97 | + // used for the push message summary. |
98 | + Snippet string `json:"snippet"` |
99 | + // Payload represents the message payload. |
100 | + Payload payload `json:"payload"` |
101 | +} |
102 | + |
103 | +func (m message) String() string { |
104 | + return fmt.Sprintf("Id: %d, snippet: '%s'\n", m.Id, m.Snippet[:10]) |
105 | +} |
106 | + |
107 | +// ById implements sort.Interface for []message based on |
108 | +// the Id field. |
109 | +type byId []message |
110 | + |
111 | +func (m byId) Len() int { return len(m) } |
112 | +func (m byId) Swap(i, j int) { m[i], m[j] = m[j], m[i] } |
113 | +func (m byId) Less(i, j int) bool { return m[i].Id < m[j].Id } |
114 | + |
115 | +// payload represents the message payload. |
116 | +type payload struct { |
117 | + Headers []messageHeader `json:"headers"` |
118 | +} |
119 | + |
120 | +func (p *payload) mapHeaders() headers { |
121 | + headers := make(map[string]string) |
122 | + for _, hdr := range p.Headers { |
123 | + headers[hdr.Name] = hdr.Value |
124 | + } |
125 | + return headers |
126 | +} |
127 | + |
128 | +func (hdr headers) getTimestamp() time.Time { |
129 | + timestamp, ok := hdr[hdrDATE] |
130 | + if !ok { |
131 | + return time.Now() |
132 | + } |
133 | + |
134 | + if t, err := time.Parse(gmailTime, timestamp); err == nil { |
135 | + return t |
136 | + } |
137 | + return time.Now() |
138 | +} |
139 | + |
140 | +func (hdr headers) getEpoch() int64 { |
141 | + return hdr.getTimestamp().Unix() |
142 | +} |
143 | + |
144 | +// messageHeader represents the message headers. |
145 | +type messageHeader struct { |
146 | + Name string `json:"name"` |
147 | + Value string `json:"value"` |
148 | +} |
149 | + |
150 | +type errorResp struct { |
151 | + Err struct { |
152 | + Code uint64 `json:"code"` |
153 | + Message string `json:"message"` |
154 | + Errors []struct { |
155 | + Domain string `json:"domain"` |
156 | + Reason string `json:"reason"` |
157 | + Message string `json:"message"` |
158 | + } `json:"errors"` |
159 | + } `json:"error"` |
160 | +} |
161 | + |
162 | +func (err *errorResp) Error() string { |
163 | + return fmt.Sprint("backend response:", err.Err.Message) |
164 | +} |
165 | + |
166 | +const ( |
167 | + hdrDATE = "Date" |
168 | + hdrSUBJECT = "Subject" |
169 | + hdrFROM = "From" |
170 | +) |
171 | |
172 | === added file 'plugins/dekko/dekko.go' |
173 | --- plugins/dekko/dekko.go 1970-01-01 00:00:00 +0000 |
174 | +++ plugins/dekko/dekko.go 2016-07-22 11:44:01 +0000 |
175 | @@ -0,0 +1,346 @@ |
176 | +/* |
177 | + Copyright 2014 Canonical Ltd. |
178 | + |
179 | + This program is free software: you can redistribute it and/or modify it |
180 | + under the terms of the GNU General Public License version 3, as published |
181 | + by the Free Software Foundation. |
182 | + |
183 | + This program is distributed in the hope that it will be useful, but |
184 | + WITHOUT ANY WARRANTY; without even the implied warranties of |
185 | + MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
186 | + PURPOSE. See the GNU General Public License for more details. |
187 | + |
188 | + You should have received a copy of the GNU General Public License along |
189 | + with this program. If not, see <http://www.gnu.org/licenses/>. |
190 | +*/ |
191 | + |
192 | +package dekko |
193 | + |
194 | +import ( |
195 | + "encoding/json" |
196 | + "fmt" |
197 | + "net/http" |
198 | + "net/mail" |
199 | + "net/url" |
200 | + "os" |
201 | + "regexp" |
202 | + "sort" |
203 | + "strings" |
204 | + "time" |
205 | + |
206 | + "log" |
207 | + |
208 | + "launchpad.net/account-polld/accounts" |
209 | + "launchpad.net/account-polld/gettext" |
210 | + "launchpad.net/account-polld/plugins" |
211 | + "launchpad.net/account-polld/qtcontact" |
212 | +) |
213 | + |
214 | +const ( |
215 | + APP_ID = "dekko.dekkoproject_dekko" |
216 | + dekkoDispatchUrl = "dekko://notify/%d/%s/%s" |
217 | + // If there's more than 10 emails in one batch, we don't show 10 notification |
218 | + // bubbles, but instead show one summary. We always show all notifications in the |
219 | + // indicator. |
220 | + individualNotificationsLimit = 10 |
221 | + pluginName = "dekko" |
222 | +) |
223 | + |
224 | +type reportedIdMap map[string]time.Time |
225 | + |
226 | +var baseUrl, _ = url.Parse("https://www.googleapis.com/gmail/v1/users/me/") |
227 | + |
228 | +// timeDelta defines how old messages can be to be reported. |
229 | +var timeDelta = time.Duration(time.Hour * 24) |
230 | + |
231 | +// trackDelta defines how old messages can be before removed from tracking |
232 | +var trackDelta = time.Duration(time.Hour * 24 * 7) |
233 | + |
234 | +// relativeTimeDelta is the same as timeDelta |
235 | +var relativeTimeDelta string = "1d" |
236 | + |
237 | +// regexp for identifying non-ascii characters |
238 | +var nonAsciiChars, _ = regexp.Compile("[^\x00-\x7F]") |
239 | + |
240 | +type GmailPlugin struct { |
241 | + // reportedIds holds the messages that have already been notified. This |
242 | + // approach is taken against timestamps as it avoids needing to call |
243 | + // get on the message. |
244 | + reportedIds reportedIdMap |
245 | + accountId uint |
246 | +} |
247 | + |
248 | +func idsFromPersist(accountId uint) (ids reportedIdMap, err error) { |
249 | + err = plugins.FromPersist(pluginName, accountId, &ids) |
250 | + if err != nil { |
251 | + return nil, err |
252 | + } |
253 | + // discard old ids |
254 | + timestamp := time.Now() |
255 | + for k, v := range ids { |
256 | + delta := timestamp.Sub(v) |
257 | + if delta > trackDelta { |
258 | + log.Print("gmail plugin ", accountId, ": deleting ", k, " as ", delta, " is greater than ", trackDelta) |
259 | + delete(ids, k) |
260 | + } |
261 | + } |
262 | + return ids, nil |
263 | +} |
264 | + |
265 | +func (ids reportedIdMap) persist(accountId uint) (err error) { |
266 | + err = plugins.Persist(pluginName, accountId, ids) |
267 | + if err != nil { |
268 | + log.Print("gmail plugin ", accountId, ": failed to save state: ", err) |
269 | + } |
270 | + return nil |
271 | +} |
272 | + |
273 | +func New(accountId uint) *GmailPlugin { |
274 | + reportedIds, err := idsFromPersist(accountId) |
275 | + if err != nil { |
276 | + log.Print("gmail plugin ", accountId, ": cannot load previous state from storage: ", err) |
277 | + } else { |
278 | + log.Print("gmail plugin ", accountId, ": last state loaded from storage") |
279 | + } |
280 | + return &GmailPlugin{reportedIds: reportedIds, accountId: accountId} |
281 | +} |
282 | + |
283 | +func (p *GmailPlugin) ApplicationId() plugins.ApplicationId { |
284 | + return plugins.ApplicationId(APP_ID) |
285 | +} |
286 | + |
287 | +func (p *GmailPlugin) Poll(authData *accounts.AuthData) ([]*plugins.PushMessageBatch, error) { |
288 | + // This envvar check is to ease testing. |
289 | + if token := os.Getenv("ACCOUNT_POLLD_TOKEN_GMAIL"); token != "" { |
290 | + authData.AccessToken = token |
291 | + } |
292 | + |
293 | + resp, err := p.requestMessageList(authData.AccessToken) |
294 | + if err != nil { |
295 | + return nil, err |
296 | + } |
297 | + messages, err := p.parseMessageListResponse(resp) |
298 | + if err != nil { |
299 | + return nil, err |
300 | + } |
301 | + |
302 | + // TODO use the batching API defined in https://developers.google.com/gmail/api/guides/batch |
303 | + for i := range messages { |
304 | + resp, err := p.requestMessage(messages[i].Id, authData.AccessToken) |
305 | + if err != nil { |
306 | + return nil, err |
307 | + } |
308 | + messages[i], err = p.parseMessageResponse(resp) |
309 | + if err != nil { |
310 | + return nil, err |
311 | + } |
312 | + } |
313 | + notif, err := p.createNotifications(messages) |
314 | + if err != nil { |
315 | + return nil, err |
316 | + } |
317 | + return []*plugins.PushMessageBatch{ |
318 | + &plugins.PushMessageBatch{ |
319 | + Messages: notif, |
320 | + Limit: individualNotificationsLimit, |
321 | + OverflowHandler: p.handleOverflow, |
322 | + Tag: "dekko", |
323 | + }}, nil |
324 | + |
325 | +} |
326 | + |
327 | +func (p *GmailPlugin) reported(id string) bool { |
328 | + _, ok := p.reportedIds[id] |
329 | + return ok |
330 | +} |
331 | + |
332 | +func (p *GmailPlugin) createNotifications(messages []message) ([]*plugins.PushMessage, error) { |
333 | + timestamp := time.Now() |
334 | + pushMsgMap := make(pushes) |
335 | + |
336 | + for _, msg := range messages { |
337 | + hdr := msg.Payload.mapHeaders() |
338 | + |
339 | + from := hdr[hdrFROM] |
340 | + var avatarPath string |
341 | + |
342 | + emailAddress, err := mail.ParseAddress(from) |
343 | + if err != nil { |
344 | + // If the email address contains non-ascii characters, we get an |
345 | + // error so we're going to try again, this time mangling the name |
346 | + // by removing all non-ascii characters. We only care about the email |
347 | + // address here anyway. |
348 | + // XXX: We can't check the error message due to [1]: the error |
349 | + // message is different in go < 1.3 and > 1.5. |
350 | + // [1] https://github.com/golang/go/issues/12492 |
351 | + mangledAddr := nonAsciiChars.ReplaceAllString(from, "") |
352 | + mangledEmail, mangledParseError := mail.ParseAddress(mangledAddr) |
353 | + if mangledParseError == nil { |
354 | + emailAddress = mangledEmail |
355 | + } |
356 | + } else if emailAddress.Name != "" { |
357 | + // We only want the Name if the first ParseAddress |
358 | + // call was successful. I.e. we do not want the name |
359 | + // from a mangled email address. |
360 | + from = emailAddress.Name |
361 | + } |
362 | + |
363 | + if emailAddress != nil { |
364 | + avatarPath = qtcontact.GetAvatar(emailAddress.Address) |
365 | + // If icon path starts with a path separator, assume local file path, |
366 | + // encode it and prepend file scheme defined in RFC 1738. |
367 | + if strings.HasPrefix(avatarPath, string(os.PathSeparator)) { |
368 | + avatarPath = url.QueryEscape(avatarPath) |
369 | + avatarPath = "file://" + avatarPath |
370 | + } |
371 | + } |
372 | + |
373 | + msgStamp := hdr.getTimestamp() |
374 | + |
375 | + if _, ok := pushMsgMap[msg.ThreadId]; ok { |
376 | + // TRANSLATORS: the %s is an appended "from" corresponding to an specific email thread |
377 | + pushMsgMap[msg.ThreadId].Notification.Card.Summary += fmt.Sprintf(gettext.Gettext(", %s"), from) |
378 | + } else if timestamp.Sub(msgStamp) < timeDelta { |
379 | + // TRANSLATORS: the %s is the "from" header corresponding to a specific email |
380 | + summary := fmt.Sprintf(gettext.Gettext("%s"), from) |
381 | + // TRANSLATORS: the first %s refers to the email "subject", the second %s refers "from" |
382 | + body := fmt.Sprintf(gettext.Gettext("%s\n%s"), hdr[hdrSUBJECT], msg.Snippet) |
383 | + // fmt with label personal and threadId |
384 | + action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX", msg.Id) |
385 | + epoch := hdr.getEpoch() |
386 | + pushMsgMap[msg.ThreadId] = plugins.NewStandardPushMessage(summary, body, action, avatarPath, epoch) |
387 | + } else { |
388 | + log.Print("gmail plugin ", p.accountId, ": skipping message id ", msg.Id, " with date ", msgStamp, " older than ", timeDelta) |
389 | + } |
390 | + } |
391 | + pushMsg := make([]*plugins.PushMessage, 0, len(pushMsgMap)) |
392 | + for _, v := range pushMsgMap { |
393 | + pushMsg = append(pushMsg, v) |
394 | + } |
395 | + return pushMsg, nil |
396 | + |
397 | +} |
398 | +func (p *GmailPlugin) handleOverflow(pushMsg []*plugins.PushMessage) *plugins.PushMessage { |
399 | + // TODO it would probably be better to grab the estimate that google returns in the message list. |
400 | + approxUnreadMessages := len(pushMsg) |
401 | + |
402 | + // TRANSLATORS: the %d refers to the number of new email messages. |
403 | + summary := fmt.Sprintf(gettext.Gettext("You have %d new messages"), approxUnreadMessages) |
404 | + |
405 | + body := "" |
406 | + |
407 | + // fmt with label personal and no threadId |
408 | + action := fmt.Sprintf(dekkoDispatchUrl, p.accountId, "INBOX") |
409 | + epoch := time.Now().Unix() |
410 | + |
411 | + return plugins.NewStandardPushMessage(summary, body, action, "", epoch) |
412 | +} |
413 | + |
414 | +func (p *GmailPlugin) parseMessageListResponse(resp *http.Response) ([]message, error) { |
415 | + defer resp.Body.Close() |
416 | + decoder := json.NewDecoder(resp.Body) |
417 | + |
418 | + if resp.StatusCode != http.StatusOK { |
419 | + var errResp errorResp |
420 | + if err := decoder.Decode(&errResp); err != nil { |
421 | + return nil, err |
422 | + } |
423 | + if errResp.Err.Code == 401 { |
424 | + return nil, plugins.ErrTokenExpired |
425 | + } |
426 | + return nil, &errResp |
427 | + } |
428 | + |
429 | + var messages messageList |
430 | + if err := decoder.Decode(&messages); err != nil { |
431 | + return nil, err |
432 | + } |
433 | + |
434 | + filteredMsg := p.messageListFilter(messages.Messages) |
435 | + |
436 | + return filteredMsg, nil |
437 | +} |
438 | + |
439 | +// messageListFilter returns a subset of unread messages where the subset |
440 | +// depends on not being in reportedIds. Before returning, reportedIds is |
441 | +// updated with the new list of unread messages. |
442 | +func (p *GmailPlugin) messageListFilter(messages []message) []message { |
443 | + sort.Sort(byId(messages)) |
444 | + var reportMsg []message |
445 | + var ids = make(reportedIdMap) |
446 | + |
447 | + for _, msg := range messages { |
448 | + if !p.reported(msg.Id) { |
449 | + reportMsg = append(reportMsg, msg) |
450 | + } |
451 | + ids[msg.Id] = time.Now() |
452 | + } |
453 | + p.reportedIds = ids |
454 | + p.reportedIds.persist(p.accountId) |
455 | + return reportMsg |
456 | +} |
457 | + |
458 | +func (p *GmailPlugin) parseMessageResponse(resp *http.Response) (message, error) { |
459 | + defer resp.Body.Close() |
460 | + decoder := json.NewDecoder(resp.Body) |
461 | + |
462 | + if resp.StatusCode != http.StatusOK { |
463 | + var errResp errorResp |
464 | + if err := decoder.Decode(&errResp); err != nil { |
465 | + return message{}, err |
466 | + } |
467 | + return message{}, &errResp |
468 | + } |
469 | + |
470 | + var msg message |
471 | + if err := decoder.Decode(&msg); err != nil { |
472 | + return message{}, err |
473 | + } |
474 | + |
475 | + return msg, nil |
476 | +} |
477 | + |
478 | +func (p *GmailPlugin) requestMessage(id, accessToken string) (*http.Response, error) { |
479 | + u, err := baseUrl.Parse("messages/" + id) |
480 | + if err != nil { |
481 | + return nil, err |
482 | + } |
483 | + |
484 | + query := u.Query() |
485 | + // only request specific fields |
486 | + query.Add("fields", "snippet,threadId,id,payload/headers") |
487 | + // get the full message to get From and Subject from headers |
488 | + query.Add("format", "full") |
489 | + u.RawQuery = query.Encode() |
490 | + |
491 | + req, err := http.NewRequest("GET", u.String(), nil) |
492 | + if err != nil { |
493 | + return nil, err |
494 | + } |
495 | + req.Header.Set("Authorization", "Bearer "+accessToken) |
496 | + |
497 | + return http.DefaultClient.Do(req) |
498 | +} |
499 | + |
500 | +func (p *GmailPlugin) requestMessageList(accessToken string) (*http.Response, error) { |
501 | + u, err := baseUrl.Parse("messages") |
502 | + if err != nil { |
503 | + return nil, err |
504 | + } |
505 | + |
506 | + query := u.Query() |
507 | + |
508 | + // get all unread inbox emails received after |
509 | + // the last time we checked. If this is the first |
510 | + // time we check, get unread emails after timeDelta |
511 | + query.Add("q", fmt.Sprintf("is:unread in:inbox newer_than:%s", relativeTimeDelta)) |
512 | + u.RawQuery = query.Encode() |
513 | + |
514 | + req, err := http.NewRequest("GET", u.String(), nil) |
515 | + if err != nil { |
516 | + return nil, err |
517 | + } |
518 | + req.Header.Set("Authorization", "Bearer "+accessToken) |
519 | + |
520 | + return http.DefaultClient.Do(req) |
521 | +} |
PASSED: Continuous integration, rev:170 /jenkins. canonical. com/system- apps/job/ lp-account- polld-ci/ 15/ /jenkins. canonical. com/system- apps/job/ build/1032 /jenkins. canonical. com/system- apps/job/ test-0- autopkgtest/ label=phone- armhf,release= vivid+overlay, testname= default/ 200 /jenkins. canonical. com/system- apps/job/ build-0- fetch/1032 /jenkins. canonical. com/system- apps/job/ build-1- sourcepkg/ release= vivid+overlay/ 929 /jenkins. canonical. com/system- apps/job/ build-1- sourcepkg/ release= xenial+ overlay/ 929 /jenkins. canonical. com/system- apps/job/ build-1- sourcepkg/ release= yakkety/ 929 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= vivid+overlay/ 923 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= vivid+overlay/ 923/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= xenial+ overlay/ 923 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= xenial+ overlay/ 923/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= yakkety/ 923 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= yakkety/ 923/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= vivid+overlay/ 923 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= vivid+overlay/ 923/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= xenial+ overlay/ 923 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= xenial+ overlay/ 923/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= yakkety/ 923 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= yakkety/ 923/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= vivid+overlay/ 923 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= vivid+overlay/ 923/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= xenial+ overlay/ 923 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= xenial+ overlay/ 923/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= yakkety/ 923 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= yakkety/ 923/artifact/ output/ *zip*/output. zip
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: /jenkins. canonical. com/system- apps/job/ lp-account- polld-ci/ 15/rebuild
https:/