Files
ai-operator/internal/asterisk/ari/actions.go
T

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"
}