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		defer resp.Body.Close()
133
134		var result launchpadAnswer
135
136		if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
137			return nil, err
138		}
139
140		for _, bugEntry := range result.Entries {
141			bug, err := lapi.queryBug(bugEntry.BugLink)
142			if err == nil {
143				bugs = append(bugs, bug)
144			}
145		}
146
147		// Launchpad only returns 75 results at a time. We get the next
148		// page and run another query, unless there is no other page.
149		lpURL = result.NextLink
150		if lpURL == "" {
151			break
152		}
153	}
154
155	return bugs, nil
156}
157
158func (lapi *launchpadAPI) queryBug(url string) (LPBug, error) {
159	var bug LPBug
160
161	req, err := http.NewRequest("GET", url, nil)
162	if err != nil {
163		return bug, err
164	}
165
166	resp, err := lapi.client.Do(req)
167	if err != nil {
168		return bug, err
169	}
170
171	defer resp.Body.Close()
172
173	if err := json.NewDecoder(resp.Body).Decode(&bug); err != nil {
174		return bug, err
175	}
176
177	return bug, nil
178}