diff options
Diffstat (limited to 'internal/api/trello.go')
| -rw-r--r-- | internal/api/trello.go | 211 |
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 } |
