Skip to content
This repository has been archived by the owner on Oct 2, 2020. It is now read-only.

Add Report state change, assignee update, and comment functionality #18

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 43 additions & 21 deletions h1/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,42 +29,64 @@ import (
// HackerOne API docs: https://api.hackerone.com/docs/v1#activity
type Activity struct {
report *Report
ID *string `json:"id"`
Type *string `json:"type"`
Message *string `json:"message"`
Internal *bool `json:"internal"`
CreatedAt *Timestamp `json:"created_at"`
UpdatedAt *Timestamp `json:"updated_at"`
RawActor json.RawMessage `json:"actor"` // Used by the Actor() method
ID *string `json:"-"`
Type *string `json:"-"`
Message *string `json:"message,omitempty"`
Internal *bool `json:"internal,omitempty"`
CreatedAt *Timestamp `json:"created_at,omitempty"`
UpdatedAt *Timestamp `json:"updated_at,omitempty"`
RawActor json.RawMessage `json:"actor,omitempty"` // Used by the Actor() method
Attachments []Attachment `json:"attachments,omitempty"`
rawData []byte // Used by the Activity() method
}

// Helper types for JSONUnmarshal
type activity Activity // Used to avoid recursion of JSONUnmarshal
type activityUnmarshalHelper struct {
activity
type activityJSONHelper struct {
ID *string `json:"id,omitempty"`
Type *string `json:"type,omitempty"`
Attributes *activity `json:"attributes"`
Relationships struct {
Attachments struct {
Data []Attachment `json:"data"`
Relationships *struct {
Attachments *struct {
Data []Attachment `json:"data,omitempty"`
} `json:"attachments,omitempty"`
RawActor struct {
Data json.RawMessage `json:"data"`
} `json:"actor"`
} `json:"relationships"`
RawActor *struct {
Data json.RawMessage `json:"data,omitempty"`
} `json:"actor,omitempty"`
} `json:"relationships,omitempty"`
}

// MarshalJSON allows JSONAPI attributes and relationships to unmarshal cleanly.
func (a *Activity) MarshalJSON() ([]byte, error) {
act := activity(*a)
helper := activityJSONHelper{
ID: a.ID,
Type: a.Type,
Attributes: &act,
}
// TODO: Build relationships if needed
//helper.Relationships.Attachments.Data = act.Attachments
act.Attachments = nil
return json.Marshal(&helper)
}

// UnmarshalJSON allows JSONAPI attributes and relationships to unmarshal cleanly.
func (a *Activity) UnmarshalJSON(b []byte) error {
var helper activityUnmarshalHelper
helper.Attributes = &helper.activity
var helper activityJSONHelper
if err := json.Unmarshal(b, &helper); err != nil {
return err
}
*a = Activity(helper.activity)
a.Attachments = helper.Relationships.Attachments.Data
a.RawActor = helper.Relationships.RawActor.Data
*a = Activity(*helper.Attributes)
a.ID = helper.ID
a.Type = helper.Type
if helper.Relationships != nil {
if helper.Relationships.Attachments != nil {
a.Attachments = helper.Relationships.Attachments.Data
}
if helper.Relationships.RawActor != nil {
a.RawActor = helper.Relationships.RawActor.Data
}
}
a.rawData = b
return nil
}
Expand Down
25 changes: 24 additions & 1 deletion h1/h1.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ package h1
import (
"github.com/google/go-querystring/query"

"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
Expand Down Expand Up @@ -104,18 +106,39 @@ func NewClient(httpClient *http.Client) *Client {
return c
}

// dataRequest is used to cast requests to H1
type dataRequest struct {
Data interface{} `json:"data,omitempty"`
}

// NewRequest creates an API request. A relative URL can be provided in urlStr
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
}

req, err := http.NewRequest(method, c.BaseURL.ResolveReference(rel).String(), nil)
var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
dat := dataRequest{
Data: body,
}
err := json.NewEncoder(buf).Encode(&dat)
if err != nil {
return nil, err
}
}

req, err := http.NewRequest(method, c.BaseURL.ResolveReference(rel).String(), buf)
if err != nil {
return nil, err
}

if body != nil {
req.Header.Add("Content-Type", "application/json")
}

req.Header.Add("User-Agent", c.UserAgent)

return req, nil
Expand Down
9 changes: 9 additions & 0 deletions h1/h1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"bytes"
"io"
"io/ioutil"
"math"
"net/http"
"net/http/httptest"
"net/url"
Expand Down Expand Up @@ -144,6 +145,14 @@ func Test_NewRequest(t *testing.T) {
_, err := client.NewRequest("GET", "http://[fe80::1%en0]/", nil)
assert.NotNil(t, err)

// Check that an invalid body fails
_, err = client.NewRequest("GET", "/", struct {
InvalidField float64 `json:"invalid_field"`
}{
math.NaN(),
})
assert.NotNil(t, err)

// Check that an invalid base URL fails
badclient := NewClient(nil)
badclient.BaseURL = &url.URL{
Expand Down
98 changes: 98 additions & 0 deletions h1/report_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,104 @@ func (s *ReportService) Get(ID string) (*Report, *Response, error) {
return rResp, resp, err
}

// CreateComment creates a Comment on a report by ID
func (s *ReportService) CreateComment(ID string, message string, internal bool) (*Activity, *Response, error) {
comment := &Activity{
Type: String(ActivityCommentType),
Internal: &internal,
Message: &message,
}

req, err := s.client.NewRequest("POST", fmt.Sprintf("reports/%s/activities", ID), comment)
if err != nil {
return nil, nil, err
}

rResp := new(Activity)
resp, err := s.client.Do(req, rResp)
if err != nil {
return nil, resp, err
}

return rResp, resp, err
}

// reportUpdateAssigneeRequest is used for making report assignee updates
type reportUpdateAssigneeRequestAttributes struct {
Message string `json:"message"`
}
type reportUpdateAssigneeRequest struct {
ID *string `json:"id,omitempty"`
Type string `json:"type"`
Attributes reportUpdateAssigneeRequestAttributes `json:"attributes"`
}

// UpdateAssignee creates a Comment on a report by ID
func (s *ReportService) UpdateAssignee(ID string, message string, assignee interface{}) (*Report, *Response, error) {
request := &reportUpdateAssigneeRequest{
Attributes: reportUpdateAssigneeRequestAttributes{
Message: message,
},
}
switch assignee.(type) {
case *User:
request.ID = assignee.(*User).ID
request.Type = "user"
case *Group:
request.ID = assignee.(*Group).ID
request.Type = "group"
default:
request.Type = "nobody"
}

req, err := s.client.NewRequest("PUT", fmt.Sprintf("reports/%s/assignee", ID), request)
if err != nil {
return nil, nil, err
}

rResp := new(Report)
resp, err := s.client.Do(req, rResp)
if err != nil {
return nil, resp, err
}

return rResp, resp, err
}

// reportUpdateAssigneeRequest is used for making report assignee updates
type reportStateChangeRequestAttributes struct {
Message string `json:"message,omitempty"`
State string `json:"state"`
}
type reportStateChangeRequest struct {
Type string `json:"type"`
Attributes reportStateChangeRequestAttributes `json:"attributes"`
}

// UpdateState changes a report's state by ID
func (s *ReportService) UpdateState(ID string, state string, message string) (*Report, *Response, error) {
request := &reportStateChangeRequest{
Type: "state-change",
Attributes: reportStateChangeRequestAttributes{
Message: message,
State: state,
},
}

req, err := s.client.NewRequest("POST", fmt.Sprintf("reports/%s/state_changes", ID), request)
if err != nil {
return nil, nil, err
}

rResp := new(Report)
resp, err := s.client.Do(req, rResp)
if err != nil {
return nil, resp, err
}

return rResp, resp, err
}

// ReportListFilter specifies optional parameters to the ReportService.List method.
//
// HackerOne API docs: https://api.hackerone.com/docs/v1#reports/query
Expand Down
Loading