package api import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strconv" "time" ) const defaultBaseURL = "https://api.timeweb.cloud" // Client is a minimal HTTP client for the Timeweb Cloud API. type Client struct { baseURL string token string client *http.Client } // NewClient creates a new Timeweb Cloud API client. func NewClient(token string) *Client { return &Client{ baseURL: defaultBaseURL, token: token, client: &http.Client{Timeout: 30 * time.Second}, } } func (c *Client) request(ctx context.Context, method, path string, body []byte) (*http.Response, error) { url := c.baseURL + path var bodyReader io.Reader if body != nil { bodyReader = bytes.NewReader(body) } req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Content-Type", "application/json") return c.client.Do(req) } // CreateServerRequest is the payload for creating a server. type CreateServerRequest struct { Name string `json:"name"` OsID int32 `json:"os_id,omitempty"` ImageID string `json:"image_id,omitempty"` PresetID int32 `json:"preset_id,omitempty"` Bandwidth int32 `json:"bandwidth,omitempty"` SSHKeysIds []int32 `json:"ssh_keys_ids,omitempty"` CloudInit string `json:"cloud_init,omitempty"` AvailabilityZone string `json:"availability_zone,omitempty"` Hostname string `json:"hostname,omitempty"` Comment string `json:"comment,omitempty"` } // CreateServerResponse is the response from creating a server. type CreateServerResponse struct { Server Server `json:"server"` } // Server represents a Timeweb Cloud VDS. type Server struct { ID int32 `json:"id"` Name string `json:"name"` Status string `json:"status"` Location string `json:"location"` } // CreateServer creates a new cloud server. func (c *Client) CreateServer(ctx context.Context, req CreateServerRequest) (*CreateServerResponse, error) { payload, err := json.Marshal(req) if err != nil { return nil, err } resp, err := c.request(ctx, http.MethodPost, "/api/v1/servers", payload) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) } var result CreateServerResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return &result, nil } // DeleteServer deletes a cloud server by ID. func (c *Client) DeleteServer(ctx context.Context, id int32) error { path := "/api/v1/servers/" + strconv.FormatInt(int64(id), 10) resp, err := c.request(ctx, http.MethodDelete, path, nil) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) } return nil } // GetServersResponse is the response for listing servers. type GetServersResponse struct { Meta Meta `json:"meta"` Servers []Server `json:"servers"` } // Meta contains pagination info. type Meta struct { Total int32 `json:"total"` } // GetServers lists all cloud servers. func (c *Client) GetServers(ctx context.Context) (*GetServersResponse, error) { resp, err := c.request(ctx, http.MethodGet, "/api/v1/servers", nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) } var result GetServersResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return &result, nil } type OS struct { ID int32 `json:"id"` Family string `json:"family"` Name string `json:"name"` Version string `json:"version"` VersionCodename string `json:"version_codename"` Description string `json:"description"` } type GetOSListResponse struct { OsList []OS `json:"servers_os"` } func (c *Client) GetOSList(ctx context.Context) (*GetOSListResponse, error) { resp, err := c.request(ctx, http.MethodGet, "/api/v1/os/servers", nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) } var result GetOSListResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return &result, nil } type Preset struct { ID int32 `json:"id"` Location string `json:"location"` Price float64 `json:"price"` CPU int32 `json:"cpu"` CPUFrequency string `json:"cpu_frequency"` RAM int32 `json:"ram"` Disk int32 `json:"disk"` DiskType string `json:"disk_type"` Bandwidth int32 `json:"bandwidth"` DescriptionShort string `json:"description_short"` IsDedicatedCPU bool `json:"is_dedicated_cpu"` Tags []string `json:"tags"` } type GetPresetsResponse struct { Presets []Preset `json:"server_presets"` } func (c *Client) GetServerPresets(ctx context.Context) (*GetPresetsResponse, error) { resp, err := c.request(ctx, http.MethodGet, "/api/v1/presets/servers", nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) } var result GetPresetsResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return &result, nil }