package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/cookiejar" "strings" "time" ) // APIClient wraps HTTP calls to the dumpster server. type APIClient struct { BaseURL string HTTP *http.Client SessionToken string } // NewAPIClient creates an API client with a cookie jar for session auth. func NewAPIClient(baseURL string) *APIClient { jar, _ := cookiejar.New(nil) return &APIClient{ BaseURL: strings.TrimRight(baseURL, "/"), HTTP: &http.Client{ Jar: jar, Timeout: 15 * time.Second, }, } } // ---- Auth ---- type LoginRequest struct { Email string `json:"email"` Password string `json:"password"` } type LoginResponse struct { ID string `json:"id"` } // Login authenticates and stores the session cookie. func (c *APIClient) Login(email, password string) (*LoginResponse, error) { body, _ := json.Marshal(LoginRequest{Email: email, Password: password}) resp, err := c.HTTP.Post(c.BaseURL+"/api/v1/auth/login/password", "application/json", bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("login request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("login failed (%d): %s", resp.StatusCode, string(b)) } // Extract session token from cookie for _, cookie := range resp.Cookies() { if cookie.Name == "dumpster_session" { c.SessionToken = cookie.Value break } } var lr LoginResponse if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil { return nil, fmt.Errorf("decode login: %w", err) } return &lr, nil } // SetToken sets the session cookie directly (for pre-authenticated sessions). func (c *APIClient) SetToken(token string) { c.SessionToken = token // Set cookie on all requests to the base URL // We do this by setting up a cookie manually } // doRequest performs an authenticated request using the session cookie. func (c *APIClient) doRequest(method, path string, body interface{}) ([]byte, error) { var bodyReader io.Reader if body != nil { data, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal body: %w", err) } bodyReader = bytes.NewReader(data) } req, err := http.NewRequest(method, c.BaseURL+path, bodyReader) if err != nil { return nil, fmt.Errorf("create request: %w", err) } if body != nil { req.Header.Set("Content-Type", "application/json") } // Add session cookie if c.SessionToken != "" { req.AddCookie(&http.Cookie{ Name: "dumpster_session", Value: c.SessionToken, }) } resp, err := c.HTTP.Do(req) if err != nil { return nil, fmt.Errorf("do request: %w", err) } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read body: %w", err) } if resp.StatusCode >= 400 { return nil, fmt.Errorf("API error (%d): %s", resp.StatusCode, string(data)) } return data, nil } // ---- Data types ---- type UserProfile struct { ID string `json:"id"` Username string `json:"username"` DisplayName string `json:"display_name"` Email string `json:"email"` Avatar string `json:"avatar"` Bio string `json:"bio"` AccentColor string `json:"accent_color"` Status string `json:"status"` StatusText string `json:"status_text"` CreatedAt string `json:"created_at"` } type Server struct { ID string `json:"id"` Name string `json:"name"` Icon string `json:"icon"` OwnerID string `json:"owner_id"` CreatedAt string `json:"created_at"` } type Channel struct { ID string `json:"id"` ServerID string `json:"server_id"` Name string `json:"name"` Type string `json:"type"` Category string `json:"category"` Position int `json:"position"` CreatedAt string `json:"created_at"` } type Message struct { ID string `json:"id"` ChannelID string `json:"channel_id"` AuthorID string `json:"author_id"` AuthorName string `json:"author_username"` DisplayName *string `json:"author_display_name"` Content string `json:"content"` EditedAt *string `json:"edited_at"` CreatedAt string `json:"created_at"` } type VoiceTokenResponse struct { Token string `json:"token"` LiveKitURL string `json:"livekit_url"` } // ---- API methods ---- func (c *APIClient) GetMe() (*UserProfile, error) { data, err := c.doRequest("GET", "/api/v1/auth/me", nil) if err != nil { return nil, err } var user UserProfile if err := json.Unmarshal(data, &user); err != nil { return nil, fmt.Errorf("decode me: %w", err) } return &user, nil } func (c *APIClient) GetServers() ([]Server, error) { data, err := c.doRequest("GET", "/api/v1/servers", nil) if err != nil { return nil, err } var servers []Server if err := json.Unmarshal(data, &servers); err != nil { return nil, fmt.Errorf("decode servers: %w", err) } return servers, nil } func (c *APIClient) GetChannels(serverID string) ([]Channel, error) { data, err := c.doRequest("GET", "/api/v1/channels?server_id="+serverID, nil) if err != nil { return nil, err } var channels []Channel if err := json.Unmarshal(data, &channels); err != nil { return nil, fmt.Errorf("decode channels: %w", err) } return channels, nil } func (c *APIClient) GetMessages(channelID string, limit int, before string) ([]Message, error) { path := fmt.Sprintf("/api/v1/channels/%s/messages?limit=%d", channelID, limit) if before != "" { path += "&before=" + before } data, err := c.doRequest("GET", path, nil) if err != nil { return nil, err } var messages []Message if err := json.Unmarshal(data, &messages); err != nil { return nil, fmt.Errorf("decode messages: %w", err) } return messages, nil } func (c *APIClient) SendMessage(channelID, content string) (*Message, error) { data, err := c.doRequest("POST", "/api/v1/channels/"+channelID+"/messages", map[string]string{"content": content}) if err != nil { return nil, err } var msg Message if err := json.Unmarshal(data, &msg); err != nil { return nil, fmt.Errorf("decode message: %w", err) } return &msg, nil } func (c *APIClient) GetVoiceToken(roomName string) (*VoiceTokenResponse, error) { data, err := c.doRequest("POST", "/api/v1/voice/token", map[string]string{"room_name": roomName}) if err != nil { return nil, err } var resp VoiceTokenResponse if err := json.Unmarshal(data, &resp); err != nil { return nil, fmt.Errorf("decode voice token: %w", err) } return &resp, nil }