package platform import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) // ClipboardMaxBytes mirrors the backend's CDROP_CLIPBOARD_MAX_BYTES (default // 64 KiB). Uploads larger than this are rejected client-side, matching the web. const ClipboardMaxBytes = 64 * 1024 // Client is an authenticated client for the cdrop backend API. The access token // is held here in Go (never in the WebView); deviceName identifies this device // as the source of clipboard writes via the X-Device-Name header. type Client struct { baseURL string token string deviceName string http *http.Client } // NewClient builds a Client. baseURL is the backend origin, e.g. // https://drop.commilitia.net. func NewClient(baseURL, accessToken, deviceName string) *Client { return &Client{ baseURL: strings.TrimRight(baseURL, "/"), token: accessToken, deviceName: deviceName, http: &http.Client{Timeout: 30 * time.Second}, } } // ClipboardContent is the cloud clipboard state. UpdatedAt is the server's // authoritative timestamp (also the ETag); 0 means never written. type ClipboardContent struct { Content string `json:"content"` ContentType string `json:"content_type"` SourceDevice string `json:"source_device"` UpdatedAt int64 `json:"updated_at"` } func (c *Client) authReq(ctx context.Context, method, path string, body io.Reader) (*http.Request, error) { req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Accept", "application/json") if c.deviceName != "" { req.Header.Set("X-Device-Name", c.deviceName) } return req, nil } // Clipboard fetches the current cloud clipboard content. func (c *Client) Clipboard(ctx context.Context) (ClipboardContent, error) { req, err := c.authReq(ctx, http.MethodGet, "/api/clipboard", nil) if err != nil { return ClipboardContent{}, fmt.Errorf("api: build clipboard request: %w", err) } resp, err := c.http.Do(req) if err != nil { return ClipboardContent{}, fmt.Errorf("api: clipboard request: %w", err) } defer resp.Body.Close() if resp.StatusCode/100 != 2 { return ClipboardContent{}, fmt.Errorf("api: GET clipboard status %d", resp.StatusCode) } var cc ClipboardContent if err := json.NewDecoder(resp.Body).Decode(&cc); err != nil { return ClipboardContent{}, fmt.Errorf("api: decode clipboard: %w", err) } return cc, nil } // PutClipboard uploads plain text to the cloud clipboard. func (c *Client) PutClipboard(ctx context.Context, text string) error { if len(text) > ClipboardMaxBytes { return fmt.Errorf("api: clipboard content %d bytes exceeds limit %d", len(text), ClipboardMaxBytes) } payload, err := json.Marshal(map[string]string{ "content": text, "content_type": "text/plain", }) if err != nil { return fmt.Errorf("api: marshal clipboard: %w", err) } req, err := c.authReq(ctx, http.MethodPut, "/api/clipboard", bytes.NewReader(payload)) if err != nil { return fmt.Errorf("api: build clipboard request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := c.http.Do(req) if err != nil { return fmt.Errorf("api: clipboard upload: %w", err) } defer resp.Body.Close() if resp.StatusCode/100 != 2 { return fmt.Errorf("api: PUT clipboard status %d", resp.StatusCode) } return nil }