summaryrefslogtreecommitdiff
path: root/internal/api/trello.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/trello.go')
-rw-r--r--internal/api/trello.go211
1 files changed, 44 insertions, 167 deletions
diff --git a/internal/api/trello.go b/internal/api/trello.go
index 4c4dc95..4cf7e9e 100644
--- a/internal/api/trello.go
+++ b/internal/api/trello.go
@@ -2,44 +2,41 @@ package api
import (
"context"
- "encoding/json"
"fmt"
- "io"
"log"
- "net/http"
"net/url"
"sort"
- "strings"
"sync"
"time"
"task-dashboard/internal/models"
)
-const (
- trelloBaseURL = "https://api.trello.com/1"
-)
+const trelloBaseURL = "https://api.trello.com/1"
// TrelloClient handles interactions with the Trello API
type TrelloClient struct {
- apiKey string
- token string
- baseURL string
- httpClient *http.Client
+ BaseClient
+ apiKey string
+ token string
}
// NewTrelloClient creates a new Trello API client
func NewTrelloClient(apiKey, token string) *TrelloClient {
return &TrelloClient{
- apiKey: apiKey,
- token: token,
- baseURL: trelloBaseURL,
- httpClient: &http.Client{
- Timeout: 15 * time.Second,
- },
+ BaseClient: NewBaseClient(trelloBaseURL),
+ apiKey: apiKey,
+ token: token,
}
}
+func (c *TrelloClient) authParams() url.Values {
+ params := url.Values{}
+ params.Set("key", c.apiKey)
+ params.Set("token", c.token)
+ return params
+}
+
// trelloBoardResponse represents a board from the API
type trelloBoardResponse struct {
ID string `json:"id"`
@@ -65,43 +62,22 @@ type trelloListResponse struct {
// GetBoards fetches all boards for the authenticated user
func (c *TrelloClient) GetBoards(ctx context.Context) ([]models.Board, error) {
- params := url.Values{}
- params.Set("key", c.apiKey)
- params.Set("token", c.token)
+ params := c.authParams()
params.Set("filter", "open")
- params.Set("fields", "id,name") // Only fetch required fields
-
- reqURL := fmt.Sprintf("%s/members/me/boards?%s", c.baseURL, params.Encode())
- req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %w", err)
- }
-
- resp, err := c.httpClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to fetch boards: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body))
- }
+ params.Set("fields", "id,name")
var apiBoards []trelloBoardResponse
- if err := json.NewDecoder(resp.Body).Decode(&apiBoards); err != nil {
- return nil, fmt.Errorf("failed to decode response: %w", err)
+ if err := c.Get(ctx, "/members/me/boards?"+params.Encode(), nil, &apiBoards); err != nil {
+ return nil, fmt.Errorf("failed to fetch boards: %w", err)
}
- // Convert to our model
boards := make([]models.Board, 0, len(apiBoards))
for _, apiBoard := range apiBoards {
- board := models.Board{
+ boards = append(boards, models.Board{
ID: apiBoard.ID,
Name: apiBoard.Name,
- Cards: []models.Card{}, // Will be populated by GetCards
- }
- boards = append(boards, board)
+ Cards: []models.Card{},
+ })
}
return boards, nil
@@ -109,32 +85,14 @@ func (c *TrelloClient) GetBoards(ctx context.Context) ([]models.Board, error) {
// GetCards fetches all cards for a specific board
func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.Card, error) {
- params := url.Values{}
- params.Set("key", c.apiKey)
- params.Set("token", c.token)
+ params := c.authParams()
params.Set("filter", "visible")
- params.Set("fields", "id,name,idList,due,url,idBoard") // Only fetch required fields
-
- reqURL := fmt.Sprintf("%s/boards/%s/cards?%s", c.baseURL, boardID, params.Encode())
- req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %w", err)
- }
-
- resp, err := c.httpClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to fetch cards: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body))
- }
+ params.Set("fields", "id,name,idList,due,url,idBoard")
var apiCards []trelloCardResponse
- if err := json.NewDecoder(resp.Body).Decode(&apiCards); err != nil {
- return nil, fmt.Errorf("failed to decode response: %w", err)
+ path := fmt.Sprintf("/boards/%s/cards?%s", boardID, params.Encode())
+ if err := c.Get(ctx, path, nil, &apiCards); err != nil {
+ return nil, fmt.Errorf("failed to fetch cards: %w", err)
}
log.Printf("Trello GetCards: board %s returned %d cards from API", boardID, len(apiCards))
@@ -143,15 +101,13 @@ func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.C
lists, err := c.getLists(ctx, boardID)
listMap := make(map[string]string)
if err != nil {
- log.Printf("Warning: failed to fetch lists for board %s, cards will have empty list names: %v", boardID, err)
+ log.Printf("Warning: failed to fetch lists for board %s: %v", boardID, err)
} else {
- // Build map of list ID to name
for _, list := range lists {
listMap[list.ID] = list.Name
}
}
- // Convert to our model
cards := make([]models.Card, 0, len(apiCards))
for _, apiCard := range apiCards {
card := models.Card{
@@ -162,10 +118,8 @@ func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.C
URL: apiCard.URL,
}
- // Parse due date if present
if apiCard.Due != nil && *apiCard.Due != "" {
- dueDate, err := time.Parse(time.RFC3339, *apiCard.Due)
- if err == nil {
+ if dueDate, err := time.Parse(time.RFC3339, *apiCard.Due); err == nil {
card.DueDate = &dueDate
}
}
@@ -178,34 +132,15 @@ func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.C
// getLists fetches lists for a board
func (c *TrelloClient) getLists(ctx context.Context, boardID string) ([]models.List, error) {
- params := url.Values{}
- params.Set("key", c.apiKey)
- params.Set("token", c.token)
- params.Set("fields", "id,name") // Only fetch required fields
-
- reqURL := fmt.Sprintf("%s/boards/%s/lists?%s", c.baseURL, boardID, params.Encode())
- req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %w", err)
- }
-
- resp, err := c.httpClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to fetch lists: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body))
- }
+ params := c.authParams()
+ params.Set("fields", "id,name")
var apiLists []trelloListResponse
- if err := json.NewDecoder(resp.Body).Decode(&apiLists); err != nil {
- return nil, fmt.Errorf("failed to decode response: %w", err)
+ path := fmt.Sprintf("/boards/%s/lists?%s", boardID, params.Encode())
+ if err := c.Get(ctx, path, nil, &apiLists); err != nil {
+ return nil, fmt.Errorf("failed to fetch lists: %w", err)
}
- // Convert to model
lists := make([]models.List, 0, len(apiLists))
for _, apiList := range apiLists {
lists = append(lists, models.List{
@@ -222,7 +157,7 @@ func (c *TrelloClient) GetLists(ctx context.Context, boardID string) ([]models.L
return c.getLists(ctx, boardID)
}
-// GetBoardsWithCards fetches all boards and their cards in one call
+// GetBoardsWithCards fetches all boards and their cards concurrently
func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board, error) {
boards, err := c.GetBoards(ctx)
if err != nil {
@@ -237,24 +172,19 @@ func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board,
go func(i int) {
defer wg.Done()
- // Acquire semaphore
sem <- struct{}{}
defer func() { <-sem }()
- // Fetch cards
cards, err := c.GetCards(ctx, boards[i].ID)
if err != nil {
log.Printf("Error fetching cards for board %s (%s): %v", boards[i].Name, boards[i].ID, err)
} else {
- // Set BoardName for each card
for j := range cards {
cards[j].BoardName = boards[i].Name
}
- // It is safe to write to specific indices of the slice concurrently
boards[i].Cards = cards
}
- // Fetch lists
lists, err := c.getLists(ctx, boards[i].ID)
if err != nil {
log.Printf("Error fetching lists for board %s: %v", boards[i].Name, err)
@@ -266,18 +196,15 @@ func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board,
wg.Wait()
- // Sort boards: Non-empty boards first, newest card activity, then alphabetical by name
- // Trello card IDs are chronologically sortable (newer IDs > older IDs)
+ // Sort boards: Non-empty boards first, newest card activity, then alphabetical
sort.Slice(boards, func(i, j int) bool {
hasCardsI := len(boards[i].Cards) > 0
hasCardsJ := len(boards[j].Cards) > 0
- // 1. Prioritize boards with cards
if hasCardsI != hasCardsJ {
- return hasCardsI // true (non-empty) comes before false
+ return hasCardsI
}
- // 2. If both have cards, compare by newest card (max ID)
if hasCardsI && hasCardsJ {
maxIDI := ""
for _, card := range boards[i].Cards {
@@ -294,11 +221,10 @@ func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board,
}
if maxIDI != maxIDJ {
- return maxIDI > maxIDJ // Newer (larger) ID comes first
+ return maxIDI > maxIDJ
}
}
- // 3. Fallback to alphabetical by name
return boards[i].Name < boards[j].Name
})
@@ -307,48 +233,21 @@ func (c *TrelloClient) GetBoardsWithCards(ctx context.Context) ([]models.Board,
// CreateCard creates a new card in the specified list
func (c *TrelloClient) CreateCard(ctx context.Context, listID, name, description string, dueDate *time.Time) (*models.Card, error) {
- // Prepare request payload
- data := url.Values{}
- data.Set("key", c.apiKey)
- data.Set("token", c.token)
+ data := c.authParams()
data.Set("idList", listID)
data.Set("name", name)
-
if description != "" {
data.Set("desc", description)
}
-
if dueDate != nil {
data.Set("due", dueDate.Format(time.RFC3339))
}
- // Create POST request
- reqURL := c.baseURL + "/cards"
- req, err := http.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(data.Encode()))
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %w", err)
- }
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
- // Execute request
- resp, err := c.httpClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to create card: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- return nil, fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body))
- }
-
- // Decode response
var apiCard trelloCardResponse
- if err := json.NewDecoder(resp.Body).Decode(&apiCard); err != nil {
- return nil, fmt.Errorf("failed to decode response: %w", err)
+ if err := c.PostForm(ctx, "/cards", nil, data.Encode(), &apiCard); err != nil {
+ return nil, fmt.Errorf("failed to create card: %w", err)
}
- // Convert to our model
card := &models.Card{
ID: apiCard.ID,
Name: apiCard.Name,
@@ -356,10 +255,8 @@ func (c *TrelloClient) CreateCard(ctx context.Context, listID, name, description
URL: apiCard.URL,
}
- // Parse due date if present
if apiCard.Due != nil && *apiCard.Due != "" {
- parsedDate, err := time.Parse(time.RFC3339, *apiCard.Due)
- if err == nil {
+ if parsedDate, err := time.Parse(time.RFC3339, *apiCard.Due); err == nil {
card.DueDate = &parsedDate
}
}
@@ -369,35 +266,15 @@ func (c *TrelloClient) CreateCard(ctx context.Context, listID, name, description
// UpdateCard updates a card with the specified changes
func (c *TrelloClient) UpdateCard(ctx context.Context, cardID string, updates map[string]interface{}) error {
- // Prepare request payload
- data := url.Values{}
- data.Set("key", c.apiKey)
- data.Set("token", c.token)
-
- // Add updates to payload
+ data := c.authParams()
for key, value := range updates {
data.Set(key, fmt.Sprintf("%v", value))
}
- // Create PUT request
- reqURL := fmt.Sprintf("%s/cards/%s", c.baseURL, cardID)
- req, err := http.NewRequestWithContext(ctx, "PUT", reqURL, strings.NewReader(data.Encode()))
- if err != nil {
- return fmt.Errorf("failed to create request: %w", err)
- }
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
- // Execute request
- resp, err := c.httpClient.Do(req)
- if err != nil {
+ path := fmt.Sprintf("/cards/%s", cardID)
+ if err := c.Put(ctx, path, nil, data.Encode(), nil); err != nil {
return fmt.Errorf("failed to update card: %w", err)
}
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- body, _ := io.ReadAll(resp.Body)
- return fmt.Errorf("trello API error (status %d): %s", resp.StatusCode, string(body))
- }
return nil
}