1package launchpad
2
3/*
4 * A wrapper around the Launchpad API. The documentation can be found at:
5 * https://launchpad.net/+apidoc/devel.html
6 *
7 * TODO:
8 * - Retrieve all messages associated to bugs
9 * - Retrieve bug status
10 * - Retrieve activity log
11 * - SearchTasks should yield bugs one by one
12 *
13 * TODO (maybe):
14 * - Authentication (this might help retrieving email adresses)
15 */
16
17import (
18 "encoding/json"
19 "fmt"
20 "net/http"
21 "net/url"
22)
23
24const apiRoot = "https://api.launchpad.net/devel"
25
26// Person describes a person on Launchpad (a bug owner, a message author, ...).
27type LPPerson struct {
28 Name string `json:"display_name"`
29 Login string `json:"name"`
30}
31
32// Caching all the LPPerson we know.
33// The keys are links to an owner page, such as
34// https://api.launchpad.net/devel/~login
35var personCache = make(map[string]LPPerson)
36
37func (owner *LPPerson) UnmarshalJSON(data []byte) error {
38 type LPPersonX LPPerson // Avoid infinite recursion
39 var ownerLink string
40 if err := json.Unmarshal(data, &ownerLink); err != nil {
41 return err
42 }
43
44 // First, try to gather info about the bug owner using our cache.
45 if cachedPerson, hasKey := personCache[ownerLink]; hasKey {
46 *owner = cachedPerson
47 return nil
48 }
49
50 // If the bug owner is not already known, we have to send a request.
51 req, err := http.NewRequest("GET", ownerLink, nil)
52 if err != nil {
53 return nil
54 }
55
56 client := &http.Client{}
57 resp, err := client.Do(req)
58 if err != nil {
59 return nil
60 }
61
62 defer resp.Body.Close()
63
64 var p LPPersonX
65 if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
66 return nil
67 }
68 *owner = LPPerson(p)
69 // Do not forget to update the cache.
70 personCache[ownerLink] = *owner
71 return nil
72}
73
74// LPBug describes a Launchpad bug.
75type LPBug struct {
76 Title string `json:"title"`
77 ID int `json:"id"`
78 Owner LPPerson `json:"owner_link"`
79 Description string `json:"description"`
80 CreatedAt string `json:"date_created"`
81}
82
83type launchpadBugEntry struct {
84 BugLink string `json:"bug_link"`
85 SelfLink string `json:"self_link"`
86}
87
88type launchpadAnswer struct {
89 Entries []launchpadBugEntry `json:"entries"`
90 Start int `json:"start"`
91 NextLink string `json:"next_collection_link"`
92}
93
94type launchpadAPI struct {
95 client *http.Client
96}
97
98func (lapi *launchpadAPI) Init() error {
99 lapi.client = &http.Client{}
100 return nil
101}
102
103func (lapi *launchpadAPI) SearchTasks(project string) ([]LPBug, error) {
104 var bugs []LPBug
105
106 // First, let us build the URL. Not all statuses are included by
107 // default, so we have to explicitely enumerate them.
108 validStatuses := [13]string{
109 "New", "Incomplete", "Opinion", "Invalid",
110 "Won't Fix", "Expired", "Confirmed", "Triaged",
111 "In Progress", "Fix Committed", "Fix Released",
112 "Incomplete (with response)", "Incomplete (without response)",
113 }
114 queryParams := url.Values{}
115 queryParams.Add("ws.op", "searchTasks")
116 for _, validStatus := range validStatuses {
117 queryParams.Add("status", validStatus)
118 }
119 lpURL := fmt.Sprintf("%s/%s?%s", apiRoot, project, queryParams.Encode())
120
121 for {
122 req, err := http.NewRequest("GET", lpURL, nil)
123 if err != nil {
124 return nil, err
125 }
126
127 resp, err := lapi.client.Do(req)
128 if err != nil {
129 return nil, err
130 }
131
132 var result launchpadAnswer
133
134 err = json.NewDecoder(resp.Body).Decode(&result)
135 _ = resp.Body.Close()
136
137 if err != nil {
138 return nil, err
139 }
140
141 for _, bugEntry := range result.Entries {
142 bug, err := lapi.queryBug(bugEntry.BugLink)
143 if err == nil {
144 bugs = append(bugs, bug)
145 }
146 }
147
148 // Launchpad only returns 75 results at a time. We get the next
149 // page and run another query, unless there is no other page.
150 lpURL = result.NextLink
151 if lpURL == "" {
152 break
153 }
154 }
155
156 return bugs, nil
157}
158
159func (lapi *launchpadAPI) queryBug(url string) (LPBug, error) {
160 var bug LPBug
161
162 req, err := http.NewRequest("GET", url, nil)
163 if err != nil {
164 return bug, err
165 }
166
167 resp, err := lapi.client.Do(req)
168 if err != nil {
169 return bug, err
170 }
171
172 defer resp.Body.Close()
173
174 if err := json.NewDecoder(resp.Body).Decode(&bug); err != nil {
175 return bug, err
176 }
177
178 return bug, nil
179}