package pocketbase import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" ) // Client handles interactions with the PocketBase API. type Client struct { baseURL string user string password string category string http *http.Client } // New creates a new PocketBase client. func New(baseURL, user, password, category string) *Client { // Normalise: ensure baseURL does not already end with /api. baseURL = strings.TrimSuffix(baseURL, "/api") baseURL = strings.TrimSuffix(baseURL, "/") return &Client{ baseURL: baseURL, user: user, password: password, category: category, http: &http.Client{ Timeout: 30 * time.Second, }, } } type authRequest struct { Identity string `json:"identity"` Password string `json:"password"` } type authResponse struct { Token string `json:"token"` } type record struct { URL string `json:"url"` } type recordsResponse struct { Items []record `json:"items"` } // GetBookmarkURLs authenticates and retrieves URLs from the bookmarks collection. func (c *Client) GetBookmarkURLs(ctx context.Context) ([]string, error) { token, err := c.authenticate(ctx) if err != nil { return nil, fmt.Errorf("pocketbase auth: %w", err) } urls, err := c.fetchURLs(ctx, token) if err != nil { return nil, fmt.Errorf("pocketbase fetch URLs: %w", err) } return urls, nil } func (c *Client) authenticate(ctx context.Context) (string, error) { reqBody, err := json.Marshal(authRequest{ Identity: c.user, Password: c.password, }) if err != nil { return "", err } authURL := c.baseURL + "/api/collections/users/auth-with-password" req, err := http.NewRequestWithContext(ctx, "POST", authURL, bytes.NewBuffer(reqBody)) if err != nil { return "", err } req.Header.Set("Content-Type", "application/json") resp, err := c.http.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("unexpected status: %d", resp.StatusCode) } var authResp authResponse if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil { return "", err } return authResp.Token, nil } func (c *Client) fetchURLs(ctx context.Context, token string) ([]string, error) { // Filter: category=, perPage=200, fields=url filter := fmt.Sprintf(`category="%s"`, c.category) u, err := url.Parse(c.baseURL + "/api/collections/bookmarks/records") if err != nil { return nil, fmt.Errorf("invalid base URL: %w", err) } q := u.Query() q.Set("filter", filter) q.Set("perPage", "200") q.Set("fields", "url") u.RawQuery = q.Encode() req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } req.Header.Set("Authorization", token) resp, err := c.http.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) } var recs recordsResponse if err := json.NewDecoder(resp.Body).Decode(&recs); err != nil { return nil, err } var urls []string for _, r := range recs.Items { if r.URL != "" { urls = append(urls, r.URL) } } return urls, nil }