Files

250 lines
6.3 KiB
Go

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
}