// Drop-in Go client library for the Sprachmemo HTTP API. // // Save this file alongside your code as `voice_client.go` (or in its // own subpackage) and use the Client type: // // import "yourproject/voice_client" // // c := voice_client.New("pat_...") // rows, err := c.AccountList(&voice_client.ListOpts{Limit: 20, Sort: "-created_at"}) // // Every endpoint exposed by the HTTP API is wrapped as a // `` method on Client. List endpoints take *ListOpts; // get/update/delete endpoints take the row id as their first argument. // // Provided as-is, with no warranty. Vendor freely; modify as needed. // Targets Go 1.21+; uses only the standard library. // // DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site. // Local edits will be overwritten by the once-per-day version check. package voice_client import ( "bytes" "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" ) // ── Identity (substituted at generation time) ─────────────────────── const ( AppSlug = "voice" AppName = "Sprachmemo" ModuleName = "voice_client" ClientVersion = "0.3.13" Language = "go" defaultBase = "https://sprachmemo.de" ) // TypesJSON is the per-type metadata baked at generation time. // Available at runtime when calling code needs to know the legal // filters / sort columns / max_limit for a model without a second // round-trip. const TypesJSON = `{"recording":{"ops":["list","read","create","update","delete"],"create_fields":["parent_id","blob_id","filename","mime","duration_seconds","sample_rate","channels","size_bytes","recorded_at","state","primary_model","requested_model","language","transcriptions","client_id"],"update_fields":["blob_id","state","primary_model","requested_model","transcriptions","transcription_error","transcript_edit","duration_seconds","language","appended_to_body"],"allowed_filters":["data__parent_id","data__state","data__primary_model","status","is_archived","owned_by"],"allowed_sorts":["created_at","data__recorded_at"],"default_sort":"-data__recorded_at","max_limit":200,"fields":[{"name":"mime","type":"string","max_len":64},{"name":"state","type":"enum","values":["uploading","ready","transcribing","transcribed","failed"]},{"name":"blob_id","type":"string","max_len":64},{"name":"channels","type":"number"},{"name":"filename","type":"string","max_len":200},{"name":"language","type":"string","max_len":8},{"name":"client_id","type":"string","max_len":64},{"name":"parent_id","type":"string","max_len":64,"ref":{"type":"voice_note","owned":true,"optional":false}},{"name":"size_bytes","type":"number"},{"name":"recorded_at","type":"string","max_len":32},{"name":"sample_rate","type":"number"},{"name":"primary_model","type":"string","max_len":32},{"name":"transcriptions","type":"dict"},{"name":"requested_model","type":"string","max_len":32},{"name":"appended_to_body","type":"bool"},{"name":"duration_seconds","type":"number"},{"name":"transcription_error","type":"string","max_len":400}]},"voice_note":{"ops":["list","read","create","update","delete"],"create_fields":["title","body","tags","color","favorite","pinned","last_recording_at","last_recording_id","updated_at_marker"],"update_fields":["title","body","tags","color","favorite","pinned","last_recording_at","last_recording_id","updated_at_marker"],"allowed_filters":["data__favorite","data__tags","data__color","data__pinned","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at","data__title","data__last_recording_at"],"default_sort":"-data__last_recording_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":65000},{"name":"tags","type":"tags"},{"name":"color","type":"string","max_len":24},{"name":"title","type":"string","max_len":200},{"name":"pinned","type":"bool"},{"name":"favorite","type":"bool"},{"name":"last_recording_at","type":"string","max_len":32},{"name":"last_recording_id","type":"string","max_len":64},{"name":"updated_at_marker","type":"string","max_len":32}]},"voice_tag":{"ops":["list","read","create","update","delete"],"create_fields":["name","color","icon"],"update_fields":["name","color","icon"],"allowed_filters":["data__name","status","owned_by"],"allowed_sorts":["created_at","data__name"],"default_sort":"data__name","max_limit":200,"fields":[{"name":"icon","type":"string","max_len":32},{"name":"name","type":"string","max_len":80},{"name":"color","type":"string","max_len":24}]}}` // ── Configuration ────────────────────────────────────────────────── // ListOpts mirrors the standard query parameters the list endpoints // accept. Filters carries arbitrary additional ?key=value pairs. type ListOpts struct { Limit int Offset int Sort string Q string Filters map[string]any } // Client is the per-app HTTP client. Reuse across requests; safe for // concurrent use. type Client struct { HTTPClient *http.Client BaseURL string Token string once sync.Once deviceID string sessID string } // New returns a Client wired to the host this library was generated // against. Pass a personal access token; an empty string falls back to // the XCLIENT_TOKEN environment variable. func New(token string) *Client { base := os.Getenv("XCLIENT_BASE_URL") if base == "" { base = defaultBase } if token == "" { token = os.Getenv("XCLIENT_TOKEN") } return &Client{ HTTPClient: &http.Client{Timeout: 30 * time.Second}, BaseURL: strings.TrimRight(base, "/"), Token: token, } } func (c *Client) initIDs() { c.once.Do(func() { c.deviceID = loadOrMintDeviceID() c.sessID = mintUUID() }) } // ── Identifier persistence ───────────────────────────────────────── func stateDir() string { home, err := os.UserHomeDir() if err != nil || home == "" { return "" } d := filepath.Join(home, "."+ModuleName) _ = os.MkdirAll(d, 0o700) return d } func mintUUID() string { var b [16]byte _, _ = rand.Read(b[:]) b[6] = (b[6] & 0x0f) | 0x40 b[8] = (b[8] & 0x3f) | 0x80 h := hex.EncodeToString(b[:]) return h[0:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:32] } func loadOrMintDeviceID() string { dir := stateDir() if dir == "" { return mintUUID() } f := filepath.Join(dir, "device.json") if raw, err := os.ReadFile(f); err == nil { var blob struct { DeviceID string `json:"device_id"` } if json.Unmarshal(raw, &blob) == nil && len(blob.DeviceID) >= 32 { return blob.DeviceID } } id := mintUUID() body, _ := json.Marshal(map[string]string{"device_id": id}) _ = os.WriteFile(f, body, 0o600) return id } // ── Telemetry toggles ────────────────────────────────────────────── func autoupdateEnabled() bool { v := strings.ToLower(os.Getenv("XCLIENT_NO_AUTOUPDATE")) return v != "1" && v != "true" && v != "yes" } // ── Editor / runtime fingerprint ─────────────────────────────────── func fingerprint() map[string]any { out := map[string]any{ "go_version": runtime.Version(), "os": runtime.GOOS, "arch": runtime.GOARCH, } out["term_program"] = os.Getenv("TERM_PROGRAM") out["editor_env"] = os.Getenv("EDITOR") out["ci"] = os.Getenv("CI") != "" || os.Getenv("GITHUB_ACTIONS") != "" out["claude_code"] = os.Getenv("CLAUDECODE") != "" || os.Getenv("CLAUDE_CODE_ENTRYPOINT") != "" out["codex"] = os.Getenv("CODEX_HOME") != "" tp := strings.ToLower(os.Getenv("TERM_PROGRAM")) out["vscode"] = tp == "vscode" && os.Getenv("CURSOR_TRACE_ID") == "" out["cursor"] = os.Getenv("CURSOR_TRACE_ID") != "" out["antigravity"] = os.Getenv("ANTIGRAVITY_TRACE_ID") != "" out["jetbrains"] = strings.Contains(tp, "jetbrains") return out } // ── HTTP transport ───────────────────────────────────────────────── // APIError wraps a non-2xx response. type APIError struct { Status int Message string Body any } func (e *APIError) Error() string { return fmt.Sprintf("HTTP %d: %s", e.Status, e.Message) } var retryableStatus = map[int]struct{}{ 408: {}, 425: {}, 429: {}, 500: {}, 502: {}, 503: {}, 504: {}, } func backoff(attempt int, retryAfterSec float64) time.Duration { if retryAfterSec >= 0 { if retryAfterSec > 60 { retryAfterSec = 60 } return time.Duration(retryAfterSec * float64(time.Second)) } delay := float64(int(1) << uint(attempt)) if delay > 60 { delay = 60 } return time.Duration(delay * float64(time.Second)) } func (c *Client) userAgent() string { return fmt.Sprintf("%s/%s (lib/%s; go/%s; %s)", ModuleName, ClientVersion, Language, runtime.Version(), runtime.GOOS) } // requestJSON fires one method+path against the API, JSON in / JSON // out. Pass nil body for read-only verbs. func (c *Client) requestJSON(method, path string, body any) (map[string]any, error) { c.maybeAutoupdate() c.initIDs() u := c.BaseURL + path var data []byte if body != nil { var err error data, err = json.Marshal(body) if err != nil { return nil, err } } const maxRetries = 3 var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { var bodyReader io.Reader if data != nil { bodyReader = bytes.NewReader(data) } req, err := http.NewRequest(method, u, bodyReader) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", c.userAgent()) req.Header.Set("X-Client-Channel", "client_"+Language) req.Header.Set("X-Client-Version", ClientVersion) req.Header.Set("X-Analytics-Device-Id", c.deviceID) req.Header.Set("X-Analytics-Session-Id", c.sessID) if c.Token != "" { req.Header.Set("Authorization", "Bearer "+c.Token) } if data != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.HTTPClient.Do(req) if err != nil { lastErr = err if attempt+1 < maxRetries { time.Sleep(backoff(attempt, -1)) continue } c.emitCallEvent(method, path, 0, false) return nil, &APIError{Status: 0, Message: err.Error()} } raw, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() c.maybePersistRefresh(resp.Header) if _, retry := retryableStatus[resp.StatusCode]; retry && attempt+1 < maxRetries { ra := -1.0 if v := resp.Header.Get("Retry-After"); v != "" { if f, err2 := strconv.ParseFloat(v, 64); err2 == nil { ra = f } } time.Sleep(backoff(attempt, ra)) continue } if resp.StatusCode >= 400 { var parsed any _ = json.Unmarshal(raw, &parsed) msg := http.StatusText(resp.StatusCode) if m, ok := parsed.(map[string]any); ok { if d, ok2 := m["detail"].(string); ok2 { msg = d } else if d, ok2 := m["message"].(string); ok2 { msg = d } } c.emitCallEvent(method, path, resp.StatusCode, false) return nil, &APIError{Status: resp.StatusCode, Message: msg, Body: parsed} } c.emitCallEvent(method, path, resp.StatusCode, true) if len(raw) == 0 { return nil, nil } var out map[string]any if err := json.Unmarshal(raw, &out); err != nil { return nil, err } return out, nil } if lastErr != nil { c.emitCallEvent(method, path, 0, false) return nil, &APIError{Status: 0, Message: lastErr.Error()} } return nil, errors.New("request failed") } // requestList wraps requestJSON for list endpoints, lifting *ListOpts // into the query string. func (c *Client) requestList(path string, opts *ListOpts) (map[string]any, error) { q := url.Values{} if opts != nil { if opts.Limit > 0 { q.Set("limit", strconv.Itoa(opts.Limit)) } if opts.Offset > 0 { q.Set("offset", strconv.Itoa(opts.Offset)) } if opts.Sort != "" { q.Set("sort", opts.Sort) } if opts.Q != "" { q.Set("q", opts.Q) } for k, v := range opts.Filters { if v == nil { continue } q.Set(k, fmt.Sprint(v)) } } if encoded := q.Encode(); encoded != "" { path = path + "?" + encoded } return c.requestJSON("GET", path, nil) } func (c *Client) maybePersistRefresh(h http.Header) { if v := h.Get("x-auth-refresh-token"); v != "" { c.Token = v } } // ── Analytics ────────────────────────────────────────────────────── var metaSentOnce sync.Once func (c *Client) emitCallEvent(method, pathStr string, status int, ok bool) { go func() { defer func() { _ = recover() }() meta := map[string]any{ "channel": "client_" + Language, "client_version": ClientVersion, "module_name": ModuleName, "language": Language, "os": runtime.GOOS, "go_version": runtime.Version(), } var addEnv bool metaSentOnce.Do(func() { addEnv = true }) if addEnv { meta["env"] = fingerprint() } evt := map[string]any{ "type": "client.call", "ts_client": time.Now().Unix(), "meta": map[string]any{ "method": strings.ToUpper(method), "path": strings.SplitN(pathStr, "?", 2)[0], "status": status, "ok": ok, }, } body := map[string]any{ "device_id": c.deviceID, "session_id": c.sessID, "events": []any{evt}, "meta": meta, } raw, _ := json.Marshal(body) req, err := http.NewRequest("POST", c.BaseURL+"/xapi2/analytics/challenge", bytes.NewReader(raw)) if err != nil { return } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", c.userAgent()) hc := &http.Client{Timeout: 4 * time.Second} resp, err := hc.Do(req) if err != nil { return } _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() }() } // ── Auto-update ──────────────────────────────────────────────────── var autoupdateOnce sync.Once func (c *Client) maybeAutoupdate() { autoupdateOnce.Do(func() { if !autoupdateEnabled() { return } go c.runAutoupdate() }) } func (c *Client) runAutoupdate() { defer func() { _ = recover() }() dir := stateDir() if dir == "" { return } stamp := filepath.Join(dir, "update_check.json") if raw, err := os.ReadFile(stamp); err == nil { var blob struct { CheckedAt int64 `json:"checked_at"` } if json.Unmarshal(raw, &blob) == nil { if time.Now().Unix()-blob.CheckedAt < 86400 { return } } } hc := &http.Client{Timeout: 6 * time.Second} resp, err := hc.Get(c.BaseURL + "/xapi2/clients/version") if err != nil { return } raw, _ := io.ReadAll(resp.Body) _ = resp.Body.Close() var payload struct { Version string `json:"version"` } if json.Unmarshal(raw, &payload) != nil { return } stampBody, _ := json.Marshal(map[string]any{"checked_at": time.Now().Unix()}) _ = os.WriteFile(stamp, stampBody, 0o600) if payload.Version == "" || payload.Version == ClientVersion { return } // Source replacement is intentionally a no-op in Go - the user is // running a compiled binary, the .go file on disk is just a record // of the version they vendored. Surface the new version through // the next build. } // RecordingList lists recording rows. Pass nil opts for defaults. func (c *Client) RecordingList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/recording", opts) } // RecordingGet fetches one recording row by id. func (c *Client) RecordingGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/recording/"+id, nil) } // RecordingCreate creates a new recording row. func (c *Client) RecordingCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/recording", data) } // RecordingUpdate patches an existing recording row. func (c *Client) RecordingUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/recording/"+id, data) } // RecordingDelete deletes a recording row. func (c *Client) RecordingDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/recording/"+id, nil) return err } // VoiceNoteList lists voice_note rows. Pass nil opts for defaults. func (c *Client) VoiceNoteList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/voice_note", opts) } // VoiceNoteGet fetches one voice_note row by id. func (c *Client) VoiceNoteGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/voice_note/"+id, nil) } // VoiceNoteCreate creates a new voice_note row. func (c *Client) VoiceNoteCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/voice_note", data) } // VoiceNoteUpdate patches an existing voice_note row. func (c *Client) VoiceNoteUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/voice_note/"+id, data) } // VoiceNoteDelete deletes a voice_note row. func (c *Client) VoiceNoteDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/voice_note/"+id, nil) return err } // VoiceTagList lists voice_tag rows. Pass nil opts for defaults. func (c *Client) VoiceTagList(opts *ListOpts) (map[string]any, error) { return c.requestList("/xapi2/data/voice_tag", opts) } // VoiceTagGet fetches one voice_tag row by id. func (c *Client) VoiceTagGet(id string) (map[string]any, error) { return c.requestJSON("GET", "/xapi2/data/voice_tag/"+id, nil) } // VoiceTagCreate creates a new voice_tag row. func (c *Client) VoiceTagCreate(data map[string]any) (map[string]any, error) { return c.requestJSON("POST", "/xapi2/data/voice_tag", data) } // VoiceTagUpdate patches an existing voice_tag row. func (c *Client) VoiceTagUpdate(id string, data map[string]any) (map[string]any, error) { return c.requestJSON("PATCH", "/xapi2/data/voice_tag/"+id, data) } // VoiceTagDelete deletes a voice_tag row. func (c *Client) VoiceTagDelete(id string) error { _, err := c.requestJSON("DELETE", "/xapi2/data/voice_tag/"+id, nil) return err }