github.com/CyCoreSystems/ari@v4.8.4+incompatible/ext/play/options.go (about)

     1  package play
     2  
     3  import (
     4  	"container/list"
     5  	"strings"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/pkg/errors"
    10  )
    11  
    12  var (
    13  	// DefaultPlaybackStartTimeout is the default amount of time to wait for a playback to start before declaring that the playback has failed.
    14  	DefaultPlaybackStartTimeout = 2 * time.Second
    15  
    16  	// DefaultMaxPlaybackTime is the default maximum amount of time any playback is allowed to run.  If this time is exeeded, the playback will be cancelled.
    17  	DefaultMaxPlaybackTime = 10 * time.Minute
    18  
    19  	// DefaultFirstDigitTimeout is the default amount of time to wait, after the playback for all audio completes, for the first digit to be received.
    20  	DefaultFirstDigitTimeout = 4 * time.Second
    21  
    22  	// DefaultInterDigitTimeout is the maximum time to wait for additional
    23  	// digits after the first is received.
    24  	DefaultInterDigitTimeout = 3 * time.Second
    25  
    26  	// DefaultOverallDigitTimeout is the default maximum time to wait for a
    27  	// response, after the playback for all audio is complete, regardless of the
    28  	// number of received digits or pattern matching.
    29  	DefaultOverallDigitTimeout = 3 * time.Minute
    30  
    31  	// DigitBufferSize is the number of digits stored in the received-digit
    32  	// event buffer before further digit events are ignored.  NOTE that digits
    33  	// overflowing this buffer are still stored in the digits received buffer.
    34  	// This only affects the digit _signaling_ buffer.
    35  	DigitBufferSize = 20
    36  )
    37  
    38  // Result describes the result of a playback operation
    39  type Result struct {
    40  	mu sync.Mutex
    41  
    42  	// Duration indicates how long the playback execution took, from start to finish
    43  	Duration time.Duration
    44  
    45  	// DTMF records any DTMF which was received by the playback, as modified by any match functions
    46  	DTMF string
    47  
    48  	// Error indicates any error encountered which caused the termination of the playback
    49  	Error error
    50  
    51  	// MatchResult indicates the final result of any applied match function for DTMF digits which were received
    52  	MatchResult MatchResult
    53  
    54  	// Status indicates the resulting status of the playback, why it was stopped
    55  	Status Status
    56  }
    57  
    58  // Status indicates the final status of a playback, be it individual of an entire sequence.  This Status indicates the reason the playback stopped.
    59  type Status int
    60  
    61  const (
    62  	// InProgress indicates that the audio is currently playing or is staged to play
    63  	InProgress Status = iota
    64  
    65  	// Cancelled indicates that the audio was cancelled.  This cancellation could be due
    66  	// to anything from the control context being closed or a DTMF Match being found
    67  	Cancelled
    68  
    69  	// Failed indicates that the audio playback failed.  This indicates that one
    70  	// or more of the audio playbacks failed to be played.  This could be due to
    71  	// a system, network, or Asterisk error, but it could also be due to an
    72  	// invalid audio URI.  Check the returned error for more details.
    73  	Failed
    74  
    75  	// Finished indicates that the playback completed playing all bound audio
    76  	// URIs in full.  Note that for a prompt-style execution, this also means
    77  	// that no DTMF was matched to the match function.
    78  	Finished
    79  
    80  	// Hangup indicates that the audio playback was interrupted due to a hangup.
    81  	Hangup
    82  
    83  	// Timeout indicates that audio playback timed out.  It is not known whether this was due to a failure in the playback, a network loss, or some other problem.
    84  	Timeout
    85  )
    86  
    87  // MatchResult indicates the status of a match for the received DTMF of a playback
    88  type MatchResult int
    89  
    90  const (
    91  	// Incomplete indicates that there are not enough digits to determine a match
    92  	Incomplete MatchResult = iota
    93  
    94  	// Complete indicates that a match was found and the current DTMF pattern is complete
    95  	Complete
    96  
    97  	// Invalid indicates that a match cannot befound from the current DTMF received set
    98  	Invalid
    99  )
   100  
   101  type uriList struct {
   102  	list    *list.List
   103  	current *list.Element
   104  	mu      sync.Mutex
   105  }
   106  
   107  func (u *uriList) Empty() bool {
   108  	if u == nil || u.list == nil || u.list.Len() == 0 {
   109  		return true
   110  	}
   111  	return false
   112  }
   113  
   114  func (u *uriList) Add(uri string) {
   115  	u.mu.Lock()
   116  	defer u.mu.Unlock()
   117  
   118  	if u.list == nil {
   119  		u.list = list.New()
   120  	}
   121  
   122  	u.list.PushBack(uri)
   123  
   124  	if u.current == nil {
   125  		u.current = u.list.Front()
   126  	}
   127  }
   128  
   129  func (u *uriList) First() string {
   130  	if u.list == nil {
   131  		return ""
   132  	}
   133  
   134  	u.mu.Lock()
   135  	defer u.mu.Unlock()
   136  
   137  	u.current = u.list.Front()
   138  	return u.val()
   139  }
   140  
   141  func (u *uriList) Next() string {
   142  	if u.list == nil {
   143  		return ""
   144  	}
   145  
   146  	u.mu.Lock()
   147  	defer u.mu.Unlock()
   148  
   149  	if u.current == nil {
   150  		return ""
   151  	}
   152  
   153  	u.current = u.current.Next()
   154  	return u.val()
   155  }
   156  
   157  func (u *uriList) val() string {
   158  	if u.current == nil {
   159  		return ""
   160  	}
   161  
   162  	ret, ok := u.current.Value.(string)
   163  	if !ok {
   164  		return ""
   165  	}
   166  	return ret
   167  }
   168  
   169  // Options represent the various playback options which can modify the operation of a Playback.
   170  type Options struct {
   171  	// uriList is the list of audio URIs to play
   172  	uriList *uriList
   173  
   174  	// playbackStartTimeout defines the amount of time to wait for a playback to
   175  	// start before declaring it failed.
   176  	//
   177  	// This value is important because ARI does NOT report playback failures in
   178  	// any usable way.
   179  	//
   180  	// If not specified, the default is DefaultPlaybackStartTimeout
   181  	playbackStartTimeout time.Duration
   182  
   183  	// maxPlaybackTime is the maximum amount of time to wait for a playback
   184  	// session to complete, everything included.  The playback will be
   185  	// terminated if this time is exceeded.
   186  	maxPlaybackTime time.Duration
   187  
   188  	// firstDigitTimeout is the maximum length of time to wait
   189  	// after the prompt sequence ends for the user to enter
   190  	// a response.
   191  	//
   192  	// If not specified, the default is DefaultFirstDigitTimeout.
   193  	firstDigitTimeout time.Duration
   194  
   195  	// interDigitTimeout is the maximum length of time to wait
   196  	// for an additional digit after a digit is received.
   197  	//
   198  	// If not specified, the default is DefaultInterDigitTimeout.
   199  	interDigitTimeout time.Duration
   200  
   201  	// overallDigitTimeout is the maximum length of time to wait
   202  	// for a response regardless of digits received after the completion
   203  	// of all audio playbacks.
   204  	// If not specified, the default is DefaultOverallTimeout.
   205  	overallDigitTimeout time.Duration
   206  
   207  	// matchFunc is an optional function which, if supplied, returns
   208  	// a string and an int.
   209  	//
   210  	// The string is allows the MatchFunc to return a different number
   211  	// to be used as `result.Data`.  This is commonly used for prompts
   212  	// which look for a terminator.  In such a practice, the terminator
   213  	// would be stripped from the match and this argument would be populated
   214  	// with the result.  Otherwise, the original string should be returned.
   215  	// NOTE: Whatever is returned here will become `result.Data`.
   216  	//
   217  	// The int parameter indicates the result of the match, and it should
   218  	// be one of:
   219  	//  Incomplete (0) : insufficient digits to determine match.
   220  	//  Complete (1) : A match was found.
   221  	//  Invalid (2) : A match could not be found, given the digits received.
   222  	// If this function returns a non-zero int, then the prompt will be stopped.
   223  	// If not specified MatchAny will be used.
   224  	matchFunc func(string) (string, MatchResult)
   225  
   226  	// maxReplays is the maximum number of times the audio sequence will be
   227  	// replayed if there is no response.  By default, the audio sequence is
   228  	// played only once.
   229  	maxReplays int
   230  }
   231  
   232  // NewDefaultOptions returns a set of options which represent reasonable defaults for most simple playbacks.
   233  func NewDefaultOptions() *Options {
   234  	opts := &Options{
   235  		playbackStartTimeout: DefaultPlaybackStartTimeout,
   236  		maxPlaybackTime:      DefaultMaxPlaybackTime,
   237  		uriList:              new(uriList),
   238  	}
   239  
   240  	MatchAny()(opts) // nolint  No error is possible with MatchAny
   241  
   242  	return opts
   243  }
   244  
   245  // ApplyOptions applies a set of OptionFuncs to the Playback
   246  func (o *Options) ApplyOptions(opts ...OptionFunc) (err error) {
   247  	for _, f := range opts {
   248  		err = f(o)
   249  		if err != nil {
   250  			return errors.Wrap(err, "failed to apply option")
   251  		}
   252  	}
   253  	return nil
   254  }
   255  
   256  // NewPromptOptions returns a set of options which represent reasonable defaults for most prompt playbacks.  It will terminate when any single DTMF digit is received.
   257  func NewPromptOptions() *Options {
   258  	opts := NewDefaultOptions()
   259  
   260  	opts.firstDigitTimeout = DefaultFirstDigitTimeout
   261  	opts.interDigitTimeout = DefaultInterDigitTimeout
   262  	opts.overallDigitTimeout = DefaultOverallDigitTimeout
   263  
   264  	return opts
   265  }
   266  
   267  // OptionFunc defines an interface for functions which can modify a play session's Options
   268  type OptionFunc func(*Options) error
   269  
   270  // NoExitOnDTMF disables exiting the playback when DTMF is received.  Note that
   271  // this is just a wrapper for MatchFunc(nil), so it is mutually exclusive with
   272  // MatchFunc; whichever comes later will win.
   273  func NoExitOnDTMF() OptionFunc {
   274  	return func(o *Options) error {
   275  		o.matchFunc = nil
   276  		return nil
   277  	}
   278  }
   279  
   280  // URI adds a set of audio URIs to a playback
   281  func URI(uri ...string) OptionFunc {
   282  	return func(o *Options) error {
   283  		if o.uriList == nil {
   284  			o.uriList = new(uriList)
   285  		}
   286  
   287  		for _, u := range uri {
   288  			if u != "" {
   289  				o.uriList.Add(u)
   290  			}
   291  		}
   292  		return nil
   293  	}
   294  }
   295  
   296  // PlaybackStartTimeout overrides the default playback start timeout
   297  func PlaybackStartTimeout(timeout time.Duration) OptionFunc {
   298  	return func(o *Options) error {
   299  		o.playbackStartTimeout = timeout
   300  		return nil
   301  	}
   302  }
   303  
   304  // DigitTimeouts sets the digit timeouts.  Passing a negative value to any of these indicates that the default value (shown in parentheses below) should be used.
   305  //
   306  //  - First digit timeout (4 sec):  The time (after the stop of the audio) to wait for the first digit to be received
   307  //
   308  //  - Inter digit timeout (3 sec):  The time (after receiving a digit) to wait for the _next_ digit to be received
   309  //
   310  //  - Overall digit timeout (3 min):  The maximum amount of time to wait (after the stop of the audio) for digits to be received, regardless of the digit frequency
   311  //
   312  func DigitTimeouts(first, inter, overall time.Duration) OptionFunc {
   313  	return func(o *Options) error {
   314  		if first >= 0 {
   315  			o.firstDigitTimeout = first
   316  		}
   317  		if inter >= 0 {
   318  			o.interDigitTimeout = inter
   319  		}
   320  		if overall >= 0 {
   321  			o.overallDigitTimeout = overall
   322  		}
   323  		return nil
   324  	}
   325  }
   326  
   327  // Replays sets the number of replays of the audio sequence before exiting
   328  func Replays(count int) OptionFunc {
   329  	return func(o *Options) error {
   330  		o.maxReplays = count
   331  		return nil
   332  	}
   333  }
   334  
   335  // MatchAny indicates that the playback should be considered Matched and terminated if
   336  // any DTMF digit is received during the playback or post-playback time.
   337  func MatchAny() OptionFunc {
   338  	return func(o *Options) error {
   339  		o.matchFunc = func(pat string) (string, MatchResult) {
   340  			if len(pat) > 0 {
   341  				return pat, Complete
   342  			}
   343  			return pat, Incomplete
   344  		}
   345  		return nil
   346  	}
   347  }
   348  
   349  // MatchNone indicates that the playback should never be terminated due to DTMF
   350  func MatchNone() OptionFunc {
   351  	return func(o *Options) error {
   352  		o.matchFunc = nil
   353  		return nil
   354  	}
   355  }
   356  
   357  // MatchDiscrete indicates that the playback should be considered Matched and terminated if
   358  // the received DTMF digits match any of the discrete list of strings.
   359  func MatchDiscrete(list []string) OptionFunc {
   360  	return func(o *Options) error {
   361  		o.matchFunc = func(pat string) (string, MatchResult) {
   362  			var maxLen int
   363  			for _, t := range list {
   364  				if t == pat {
   365  					return pat, Complete
   366  				}
   367  				if len(t) > maxLen {
   368  					maxLen = len(t)
   369  				}
   370  			}
   371  			if len(pat) > maxLen {
   372  				return pat, Invalid
   373  			}
   374  			return pat, Incomplete
   375  		}
   376  		return nil
   377  	}
   378  }
   379  
   380  // MatchHash indicates that the playback should be considered Matched and terminated if it contains a hash (#).  The hash (and any subsequent digits) is removed from the final result.
   381  func MatchHash() OptionFunc {
   382  	return func(o *Options) error {
   383  		o.matchFunc = func(pat string) (string, MatchResult) {
   384  			if strings.Contains(pat, "#") {
   385  				return strings.Split(pat, "#")[0], Complete
   386  			}
   387  			return pat, Incomplete
   388  		}
   389  		return nil
   390  	}
   391  }
   392  
   393  // MatchTerminator indicates that the playback shoiuld be considered Matched and terminated if it contains the provided Terminator string.  The terminator (and any subsequent digits) is removed from the final result.
   394  func MatchTerminator(terminator string) OptionFunc {
   395  	return func(o *Options) error {
   396  		o.matchFunc = func(pat string) (string, MatchResult) {
   397  			if strings.Contains(pat, terminator) {
   398  				return strings.Split(pat, terminator)[0], Complete
   399  			}
   400  			return pat, Incomplete
   401  		}
   402  		return nil
   403  	}
   404  }
   405  
   406  // MatchLen indicates that the playback should be considered Matched and terminated if the given number of DTMF digits are receieved.
   407  func MatchLen(length int) OptionFunc {
   408  	return func(o *Options) error {
   409  		o.matchFunc = func(pat string) (string, MatchResult) {
   410  			if len(pat) >= length {
   411  				return pat, Complete
   412  			}
   413  			return pat, Incomplete
   414  		}
   415  		return nil
   416  	}
   417  }
   418  
   419  // MatchLenOrTerminator indicates that the playback should be considered Matched and terminated if the given number of DTMF digits are receieved or if the given terminator is received.  If the terminator is present, it and any subsequent digits will be removed from the final result.
   420  func MatchLenOrTerminator(length int, terminator string) OptionFunc {
   421  	return func(o *Options) error {
   422  		o.matchFunc = func(pat string) (string, MatchResult) {
   423  			if len(pat) >= length {
   424  				return pat, Complete
   425  			}
   426  			if strings.Contains(pat, terminator) {
   427  				return strings.Split(pat, terminator)[0], Complete
   428  			}
   429  			return pat, Incomplete
   430  		}
   431  		return nil
   432  	}
   433  }
   434  
   435  // MatchFunc uses the provided match function to determine when the playback should be terminated based on DTMF input.
   436  func MatchFunc(f func(string) (string, MatchResult)) OptionFunc {
   437  	return func(o *Options) error {
   438  		o.matchFunc = f
   439  		return nil
   440  	}
   441  }