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 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}