package api import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "time" ) // Client wraps HTTP calls to the chat server API. type Client struct { BaseURL string Token string HTTPClient *http.Client } // NewClient creates a new API client. func NewClient(baseURL string) *Client { return &Client{ BaseURL: baseURL, HTTPClient: &http.Client{ Timeout: 30 * time.Second, }, } } func (c *Client) do(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: %w", err) } bodyReader = bytes.NewReader(data) } req, err := http.NewRequest(method, c.BaseURL+path, bodyReader) if err != nil { return nil, fmt.Errorf("request: %w", err) } req.Header.Set("Content-Type", "application/json") if c.Token != "" { req.Header.Set("Authorization", "Bearer "+c.Token) } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, fmt.Errorf("http: %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 data, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(data)) } return data, nil } // CreateSession creates a new session on the server. func (c *Client) CreateSession(nick string) (*SessionResponse, error) { data, err := c.do("POST", "/api/v1/session", &SessionRequest{Nick: nick}) if err != nil { return nil, err } var resp SessionResponse if err := json.Unmarshal(data, &resp); err != nil { return nil, fmt.Errorf("decode session: %w", err) } c.Token = resp.Token return &resp, nil } // GetState returns the current user state. func (c *Client) GetState() (*StateResponse, error) { data, err := c.do("GET", "/api/v1/state", nil) if err != nil { return nil, err } var resp StateResponse if err := json.Unmarshal(data, &resp); err != nil { return nil, fmt.Errorf("decode state: %w", err) } return &resp, nil } // SendMessage sends a message (any IRC command). func (c *Client) SendMessage(msg *Message) error { _, err := c.do("POST", "/api/v1/messages", msg) return err } // PollMessages long-polls for new messages. func (c *Client) PollMessages(afterID string, timeout int) ([]Message, error) { // Use a longer HTTP timeout than the server long-poll timeout. client := &http.Client{Timeout: time.Duration(timeout+5) * time.Second} params := url.Values{} if afterID != "" { params.Set("after", afterID) } params.Set("timeout", fmt.Sprintf("%d", timeout)) path := "/api/v1/messages" if len(params) > 0 { path += "?" + params.Encode() } req, err := http.NewRequest("GET", c.BaseURL+path, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+c.Token) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode >= 400 { return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(data)) } // The server may return an array directly or wrapped. var msgs []Message if err := json.Unmarshal(data, &msgs); err != nil { // Try wrapped format. var wrapped MessagesResponse if err2 := json.Unmarshal(data, &wrapped); err2 != nil { return nil, fmt.Errorf("decode messages: %w (raw: %s)", err, string(data)) } msgs = wrapped.Messages } return msgs, nil } // JoinChannel joins a channel via the unified command endpoint. func (c *Client) JoinChannel(channel string) error { return c.SendMessage(&Message{Command: "JOIN", To: channel}) } // PartChannel leaves a channel via the unified command endpoint. func (c *Client) PartChannel(channel string) error { return c.SendMessage(&Message{Command: "PART", To: channel}) } // ListChannels returns all channels on the server. func (c *Client) ListChannels() ([]Channel, error) { data, err := c.do("GET", "/api/v1/channels", nil) if err != nil { return nil, err } var channels []Channel if err := json.Unmarshal(data, &channels); err != nil { return nil, err } return channels, nil } // GetMembers returns members of a channel. func (c *Client) GetMembers(channel string) ([]string, error) { data, err := c.do("GET", "/api/v1/channels/"+url.PathEscape(channel)+"/members", nil) if err != nil { return nil, err } var members []string if err := json.Unmarshal(data, &members); err != nil { // Try object format. var obj map[string]interface{} if err2 := json.Unmarshal(data, &obj); err2 != nil { return nil, err } // Extract member names from whatever format. return nil, fmt.Errorf("unexpected members format: %s", string(data)) } return members, nil } // GetServerInfo returns server info. func (c *Client) GetServerInfo() (*ServerInfo, error) { data, err := c.do("GET", "/api/v1/server", nil) if err != nil { return nil, err } var info ServerInfo if err := json.Unmarshal(data, &info); err != nil { return nil, err } return &info, nil }