package tasksboard import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) type Client struct { BaseURL string Token string HTTPClient *http.Client } func (c *Client) http() *http.Client { if c.HTTPClient != nil { return c.HTTPClient } return http.DefaultClient } type Task struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Status string `json:"status"` Priority int `json:"priority"` Project *struct { ID string `json:"id"` Slug string `json:"slug"` Name string `json:"name"` } `json:"project,omitempty"` Assignee *struct { Type string `json:"type"` ID string `json:"id"` DisplayName string `json:"display_name,omitempty"` } `json:"assignee,omitempty"` } type Comment struct { ID string `json:"id"` TaskID string `json:"task_id"` AuthorType string `json:"author_type"` AuthorID string `json:"author_id"` Body string `json:"body"` CreatedAt time.Time `json:"created_at"` } func (c *Client) GetTask(ctx context.Context, taskID string) (Task, error) { var t Task if err := c.doJSON(ctx, http.MethodGet, "/api/v1/tasks/"+url.PathEscape(taskID), "", nil, &t); err != nil { return Task{}, err } return t, nil } func (c *Client) ListComments(ctx context.Context, taskID string) ([]Comment, error) { var out struct { Comments []Comment `json:"comments"` } if err := c.doJSON(ctx, http.MethodGet, "/api/v1/tasks/"+url.PathEscape(taskID)+"/comments", "", nil, &out); err != nil { return nil, err } return out.Comments, nil } func (c *Client) PostComment(ctx context.Context, taskID, body, idempotencyKey string) (Comment, error) { var out Comment payload := map[string]string{"body": body} if err := c.doJSON(ctx, http.MethodPost, "/api/v1/tasks/"+url.PathEscape(taskID)+"/comments", idempotencyKey, payload, &out); err != nil { return Comment{}, err } return out, nil } func (c *Client) doJSON(ctx context.Context, method, path, idempotencyKey string, body any, dst any) error { base := strings.TrimRight(c.BaseURL, "/") var rdr io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return err } rdr = bytes.NewReader(b) } req, err := http.NewRequestWithContext(ctx, method, base+path, rdr) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.Token) if body != nil { req.Header.Set("Content-Type", "application/json") } if idempotencyKey != "" { req.Header.Set("Idempotency-Key", idempotencyKey) } resp, err := c.http().Do(req) if err != nil { return err } defer resp.Body.Close() data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return err } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("tasks %s %s: status %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(data))) } if dst == nil || len(data) == 0 { return nil } return json.Unmarshal(data, dst) }