package launchpad
/*
* A wrapper around the Launchpad API. The documentation can be found at:
* https://launchpad.net/+apidoc/devel.html
*
* TODO:
* - Retrieve bug status
* - Retrieve activity log
* - SearchTasks should yield bugs one by one
*
* TODO (maybe):
* - Authentication (this might help retrieving email addresses)
*/
import (
"context"
"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)
// 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"`
Messages []LPMessage
}
// LPMessage describes a comment on a bug report
type LPMessage struct {
Content string `json:"content"`
CreatedAt string `json:"date_created"`
Owner LPPerson `json:"owner_link"`
ID string `json:"self_link"`
}
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 launchpadMessageAnswer struct {
Entries []LPMessage `json:"entries"`
NextLink string `json:"next_collection_link"`
}
type launchpadAPI struct {
client *http.Client
}
func (lapi *launchpadAPI) Init() error {
lapi.client = &http.Client{
Timeout: defaultTimeout,
}
return nil
}
func (lapi *launchpadAPI) SearchTasks(ctx context.Context, project string) ([]LPBug, error) {
var bugs []LPBug
// First, let us build the URL. Not all statuses are included by
// default, so we have to explicitly 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")
queryParams.Add("order_by", "-date_last_updated")
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
}
var result launchpadAnswer
err = json.NewDecoder(resp.Body).Decode(&result)
_ = resp.Body.Close()
if err != nil {
return nil, err
}
for _, bugEntry := range result.Entries {
bug, err := lapi.queryBug(ctx, 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(ctx context.Context, url string) (LPBug, error) {
var bug LPBug
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return bug, err
}
req = req.WithContext(ctx)
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
}
/* Fetch messages */
messagesCollectionLink := fmt.Sprintf("%s/bugs/%d/messages", apiRoot, bug.ID)
messages, err := lapi.queryMessages(ctx, messagesCollectionLink)
if err != nil {
return bug, err
}
bug.Messages = messages
return bug, nil
}
func (lapi *launchpadAPI) queryMessages(ctx context.Context, messagesURL string) ([]LPMessage, error) {
var messages []LPMessage
for {
req, err := http.NewRequest("GET", messagesURL, nil)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := lapi.client.Do(req)
if err != nil {
return nil, err
}
var result launchpadMessageAnswer
err = json.NewDecoder(resp.Body).Decode(&result)
_ = resp.Body.Close()
if err != nil {
return nil, err
}
messages = append(messages, result.Entries...)
// Launchpad only returns 75 results at a time. We get the next
// page and run another query, unless there is no other page.
messagesURL = result.NextLink
if messagesURL == "" {
break
}
}
return messages, nil
}