client.go
1package ysweet
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "strings"
9)
10
11type Client struct {
12 baseURL string
13}
14
15func NewClient(baseURL string) (*Client, error) {
16 baseURL = strings.TrimSuffix(baseURL, "/")
17 if baseURL == "" {
18 return nil, fmt.Errorf("baseURL is required")
19 }
20 if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
21 return nil, fmt.Errorf("baseURL must start with http:// or https://")
22 }
23 return &Client{baseURL: baseURL}, nil
24}
25
26type newDocRequest struct {
27 DocID string `json:"docID"`
28}
29
30type newDocResponse struct {
31 DocID string `json:"docID"`
32}
33
34type AuthDocResponse struct {
35 URL string `json:"url"`
36 BaseURL string `json:"baseUrl"`
37 DocID string `json:"docId"`
38 Token string `json:"token,omitempty"`
39 Authorization string `json:"authorization,omitempty"`
40}
41
42// Returns the actual WebsocketURL y-sweet expects a client to connect to, which is /d/{docID}/ws/{docID} for
43// some unknown reason. Also includes token as query string, if defined.
44func (a AuthDocResponse) WebsocketURL() string {
45 if a.Token != "" {
46 return fmt.Sprintf("%s/%s?token=%s", a.URL, a.DocID, a.Token)
47 }
48 return fmt.Sprintf("%s/%s", a.URL, a.DocID)
49}
50
51func (c *Client) NewDoc(docID string) (string, error) {
52 var body []byte
53 var err error
54 if docID != "" {
55 body, err = json.Marshal(newDocRequest{DocID: docID})
56 if err != nil {
57 return "", fmt.Errorf("failed to marshal request: %w", err)
58 }
59 } else {
60 body, err = json.Marshal(struct{}{})
61 if err != nil {
62 return "", fmt.Errorf("failed to marshal request: %w", err)
63 }
64 }
65
66 req, err := http.NewRequest(http.MethodPost, c.baseURL+"/doc/new", bytes.NewReader(body))
67 if err != nil {
68 return "", fmt.Errorf("failed to create request: %w", err)
69 }
70 req.Header.Set("Content-Type", "application/json")
71
72 resp, err := http.DefaultClient.Do(req)
73 if err != nil {
74 return "", fmt.Errorf("request failed: %w", err)
75 }
76 defer resp.Body.Close()
77
78 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
79 return "", fmt.Errorf("server returned status %d", resp.StatusCode)
80 }
81
82 var result newDocResponse
83 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
84 return "", fmt.Errorf("failed to decode response: %w", err)
85 }
86
87 if docID != "" && result.DocID != docID {
88 return "", fmt.Errorf("response docID %q does not match requested %q", result.DocID, docID)
89 }
90
91 return result.DocID, nil
92}
93
94func (c *Client) AuthDoc(docID string) (AuthDocResponse, error) {
95 if docID == "" {
96 return AuthDocResponse{}, fmt.Errorf("docID is required")
97 }
98
99 req, err := http.NewRequest(http.MethodPost, c.baseURL+"/doc/"+docID+"/auth", nil)
100 if err != nil {
101 return AuthDocResponse{}, fmt.Errorf("failed to create request: %w", err)
102 }
103
104 resp, err := http.DefaultClient.Do(req)
105 if err != nil {
106 return AuthDocResponse{}, fmt.Errorf("request failed: %w", err)
107 }
108 defer resp.Body.Close()
109
110 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
111 return AuthDocResponse{}, fmt.Errorf("server returned status %d", resp.StatusCode)
112 }
113
114 var result AuthDocResponse
115 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
116 return AuthDocResponse{}, fmt.Errorf("failed to decode response: %w", err)
117 }
118
119 return result, nil
120}