aboutsummaryrefslogblamecommitdiffstats
path: root/bridge/launchpad/launchpad_api.go
blob: 09e02bc5ee0f755cea36f1b94893990ae7c5fe15 (plain) (tree)

















































































































































































                                                                                  
package launchpad

/*
 * A wrapper around the Launchpad API. The documentation can be found at:
 * https://launchpad.net/+apidoc/devel.html
 *
 * TODO:
 * - Retrieve all messages associated to bugs
 * - Retrieve bug status
 * - Retrieve activity log
 * - SearchTasks should yield bugs one by one
 *
 * TODO (maybe):
 * - Authentication (this might help retrieving email adresses)
 */

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
)

const apiRoot = "https://api.launchpad.net/devel"

// Person describes a person on Launchpad (a bug owner, a message author, ...).
type LPPerson struct {
	Name  string `json:"display_name"`
	Login string `json:"name"`
}

// Caching all the LPPerson we know.
// The keys are links to an owner page, such as
// https://api.launchpad.net/devel/~login
var personCache = make(map[string]LPPerson)

func (owner *LPPerson) UnmarshalJSON(data []byte) error {
	type LPPersonX LPPerson // Avoid infinite recursion
	var ownerLink string
	if err := json.Unmarshal(data, &ownerLink); err != nil {
		return err
	}

	// First, try to gather info about the bug owner using our cache.
	if cachedPerson, hasKey := personCache[ownerLink]; hasKey {
		*owner = cachedPerson
		return nil
	}

	// If the bug owner is not already known, we have to send a request.
	req, err := http.NewRequest("GET", ownerLink, nil)
	if err != nil {
		return nil
	}

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil
	}

	defer resp.Body.Close()

	var p LPPersonX
	if err := json.NewDecoder(resp.Body).Decode(&p); err != nil {
		return nil
	}
	*owner = LPPerson(p)
	// Do not forget to update the cache.
	personCache[ownerLink] = *owner
	return nil
}

// LPBug describes a Launchpad bug.
type LPBug struct {
	Title       string   `json:"title"`
	ID          int      `json:"id"`
	Owner       LPPerson `json:"owner_link"`
	Description string   `json:"description"`
	CreatedAt   string   `json:"date_created"`
}

type launchpadBugEntry struct {
	BugLink  string `json:"bug_link"`
	SelfLink string `json:"self_link"`
}

type launchpadAnswer struct {
	Entries  []launchpadBugEntry `json:"entries"`
	Start    int                 `json:"start"`
	NextLink string              `json:"next_collection_link"`
}

type launchpadAPI struct {
	client *http.Client
}

func (lapi *launchpadAPI) Init() error {
	lapi.client = &http.Client{}
	return nil
}

func (lapi *launchpadAPI) SearchTasks(project string) ([]LPBug, error) {
	var bugs []LPBug

	// First, let us build the URL. Not all statuses are included by
	// default, so we have to explicitely enumerate them.
	validStatuses := [13]string{
		"New", "Incomplete", "Opinion", "Invalid",
		"Won't Fix", "Expired", "Confirmed", "Triaged",
		"In Progress", "Fix Committed", "Fix Released",
		"Incomplete (with response)", "Incomplete (without response)",
	}
	queryParams := url.Values{}
	queryParams.Add("ws.op", "searchTasks")
	for _, validStatus := range validStatuses {
		queryParams.Add("status", validStatus)
	}
	lpURL := fmt.Sprintf("%s/%s?%s", apiRoot, project, queryParams.Encode())

	for {
		req, err := http.NewRequest("GET", lpURL, nil)
		if err != nil {
			return nil, err
		}

		resp, err := lapi.client.Do(req)
		if err != nil {
			return nil, err
		}

		defer resp.Body.Close()

		var result launchpadAnswer

		if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
			return nil, err
		}

		for _, bugEntry := range result.Entries {
			bug, err := lapi.queryBug(bugEntry.BugLink)
			if err == nil {
				bugs = append(bugs, bug)
			}
		}

		// Launchpad only returns 75 results at a time. We get the next
		// page and run another query, unless there is no other page.
		lpURL = result.NextLink
		if lpURL == "" {
			break
		}
	}

	return bugs, nil
}

func (lapi *launchpadAPI) queryBug(url string) (LPBug, error) {
	var bug LPBug

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return bug, err
	}

	resp, err := lapi.client.Do(req)
	if err != nil {
		return bug, err
	}

	defer resp.Body.Close()

	if err := json.NewDecoder(resp.Body).Decode(&bug); err != nil {
		return bug, err
	}

	return bug, nil
}