launchpad_api.go

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