250 lines
6.3 KiB
Go
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
|
|
}
|