190 lines
7.2 KiB
Go
190 lines
7.2 KiB
Go
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"
|
|
}
|