package ari import ( "context" "encoding/json" "fmt" "net/http" "net/url" ) type ActionClient interface { AnswerChannel(ctx context.Context, channelID string) error HangupChannel(ctx context.Context, channelID string) error GetChannel(ctx context.Context, channelID string) (*ARIChannel, error) } type HandoffActionClient interface { RedirectChannel(ctx context.Context, channelID string, endpoint string) error ContinueInDialplan(ctx context.Context, channelID, dialplanContext, extension string, priority int) error } type BridgeActionClient interface { CreateBridge(ctx context.Context, bridgeID string, bridgeType string) error AddChannelToBridge(ctx context.Context, bridgeID string, channelID string) error RemoveChannelFromBridge(ctx context.Context, bridgeID string, channelID string) error DeleteBridge(ctx context.Context, bridgeID string) error } type ExternalMediaClient interface { CreateExternalMediaChannel(ctx context.Context, req ExternalMediaRequest) (*ARIChannel, error) GetChannelVariable(ctx context.Context, channelID string, variable string) (string, error) } type MediaActionClient interface { ActionClient BridgeActionClient ExternalMediaClient } type ExternalMediaRequest struct { ChannelID string App string ExternalHost string Encapsulation string Transport string ConnectionType string Format string Direction string Data string } func (c *Client) AnswerChannel(ctx context.Context, channelID string) error { return c.doNoBody(ctx, http.MethodPost, "channels/"+url.PathEscape(channelID)+"/answer", successCodes(), false) } func (c *Client) HangupChannel(ctx context.Context, channelID string) error { return c.doNoBody(ctx, http.MethodDelete, "channels/"+url.PathEscape(channelID), cleanupCodes(), true) } func (c *Client) PlayChannel(ctx context.Context, channelID string, media string) error { q := url.Values{"media": {media}} return c.doNoBody(ctx, http.MethodPost, "channels/"+url.PathEscape(channelID)+"/play?"+q.Encode(), successCodesWithCreated(), false) } func (c *Client) RedirectChannel(ctx context.Context, channelID string, endpoint string) error { q := url.Values{"endpoint": {endpoint}} return c.doNoBody(ctx, http.MethodPost, "channels/"+url.PathEscape(channelID)+"/redirect?"+q.Encode(), successCodes(), false) } func (c *Client) ContinueInDialplan(ctx context.Context, channelID, dialplanContext, extension string, priority int) error { q := url.Values{"context": {dialplanContext}, "extension": {extension}, "priority": {fmt.Sprintf("%d", priority)}} return c.doNoBody(ctx, http.MethodPost, "channels/"+url.PathEscape(channelID)+"/continue?"+q.Encode(), successCodes(), false) } func (c *Client) GetChannel(ctx context.Context, channelID string) (*ARIChannel, error) { resp, err := c.authenticatedRequest(ctx, http.MethodGet, "channels/"+url.PathEscape(channelID)) if err != nil { return nil, err } defer drainAndClose(resp) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("get channel returned HTTP %d", resp.StatusCode) } var ch ARIChannel if err := json.NewDecoder(resp.Body).Decode(&ch); err != nil { return nil, err } return &ch, nil } func (c *Client) CreateBridge(ctx context.Context, bridgeID string, bridgeType string) error { q := url.Values{"type": {bridgeType}, "bridgeId": {bridgeID}} return c.doNoBody(ctx, http.MethodPost, "bridges?"+q.Encode(), successCodesWithCreated(), false) } func (c *Client) AddChannelToBridge(ctx context.Context, bridgeID string, channelID string) error { q := url.Values{"channel": {channelID}} return c.doNoBody(ctx, http.MethodPost, "bridges/"+url.PathEscape(bridgeID)+"/addChannel?"+q.Encode(), successCodes(), false) } func (c *Client) RemoveChannelFromBridge(ctx context.Context, bridgeID string, channelID string) error { q := url.Values{"channel": {channelID}} return c.doNoBody(ctx, http.MethodPost, "bridges/"+url.PathEscape(bridgeID)+"/removeChannel?"+q.Encode(), cleanupCodes(), true) } func (c *Client) DeleteBridge(ctx context.Context, bridgeID string) error { return c.doNoBody(ctx, http.MethodDelete, "bridges/"+url.PathEscape(bridgeID), cleanupCodes(), true) } func (c *Client) CreateExternalMediaChannel(ctx context.Context, req ExternalMediaRequest) (*ARIChannel, error) { ch, err := c.createExternalMediaChannel(ctx, req, true) if err == nil { return ch, nil } if isBadRequest(err) && req.Data != "" { return c.createExternalMediaChannel(ctx, req, false) } return nil, err } func (c *Client) createExternalMediaChannel(ctx context.Context, req ExternalMediaRequest, includeData bool) (*ARIChannel, error) { q := url.Values{} q.Set("app", req.App) q.Set("external_host", req.ExternalHost) q.Set("encapsulation", req.Encapsulation) q.Set("transport", req.Transport) q.Set("connection_type", req.ConnectionType) q.Set("format", req.Format) q.Set("direction", req.Direction) if req.ChannelID != "" { q.Set("channelId", req.ChannelID) } if includeData && req.Data != "" { q.Set("data", req.Data) } resp, err := c.authenticatedRequest(ctx, http.MethodPost, "channels/externalMedia?"+q.Encode()) if err != nil { return nil, err } defer drainAndClose(resp) if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted { return nil, fmt.Errorf("create external media returned HTTP %d", resp.StatusCode) } var ch ARIChannel if err := json.NewDecoder(resp.Body).Decode(&ch); err != nil { return nil, err } return &ch, nil } func (c *Client) GetChannelVariable(ctx context.Context, channelID string, variable string) (string, error) { q := url.Values{"variable": {variable}} resp, err := c.authenticatedRequest(ctx, http.MethodGet, "channels/"+url.PathEscape(channelID)+"/variable?"+q.Encode()) if err != nil { return "", err } defer drainAndClose(resp) if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("get channel variable returned HTTP %d", resp.StatusCode) } var payload struct { Value string `json:"value"` } if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { return "", err } if payload.Value == "" { return "", fmt.Errorf("channel variable %s is empty", variable) } return payload.Value, nil } func (c *Client) doNoBody(ctx context.Context, method, path string, ok map[int]bool, cleanup404 bool) error { resp, err := c.authenticatedRequest(ctx, method, path) if err != nil { return err } defer drainAndClose(resp) if ok[resp.StatusCode] { return nil } if cleanup404 && resp.StatusCode == http.StatusNotFound { return nil } return fmt.Errorf("ARI %s %s returned HTTP %d", method, path, resp.StatusCode) } func (c *Client) authenticatedRequest(ctx context.Context, method, path string) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, method, c.endpoint(path), nil) if err != nil { return nil, err } req.SetBasicAuth(c.user, c.password) return c.httpClient.Do(req) } func successCodes() map[int]bool { return map[int]bool{200: true, 202: true, 204: true} } func successCodesWithCreated() map[int]bool { return map[int]bool{200: true, 201: true, 202: true, 204: true} } func cleanupCodes() map[int]bool { return map[int]bool{200: true, 202: true, 204: true, 404: true} } func isBadRequest(err error) bool { return err != nil && err.Error() == "create external media returned HTTP 400" }