aboutsummaryrefslogblamecommitdiffstats
path: root/bridge/launchpad/launchpad_api.go
blob: 763e774e6b96bee84ef442053331713d4710e249 (plain) (tree)
1
2
3
4
5
6
7






                                                                         








                                                               
                 


















                                                                               






                                                  








                                                












                                                                  




                                                          




                                        


                                        


                  
                                                                                             











                                                                              
                                                         















                                                                                

                                          



                                                                



                                                         
                                                                        















                                                                               
                                                                                    





                                                    
                                  











                                                                       

                                                                                     
                                                                        




                               

                       
 
                                                                                                       






                                                                    
                                          

























                                                                               
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 adresses)
 */

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