diff --git a/account_conn.go b/account_conn.go new file mode 100644 index 0000000..88a2046 --- /dev/null +++ b/account_conn.go @@ -0,0 +1,323 @@ +package vocode + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/milosgajdos/go-vocode/request" +) + +type AccountConnType string + +const ( + OpenAIConnType AccountConnType = "account_connection_openai" + TwilioConnType AccountConnType = "account_connection_twilio" +) + +type OpenAICreds struct { + APIKey string `json:"openai_api_key"` +} + +type OpenAIAccount struct { + Creds *OpenAICreds `json:"credentials"` +} + +type TwilioCreds struct { + AccountID string `json:"twilio_account_sid"` + AuthToken string `json:"twilio_auth_token"` +} + +type TwilioAccount struct { + Creds *TwilioCreds `json:"credentials"` + SteeringPool []string `json:"steering_pool"` + SupportsAnyCaller bool `json:"account_supports_any_caller_id"` +} + +type AccountConnsBase struct { + ID string `json:"id"` + UserID string `json:"user_id"` + Type AccountConnType `json:"type"` +} + +type AccountConns struct { + Items []AccountConn `json:"items"` + *Paging +} + +type AccountConn struct { + AccountConnsBase + TwilioAccount *TwilioAccount `json:",omitempty"` + OpenAIAccount *OpenAIAccount `json:",omitempty"` +} + +func (a *AccountConn) UnmarshalJSON(data []byte) error { + var base AccountConnsBase + if err := json.Unmarshal(data, &base); err != nil { + return err + } + a.AccountConnsBase = base + + switch a.Type { + case OpenAIConnType: + var openaiAccount OpenAIAccount + if err := json.Unmarshal(data, &openaiAccount); err != nil { + return err + } + a.OpenAIAccount = &openaiAccount + case TwilioConnType: + var twillioAccount TwilioAccount + if err := json.Unmarshal(data, &twillioAccount); err != nil { + return err + } + a.TwilioAccount = &twillioAccount + } + + return nil +} + +type AccountConnReqBase struct { + Type AccountConnType `json:"type"` + TwilioAccount *TwilioAccount `json:"-"` + OpenAIAccount *OpenAIAccount `json:"-"` +} + +func (a AccountConnReqBase) MarshalJSON() ([]byte, error) { + type Alias AccountConnReqBase + + switch a.Type { + case OpenAIConnType: + return json.Marshal(&struct { + *Alias + *OpenAIAccount + }{ + Alias: (*Alias)(&a), + OpenAIAccount: a.OpenAIAccount, + }) + case TwilioConnType: + return json.Marshal(&struct { + *Alias + *TwilioAccount + }{ + Alias: (*Alias)(&a), + TwilioAccount: a.TwilioAccount, + }) + default: + return nil, fmt.Errorf("unsupported account connection type: %s", a.Type) + } +} + +type CreateAccountConnReq struct { + AccountConnReqBase +} + +func (a CreateAccountConnReq) MarshalJSON() ([]byte, error) { + return a.AccountConnReqBase.MarshalJSON() +} + +type UpdateAccountConnReq struct { + AccountConnReqBase +} + +func (a UpdateAccountConnReq) MarshalJSON() ([]byte, error) { + return a.AccountConnReqBase.MarshalJSON() +} + +func (c *Client) ListAccountConns(ctx context.Context, paging *PageParams) (*AccountConns, error) { + u, err := url.Parse(c.opts.BaseURL + "/" + c.opts.Version + "/account_connections/list") + if err != nil { + return nil, err + } + + options := []request.HTTPOption{ + request.WithBearer(c.opts.APIKey), + } + if paging != nil { + request.WithPageParams(paging.Encode()) + } + + req, err := request.NewHTTP(ctx, http.MethodGet, u.String(), nil, options...) + if err != nil { + return nil, err + } + + resp, err := request.Do[APIError](c.opts.HTTPClient, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + actions := new(AccountConns) + if err := json.NewDecoder(resp.Body).Decode(actions); err != nil { + return nil, err + } + return actions, nil + case http.StatusForbidden: + var apiErr APIAuthError + if jsonErr := json.NewDecoder(resp.Body).Decode(&apiErr); jsonErr != nil { + return nil, errors.Join(err, jsonErr) + } + return nil, apiErr + case http.StatusTooManyRequests: + return nil, ErrTooManyRequests + case http.StatusUnprocessableEntity: + return nil, ErrUnprocessableEntity + default: + return nil, fmt.Errorf("%w: %d", ErrUnexpectedStatusCode, resp.StatusCode) + } +} + +func (c *Client) GetAccountConn(ctx context.Context, voiceID string) (*AccountConn, error) { + u, err := url.Parse(c.opts.BaseURL + "/" + c.opts.Version + "/account_connections") + if err != nil { + return nil, err + } + + options := []request.HTTPOption{ + request.WithBearer(c.opts.APIKey), + } + + req, err := request.NewHTTP(ctx, http.MethodGet, u.String(), nil, options...) + if err != nil { + return nil, err + } + q := req.URL.Query() + q.Add("id", voiceID) + req.URL.RawQuery = q.Encode() + + resp, err := request.Do[APIError](c.opts.HTTPClient, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + action := new(AccountConn) + if err := json.NewDecoder(resp.Body).Decode(action); err != nil { + return nil, err + } + return action, nil + case http.StatusForbidden: + var apiErr APIAuthError + if jsonErr := json.NewDecoder(resp.Body).Decode(&apiErr); jsonErr != nil { + return nil, errors.Join(err, jsonErr) + } + return nil, apiErr + case http.StatusTooManyRequests: + return nil, ErrTooManyRequests + case http.StatusUnprocessableEntity: + return nil, ErrUnprocessableEntity + default: + return nil, fmt.Errorf("%w: %d", ErrUnexpectedStatusCode, resp.StatusCode) + } +} + +func (c *Client) CreateAccountConn(ctx context.Context, createReq *CreateAccountConnReq) (*AccountConn, error) { + u, err := url.Parse(c.opts.BaseURL + "/" + c.opts.Version + "/account_connections/create") + if err != nil { + return nil, err + } + + var body = &bytes.Buffer{} + enc := json.NewEncoder(body) + enc.SetEscapeHTML(false) + if err := enc.Encode(createReq); err != nil { + return nil, err + } + + options := []request.HTTPOption{ + request.WithBearer(c.opts.APIKey), + } + + req, err := request.NewHTTP(ctx, http.MethodPost, u.String(), body, options...) + if err != nil { + return nil, err + } + + resp, err := request.Do[APIError](c.opts.HTTPClient, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + action := new(AccountConn) + if err := json.NewDecoder(resp.Body).Decode(action); err != nil { + return nil, err + } + return action, nil + case http.StatusForbidden: + var apiErr APIAuthError + if jsonErr := json.NewDecoder(resp.Body).Decode(&apiErr); jsonErr != nil { + return nil, errors.Join(err, jsonErr) + } + return nil, apiErr + case http.StatusTooManyRequests: + return nil, ErrTooManyRequests + case http.StatusUnprocessableEntity: + return nil, ErrUnprocessableEntity + default: + return nil, fmt.Errorf("%w: %d", ErrUnexpectedStatusCode, resp.StatusCode) + } +} + +func (c *Client) UpdateAccountConn(ctx context.Context, actionID string, updateReq *UpdateAccountConnReq) (*AccountConn, error) { + u, err := url.Parse(c.opts.BaseURL + "/" + c.opts.Version + "/account_connections/update") + if err != nil { + return nil, err + } + + var body = &bytes.Buffer{} + enc := json.NewEncoder(body) + enc.SetEscapeHTML(false) + if err := enc.Encode(updateReq); err != nil { + return nil, err + } + + options := []request.HTTPOption{ + request.WithBearer(c.opts.APIKey), + } + + req, err := request.NewHTTP(ctx, http.MethodPost, u.String(), body, options...) + if err != nil { + return nil, err + } + q := req.URL.Query() + q.Add("id", actionID) + req.URL.RawQuery = q.Encode() + + resp, err := request.Do[APIError](c.opts.HTTPClient, req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + action := new(AccountConn) + if err := json.NewDecoder(resp.Body).Decode(action); err != nil { + return nil, err + } + return action, nil + case http.StatusForbidden: + var apiErr APIAuthError + if jsonErr := json.NewDecoder(resp.Body).Decode(&apiErr); jsonErr != nil { + return nil, errors.Join(err, jsonErr) + } + return nil, apiErr + case http.StatusTooManyRequests: + return nil, ErrTooManyRequests + case http.StatusUnprocessableEntity: + return nil, ErrUnprocessableEntity + default: + return nil, fmt.Errorf("%w: %d", ErrUnexpectedStatusCode, resp.StatusCode) + } +} diff --git a/examples/account_conn/main.go b/examples/account_conn/main.go new file mode 100644 index 0000000..d19c1db --- /dev/null +++ b/examples/account_conn/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "flag" + "log" + "os" + + "github.com/milosgajdos/go-vocode" +) + +var ( + twillioAccountID string +) + +func init() { + flag.StringVar(&twillioAccountID, "twillio-account-id", "", "Twillio account ID") +} + +func main() { + client := vocode.NewClient() + ctx := context.Background() + + oaiAPIKey := os.Getenv("OPENAI_API_KEY") + if oaiAPIKey == "" { + log.Fatal("missing openai API key") + } + + twillioAuthToken := os.Getenv("TWILLIO_AUTH_TOKEN") + if twillioAuthToken == "" { + log.Fatal("missing twillio auth token") + } + + oaiConnReq := &vocode.CreateAccountConnReq{ + AccountConnReqBase: vocode.AccountConnReqBase{ + Type: vocode.OpenAIConnType, + OpenAIAccount: &vocode.OpenAIAccount{ + Creds: &vocode.OpenAICreds{ + APIKey: oaiAPIKey, + }, + }, + }, + } + + res, err := client.CreateAccountConn(ctx, oaiConnReq) + if err != nil { + log.Fatalf("failed creating openai account connection: %v", err) + } + log.Printf("created openai account connection: %v", res) + + twillioConnReq := &vocode.CreateAccountConnReq{ + AccountConnReqBase: vocode.AccountConnReqBase{ + Type: vocode.TwilioConnType, + TwilioAccount: &vocode.TwilioAccount{ + Creds: &vocode.TwilioCreds{ + AccountID: twillioAccountID, + AuthToken: twillioAuthToken, + }, + }, + }, + } + + res, err = client.CreateAccountConn(ctx, twillioConnReq) + if err != nil { + log.Fatalf("failed creating twilio account connection: %v", err) + } + log.Printf("created twillio account connection: %v", res) + + a, err := client.GetAccountConn(ctx, res.ID) + if err != nil { + log.Fatalf("failed getting account connection %s: %v", res.ID, err) + } + log.Printf("got account connection: %v", a.ID) + + accountConns, err := client.ListAccountConns(ctx, nil) + if err != nil { + log.Fatalf("failed listing account connections: %v", err) + } + log.Printf("got %d account connections: %#v", len(accountConns.Items), accountConns) +} diff --git a/examples/vector_db/main.go b/examples/vector_db/main.go index cff2569..37d3a90 100644 --- a/examples/vector_db/main.go +++ b/examples/vector_db/main.go @@ -23,8 +23,8 @@ func main() { client := vocode.NewClient() ctx := context.Background() - pcApiKey := os.Getenv("PINECONE_API_KEY") - if pcApiKey == "" { + pcAPIKey := os.Getenv("PINECONE_API_KEY") + if pcAPIKey == "" { log.Fatal("missing pinecone API key") } @@ -32,7 +32,7 @@ func main() { VectorDBReqBase: vocode.VectorDBReqBase{ Type: vocode.PineConeVectorDB, Index: pcIndex, - APIKey: pcApiKey, + APIKey: pcAPIKey, APIEnv: pcEnv, }, } diff --git a/models.go b/models.go index 8f7e009..a174c72 100644 --- a/models.go +++ b/models.go @@ -1,23 +1,5 @@ package vocode -type OpenAICreds struct { - APIKey string `json:"openai_api_key"` -} - -type AcctConnectionType string - -const ( - OpenaiConnType AcctConnectionType = "account_connection_openai" - TwilioConnType AcctConnectionType = "account_connection_twilio" -) - -type OpenAIAccount struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Type AcctConnectionType `json:"type"` - Creds *OpenAICreds `json:"credentials"` -} - type InterruptSenseType string const ( diff --git a/numbers.go b/numbers.go index 925f71f..1963bf6 100644 --- a/numbers.go +++ b/numbers.go @@ -38,12 +38,12 @@ type Numbers struct { } type TelAccount struct { - ID string `json:"id"` - UserID string `json:"user_id"` - Type AcctConnectionType `json:"type"` - Credentials map[string]any `json:"credentials"` - SteeringPool []string `json:"steering_pool"` - SupportAnyCaller bool `json:"account_supports_any_caller_id"` + ID string `json:"id"` + UserID string `json:"user_id"` + Type AccountConnType `json:"type"` + Credentials map[string]any `json:"credentials"` + SteeringPool []string `json:"steering_pool"` + SupportAnyCaller bool `json:"account_supports_any_caller_id"` } type Number struct { diff --git a/voices.go b/voices.go index d1978e0..29ab7da 100644 --- a/voices.go +++ b/voices.go @@ -180,21 +180,18 @@ func (v *Voice) UnmarshalJSON(data []byte) error { return err } v.AzureVoice = &azureVoice - case RimeVoiceType: var rimeVoice RimeVoice if err := json.Unmarshal(data, &rimeVoice); err != nil { return err } v.RimeVoice = &rimeVoice - case ElevenLabsVoiceType: var elevenLabsVoice ElevenLabsVoice if err := json.Unmarshal(data, &elevenLabsVoice); err != nil { return err } v.ElevenLabsVoice = &elevenLabsVoice - case PlayHtVoiceType: var playHtVoice PlayHtVoice if err := json.Unmarshal(data, &playHtVoice); err != nil {