├── internal ├── eventgen │ ├── .gitignore │ ├── template.tmpl │ └── main.go └── testutils │ ├── doc.go │ ├── player.go │ └── recorder.go ├── _examples ├── .gitignore ├── script │ ├── sendDtmf.sh │ └── stasisStart.sh ├── stasisStart │ ├── Dockerfile │ ├── docker-compose.yml │ └── main.go ├── infra │ ├── asterisk │ │ ├── http.conf │ │ ├── extensions.conf │ │ ├── ari.conf │ │ └── Dockerfile │ └── natsgw │ │ └── Dockerfile ├── twoapps │ ├── twoapps │ └── main.go ├── helloworld │ └── main.go ├── Makefile ├── play │ ├── README.md │ └── main.go ├── record │ └── main.go └── bridge │ └── main.go ├── doc.go ├── ext ├── play │ ├── doc.go │ ├── play.go │ ├── sequence.go │ └── example_test.go ├── bridgemon │ ├── README.md │ └── bridgemon.go ├── keyfilter │ └── keyfilter.go └── audiouri │ └── uri.go ├── message.go ├── .envrc ├── .mockery.yaml ├── dtmf.go ├── .gitignore ├── .mailmap ├── bus_test.go ├── .golangci.yml ├── callerid.go ├── direction.go ├── go.mod ├── .github └── workflows │ └── go.yml ├── client ├── native │ ├── error.go │ ├── textMessage.go │ ├── sound.go │ ├── playback.go │ ├── config.go │ ├── mailbox.go │ ├── device.go │ ├── modules.go │ ├── endpoint.go │ ├── asterisk.go │ ├── application.go │ ├── logging.go │ ├── storedRecording.go │ ├── liveRecording.go │ └── request.go └── arimocks │ ├── Sender.go │ ├── Matcher.go │ ├── DTMFSender.go │ ├── Subscriber.go │ ├── Subscription.go │ ├── AsteriskVariables.go │ ├── Sound.go │ ├── Bus.go │ └── TextMessage.go ├── sound.go ├── textMessage.go ├── CONTRIBUTORS ├── Makefile ├── rid └── rid.go ├── flake.lock ├── client.go ├── modules.go ├── datetime.go ├── context.go ├── device.go ├── recording.go ├── mailbox.go ├── bus.go ├── logging.go ├── config.go ├── asterisk.go ├── flake.nix ├── key_test.go ├── ari.proto ├── storedRecording.go ├── CODE_OF_CONDUCT.md ├── application.go ├── endpoint.go ├── playback.go ├── stdbus ├── bus_test.go └── bus.go ├── datetime_test.go ├── originate.go ├── liveRecording.go ├── go.sum └── key.go /internal/eventgen/.gitignore: -------------------------------------------------------------------------------- 1 | /eventgen 2 | -------------------------------------------------------------------------------- /internal/testutils/doc.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | -------------------------------------------------------------------------------- /_examples/.gitignore: -------------------------------------------------------------------------------- 1 | docker-compose 2 | app.static 3 | -------------------------------------------------------------------------------- /_examples/script/sendDtmf.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl http://127.0.0.1:9991/ 4 | -------------------------------------------------------------------------------- /_examples/script/stasisStart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | curl http://127.0.0.1:9990/ 4 | -------------------------------------------------------------------------------- /_examples/stasisStart/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ulexus/go-minimal 2 | COPY app.static /app 3 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package ari provides a Go library for interacting with Asterisk ARI 2 | package ari 3 | -------------------------------------------------------------------------------- /_examples/infra/asterisk/http.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | enabled=yes 3 | bindaddr=0.0.0.0 4 | bindport=8088 5 | -------------------------------------------------------------------------------- /_examples/twoapps/twoapps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CyCoreSystems/ari/HEAD/_examples/twoapps/twoapps -------------------------------------------------------------------------------- /ext/play/doc.go: -------------------------------------------------------------------------------- 1 | // Package play provides a set of tools for feature-rich audio playbacks and IVR primitives. 2 | package play 3 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // Message is the first extension of the RawMessage type, 4 | // containing only a Type 5 | type Message struct { 6 | Type string `json:"type"` 7 | } 8 | -------------------------------------------------------------------------------- /_examples/infra/asterisk/extensions.conf: -------------------------------------------------------------------------------- 1 | [default] 2 | 3 | exten => 1000,1,NoOp() 4 | same => n,Stasis(example) 5 | same => n,Hangup() 6 | 7 | exten => 1001,1,Hangup() 8 | 9 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | if ! has nix_direnv_version || ! nix_direnv_version 2.2.0; then 2 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.0/direnvrc" "sha256-5EwyKnkJNQeXrRkYbwwRBcXbibosCJqyIUuz9Xq+LRc=" 3 | fi 4 | use flake 5 | -------------------------------------------------------------------------------- /_examples/infra/asterisk/ari.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | enabled = yes 3 | allowed_origins = http://localhost/,localhost,localhost:8088,http://ari.asterisk.org 4 | 5 | [admin] 6 | type = user 7 | password = admin 8 | password_format = plain 9 | 10 | -------------------------------------------------------------------------------- /_examples/stasisStart/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | asterisk: 5 | build: ../asterisk 6 | ports: 7 | - "8088:8088" 8 | 9 | stasisstart: 10 | build: ./ 11 | links: 12 | - asterisk 13 | ports: 14 | - "9990:9990" 15 | -------------------------------------------------------------------------------- /.mockery.yaml: -------------------------------------------------------------------------------- 1 | all: false 2 | dir: "client/arimocks" 3 | filename: "{{.InterfaceName}}.go" 4 | structname: "{{.InterfaceName}}" 5 | pkgname: "arimocks" 6 | template-data: 7 | unroll-variadic: true 8 | packages: 9 | github.com/CyCoreSystems/ari/v6: 10 | config: 11 | all: true 12 | -------------------------------------------------------------------------------- /_examples/infra/natsgw/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ulexus/go-minimal 2 | COPY app.static /app 3 | CMD ["--ari.application","example", \ 4 | "--ari.username","admin", \ 5 | "--ari.password","admin", \ 6 | "--ari.websocket_url","ws://asterisk:8088/ari/events", \ 7 | "--ari.http_url","http://asterisk:8088/ari", \ 8 | "--nats.url","nats://nats:4222", \ 9 | "-v"] 10 | -------------------------------------------------------------------------------- /dtmf.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import "time" 4 | 5 | // DTMFOptions is the list of pptions for DTMF sending 6 | type DTMFOptions struct { 7 | Before time.Duration 8 | Between time.Duration 9 | Duration time.Duration 10 | After time.Duration 11 | } 12 | 13 | // DTMFSender is an object which can be send DTMF signals 14 | type DTMFSender interface { 15 | SendDTMF(dtmf string, opts *DTMFOptions) 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | bin 10 | vendor/ 11 | /.direnv/ 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | 29 | *.swp 30 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Seán C McCord Seán C. McCord Ulexus 2 | Seán C McCord Ulexus 3 | Sheena Artrip sheenobu 4 | Sharon Allsup system_user 5 | Laurel Lawson llccs 6 | Torrey Searle tsearle 7 | Torrey Searle tsearle 8 | Michael Hall mikehall76 9 | -------------------------------------------------------------------------------- /bus_test.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import "testing" 4 | 5 | func TestNullSubscription(t *testing.T) { 6 | sub := NewNullSubscription() 7 | 8 | select { 9 | case <-sub.Events(): 10 | t.Error("received event from NullSubscription") 11 | default: 12 | } 13 | 14 | sub.Cancel() 15 | 16 | select { 17 | case <-sub.Events(): 18 | default: 19 | t.Error("NullSubscription failed to close") 20 | } 21 | 22 | // Make sure subsequent Cancel doesn't break 23 | sub.Cancel() 24 | 25 | select { 26 | case <-sub.Events(): 27 | default: 28 | t.Error("NullSubscription failed to close") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /_examples/infra/asterisk/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:8 2 | 3 | RUN apt-get update 4 | RUN apt-get install -y build-essential openssl libxml2-dev libncurses5-dev uuid-dev sqlite3 libsqlite3-dev pkg-config curl libjansson-dev 5 | 6 | RUN curl -s http://downloads.asterisk.org/pub/telephony/asterisk/releases/asterisk-14.0.0-rc1.tar.gz | tar xz 7 | 8 | WORKDIR /asterisk-14.0.0-rc1 9 | RUN ./configure; make; make install; make samples 10 | 11 | COPY http.conf /etc/asterisk/http.conf 12 | COPY ari.conf /etc/asterisk/ari.conf 13 | COPY extensions.conf /etc/asterisk/extensions.conf 14 | 15 | CMD ["/usr/sbin/asterisk", "-f"] 16 | 17 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - wsl_v5 5 | settings: 6 | wsl_v5: 7 | allow-first-in-block: true 8 | allow-whole-block: false 9 | branch-max-lines: 2 10 | exclusions: 11 | generated: lax 12 | presets: 13 | - comments 14 | - common-false-positives 15 | - legacy 16 | - std-error-handling 17 | paths: 18 | - third_party$ 19 | - builtin$ 20 | - examples$ 21 | - events_gen.go 22 | formatters: 23 | exclusions: 24 | generated: lax 25 | paths: 26 | - third_party$ 27 | - builtin$ 28 | - examples$ 29 | -------------------------------------------------------------------------------- /callerid.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import "errors" 4 | 5 | // NOTE: Direct translation from ARI client 2.0 6 | 7 | // CallerIDFromString interprets the provided string 8 | // as a CallerID. Usually, this string will be of the following forms: 9 | // - "Name" 10 | // - 11 | // - "Name" number 12 | func CallerIDFromString(src string) (*CallerID, error) { 13 | // TODO: implement complete callerid parser 14 | return nil, errors.New("CallerIDFromString not yet implemented") 15 | } 16 | 17 | // String returns the stringified callerid 18 | func (cid *CallerID) String() string { 19 | return cid.Name + "<" + cid.Number + ">" 20 | } 21 | -------------------------------------------------------------------------------- /direction.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // Direction describes an audio direction, as used by Mute, Snoop, and possibly others. Valid values are "in", "out", and "both". 4 | type Direction string 5 | 6 | const ( 7 | // DirectionNone indicates audio should not flow in any direction 8 | DirectionNone Direction = "none" 9 | 10 | // DirectionIn indicates the direction flowing from the channel into Asterisk 11 | DirectionIn Direction = "in" 12 | 13 | // DirectionOut indicates the direction flowing from Asterisk to the channel 14 | DirectionOut Direction = "out" 15 | 16 | // DirectionBoth indicates both the directions flowing both inward to Asterisk and outward from Asterisk. 17 | DirectionBoth Direction = "both" 18 | ) 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/CyCoreSystems/ari/v6 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.4 6 | 7 | require ( 8 | github.com/gogo/protobuf v1.3.2 9 | github.com/oklog/ulid v1.3.1 10 | github.com/rotisserie/eris v0.5.4 11 | github.com/stretchr/testify v1.10.0 12 | golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 13 | golang.org/x/net v0.42.0 14 | golang.org/x/text v0.27.0 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 19 | github.com/kr/pretty v0.3.1 // indirect 20 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 21 | github.com/stretchr/objx v0.5.2 // indirect 22 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.21 20 | 21 | - name: lint 22 | uses: golangci/golangci-lint-action@v3.7.0 23 | with: 24 | args: ./... 25 | 26 | - name: Build API 27 | run: go build -v ./ ./stdbus ./rid 28 | 29 | - name: Build Clients 30 | run: go build -v ./client/native ./client/arimocks 31 | 32 | - name: Build Extensions 33 | run: go build -v ./ext/... 34 | 35 | - name: Test 36 | run: go test -v ./... 37 | -------------------------------------------------------------------------------- /ext/bridgemon/README.md: -------------------------------------------------------------------------------- 1 | # bridgemon 2 | 3 | [![](https://godoc.org/github.com/CyCoreSystems/ari?status.svg)](http://godoc.org/github.com/CyCoreSystems/ari) 4 | 5 | Bridge Monitor provides a simple tool to monitor and cache a bridge's data for 6 | easy, efficient access by other routines. It is safe for multi-threaded use and 7 | can be closed manually or whenever the bridge is destroyed. 8 | 9 | It is created by passing a bridge handle in. The bridge should already exist 10 | for this to be operational, and initial data is loaded when the monitor is 11 | created. 12 | 13 | There are two method for consuming the data. `Data()` provides arbitrary access 14 | to the cached bridge data while `Watch()` provides a channel over which the 15 | bridge data will be sent whenever updates are made. 16 | 17 | -------------------------------------------------------------------------------- /client/native/error.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rotisserie/eris" 7 | ) 8 | 9 | type errDataGet struct { 10 | c error 11 | entityType string 12 | entityIDfmt string 13 | entityIDctx []interface{} 14 | } 15 | 16 | func dataGetError(cause error, typ string, idfmt string, ctx ...interface{}) error { 17 | if cause == nil { 18 | return nil 19 | } 20 | 21 | return eris.Wrap(&errDataGet{ 22 | c: cause, 23 | entityType: typ, 24 | entityIDfmt: idfmt, 25 | entityIDctx: ctx, 26 | }, "failed to get data") 27 | } 28 | 29 | func (e *errDataGet) Error() string { 30 | id := fmt.Sprintf(e.entityIDfmt, e.entityIDctx...) 31 | return fmt.Sprintf("Error getting data for %v '%v': %v", e.entityType, id, e.c.Error()) 32 | } 33 | 34 | func (e *errDataGet) Cause() error { 35 | return e.c 36 | } 37 | -------------------------------------------------------------------------------- /sound.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // Sound represents a communication path to 4 | // the asterisk server for Sound resources 5 | type Sound interface { 6 | // List returns available sounds limited by the provided filters. 7 | // Valid filters are "lang", "format", and nil (no filter) 8 | List(filters map[string]string, keyFilter *Key) ([]*Key, error) 9 | 10 | // Data returns the Sound's data 11 | Data(key *Key) (*SoundData, error) 12 | } 13 | 14 | // SoundData describes a media file which may be played back 15 | type SoundData struct { 16 | // Key is the cluster-unique identifier for this sound 17 | Key *Key `json:"key"` 18 | 19 | Formats []FormatLangPair `json:"formats"` 20 | ID string `json:"id"` 21 | Text string `json:"text,omitempty"` 22 | } 23 | 24 | // FormatLangPair describes the format and language of a sound file 25 | type FormatLangPair struct { 26 | Format string `json:"format"` 27 | Language string `json:"language"` 28 | } 29 | -------------------------------------------------------------------------------- /_examples/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "golang.org/x/exp/slog" 7 | 8 | "github.com/CyCoreSystems/ari/v6/client/native" 9 | ) 10 | 11 | func main() { 12 | // OPTIONAL: setup logging 13 | log := slog.New(slog.NewTextHandler(os.Stderr, nil)) 14 | 15 | log.Info("Connecting") 16 | 17 | cl, err := native.Connect(&native.Options{ 18 | Application: "example", 19 | Logger: log.With("app", "example"), 20 | Username: "admin", 21 | Password: "admin", 22 | URL: "http://localhost:8088/ari", 23 | WebsocketURL: "ws://localhost:8088/ari/events", 24 | }) 25 | if err != nil { 26 | log.Error("Failed to build native ARI client", "error", err) 27 | return 28 | } 29 | 30 | defer cl.Close() 31 | 32 | log.Info("Connected") 33 | 34 | info, err := cl.Asterisk().Info(nil) 35 | if err != nil { 36 | log.Error("Failed to get Asterisk Info", "error", err) 37 | return 38 | } 39 | 40 | log.Info("Asterisk Info", "info", info) 41 | } 42 | -------------------------------------------------------------------------------- /internal/testutils/player.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import "github.com/CyCoreSystems/ari/v6" 4 | 5 | // A PlayerPair is the pair of results returned from a mock Play request 6 | type PlayerPair struct { 7 | Handle *ari.PlaybackHandle 8 | Err error 9 | } 10 | 11 | // Player is the test player that can be primed with sample data 12 | type Player struct { 13 | Next chan struct{} 14 | results []PlayerPair 15 | } 16 | 17 | // NewPlayer creates a new player 18 | func NewPlayer() *Player { 19 | return &Player{ 20 | Next: make(chan struct{}, 10), 21 | } 22 | } 23 | 24 | // Append appends the given Play results 25 | func (p *Player) Append(h *ari.PlaybackHandle, err error) { 26 | p.results = append(p.results, PlayerPair{h, err}) 27 | } 28 | 29 | // Play pops the top results and returns them, as well as triggering player.Next 30 | func (p *Player) Play(mediaURI string) (h *ari.PlaybackHandle, err error) { 31 | h = p.results[0].Handle 32 | err = p.results[0].Err 33 | p.results = p.results[1:] 34 | 35 | p.Next <- struct{}{} 36 | 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /textMessage.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // TextMessage needs some verbiage here 4 | type TextMessage interface { 5 | // Send() sends a text message to an endpoint 6 | Send(from, tech, resource, body string, vars map[string]string) error 7 | 8 | // SendByURI sends a text message to an endpoint by free-form URI 9 | SendByURI(from, to, body string, vars map[string]string) error 10 | } 11 | 12 | // TextMessageData describes text message 13 | type TextMessageData struct { 14 | // Key is the cluster-unique identifier for this text message 15 | Key *Key `json:"key"` 16 | 17 | Body string `json:"body"` // The body (text) of the message 18 | From string `json:"from"` // Technology-specific source URI 19 | To string `json:"to"` // Technology-specific destination URI 20 | Variables []TextMessageVariable `json:"variables,omitempty"` 21 | } 22 | 23 | // TextMessageVariable describes a key-value pair (associated with a text message) 24 | type TextMessageVariable struct { 25 | Key string `json:"key"` 26 | Value string `json:"value"` 27 | } 28 | -------------------------------------------------------------------------------- /internal/testutils/recorder.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import "github.com/CyCoreSystems/ari/v6" 4 | 5 | // A RecorderPair is the pair of results returned from a mock Record request 6 | type RecorderPair struct { 7 | Handle *ari.LiveRecordingHandle 8 | Err error 9 | } 10 | 11 | // Recorder is the test player that can be primed with sample data 12 | type Recorder struct { 13 | Next chan struct{} 14 | results []RecorderPair 15 | } 16 | 17 | // NewRecorder creates a new player 18 | func NewRecorder() *Recorder { 19 | return &Recorder{ 20 | Next: make(chan struct{}, 10), 21 | } 22 | } 23 | 24 | // Append appends the given Play results 25 | func (r *Recorder) Append(h *ari.LiveRecordingHandle, err error) { 26 | r.results = append(r.results, RecorderPair{h, err}) 27 | } 28 | 29 | // Record pops the top results and returns them, as well as triggering player.Next 30 | func (r *Recorder) Record(name string, opts *ari.RecordingOptions) (h *ari.LiveRecordingHandle, err error) { 31 | h = r.results[0].Handle 32 | err = r.results[0].Err 33 | r.results = r.results[1:] 34 | 35 | r.Next <- struct{}{} 36 | 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | 0perl <444065+0perl@users.noreply.github.com> 2 | Ashutosh Chaudhary <216.ashutosh@gmail.com> 3 | Dan Cropp <58079560+daninmadison@users.noreply.github.com> 4 | Danil Matyukhin 5 | JAF 6 | Laurel Lawson 7 | Michael Hall 8 | Mike Zimin 9 | Nuno Pereira 10 | Raian Fernandes Dos Santos 11 | Sasivarunan Kulandaivel 12 | Seán C McCord 13 | Sharon Allsup 14 | Sheena Artrip 15 | Stamkulov Sattar 16 | Torrey Searle 17 | Vipul 18 | William Edward Lee 19 | goharahmed 20 | mtryfoss 21 | realrainer <18657361+realrainer@users.noreply.github.com> 22 | seanchann 23 | serfreeman1337 24 | virgilio-a-cunha-alb 25 | Зимин Михаил Дмитриевич 26 | -------------------------------------------------------------------------------- /_examples/Makefile: -------------------------------------------------------------------------------- 1 | 2 | stasisStart-nats: .PHONY 3 | CGO_ENABLED=0 GOOS=linux \ 4 | go build -a -installsuffix cgo \ 5 | -ldflags '-w -extld ld -extldflags -static' \ 6 | -a -o stasisStart-nats/app.static stasisStart-nats/main.go 7 | docker-compose -f stasisStart-nats/docker-compose.yml build 8 | docker-compose -f stasisStart-nats/docker-compose.yml up 9 | 10 | sendDtmf-nats: .PHONY 11 | CGO_ENABLED=0 GOOS=linux \ 12 | go build -a -installsuffix cgo \ 13 | -ldflags '-w -extld ld -extldflags -static' \ 14 | -a -o sendDtmf-nats/app.static sendDtmf-nats/main.go 15 | docker-compose -f sendDtmf-nats/docker-compose.yml build 16 | docker-compose -f sendDtmf-nats/docker-compose.yml up 17 | 18 | stasisStart: .PHONY 19 | CGO_ENABLED=0 GOOS=linux \ 20 | go build -a -installsuffix cgo \ 21 | -ldflags '-w -extld ld -extldflags -static' \ 22 | -a -o stasisStart/app.static stasisStart/main.go 23 | docker-compose -f stasisStart/docker-compose.yml build 24 | docker-compose -f stasisStart/docker-compose.yml up 25 | 26 | natsgw: .PHONY 27 | CGO_ENABLED=0 GOOS=linux \ 28 | go build -a -installsuffix cgo \ 29 | -ldflags '-w -extld ld -extldflags -static' \ 30 | -a -o infra/natsgw/app.static ../cmd/ari-natsgw 31 | 32 | 33 | .PHONY: 34 | 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO111MODULE = on 2 | 3 | SHELL = /usr/bin/env bash 4 | 5 | EVENT_SPEC_FILE = internal/eventgen/json/events-2.0.0.json 6 | 7 | all: dep check api clients contributors extensions test 8 | 9 | ci: check api clients extensions test 10 | 11 | contributors: 12 | write_mailmap > CONTRIBUTORS 13 | 14 | protobuf: ari.proto 15 | protoc -I. -I./vendor -I$(GOPATH)/src --gogofast_out=Mgoogle/protobuf/timestamp.proto=github.com/gogo/protobuf/types,plugins=grpc:. ari.proto 16 | 17 | dep: 18 | go mod tidy 19 | 20 | api: 21 | go build ./ 22 | go build ./stdbus 23 | go build ./rid 24 | 25 | test: 26 | go test `go list ./... | grep -v /vendor/` 27 | 28 | check: 29 | go mod verify 30 | golangci-lint run 31 | #gometalinter --disable=gotype client/native ext/... 32 | 33 | clients: 34 | go build ./client/native 35 | go build ./client/arimocks 36 | 37 | extensions: 38 | go build ./ext/audiouri 39 | go build ./ext/bridgemon 40 | go build ./ext/keyfilter 41 | go build ./ext/play 42 | go build ./ext/record 43 | 44 | events: 45 | go build -o bin/eventgen ./internal/eventgen/... 46 | @./bin/eventgen internal/eventgen/template.tmpl ${EVENT_SPEC_FILE} |goimports > events_gen.go 47 | 48 | mock: 49 | go install github.com/vektra/mockery/v3@latest 50 | rm -Rf client/arimocks 51 | mockery 52 | 53 | -------------------------------------------------------------------------------- /rid/rid.go: -------------------------------------------------------------------------------- 1 | // Package rid provides unique resource IDs 2 | package rid 3 | 4 | import ( 5 | "crypto/rand" 6 | "strings" 7 | "time" 8 | 9 | "github.com/oklog/ulid" 10 | ) 11 | 12 | const ( 13 | // Bridge indicates the resource ID is of a bridge 14 | Bridge = "br" 15 | 16 | // Channel indicates the resource ID is of a channel 17 | Channel = "ch" 18 | 19 | // Playback indicates the resource ID is of a playback 20 | Playback = "pb" 21 | 22 | // Recording indicates the resource ID is for a recording 23 | Recording = "rc" 24 | 25 | // Snoop indicates the resource ID is for a snoop session 26 | Snoop = "sn" 27 | ) 28 | 29 | // New returns a new generic resource ID 30 | func New(kind string) string { 31 | id := strings.ToLower(ulid.MustNew(ulid.Now(), rand.Reader).String()) 32 | 33 | if kind != "" { 34 | if len(kind) > 2 { 35 | kind = kind[:2] 36 | } 37 | 38 | id += "-" + kind 39 | } 40 | 41 | return id 42 | } 43 | 44 | // Timestamp returns the timestamp stored within the resource ID 45 | func Timestamp(id string) (ts time.Time, err error) { 46 | idx := strings.Index(id, "-") 47 | if idx > 0 { 48 | id = id[:idx] 49 | } 50 | 51 | uid, err := ulid.Parse(id) 52 | if err != nil { 53 | return 54 | } 55 | 56 | ms := int64(uid.Time()) 57 | ts = time.Unix(ms/1000, (ms%1000)*1000000) 58 | 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /client/native/textMessage.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import "net/url" 4 | 5 | // TextMessage provides the ARI TextMessage accessors for the native client 6 | type TextMessage struct { 7 | client *Client 8 | } 9 | 10 | // Send sends a text message to an endpoint 11 | func (t *TextMessage) Send(from, tech, resource, body string, vars map[string]string) error { 12 | // Construct querystring values 13 | v := url.Values{} 14 | v.Set("from", from) 15 | v.Set("body", body) 16 | 17 | // vars must not be nil, or Ari will reject the request 18 | if vars == nil { 19 | vars = map[string]string{} 20 | } 21 | 22 | data := struct { 23 | Variables map[string]string `json:"variables"` 24 | }{ 25 | Variables: vars, 26 | } 27 | 28 | return t.client.put("/endpoints/"+tech+"/"+resource+"/sendMessage?"+v.Encode(), nil, &data) 29 | } 30 | 31 | // SendByURI sends a text message to an endpoint by free-form URI (rather than tech/resource) 32 | func (t *TextMessage) SendByURI(from, to, body string, vars map[string]string) error { 33 | // Construct querystring values 34 | v := url.Values{} 35 | v.Set("from", from) 36 | v.Set("to", to) 37 | v.Set("body", body) 38 | 39 | // vars must not be nil, or Ari will reject the request 40 | if vars == nil { 41 | vars = map[string]string{} 42 | } 43 | 44 | data := struct { 45 | Variables map[string]string `json:"variables"` 46 | }{ 47 | Variables: vars, 48 | } 49 | 50 | return t.client.put("/endpoints/sendMessage?"+v.Encode(), nil, &data) 51 | } 52 | -------------------------------------------------------------------------------- /internal/eventgen/template.tmpl: -------------------------------------------------------------------------------- 1 | // file generated by eventgen 2 | 3 | package ari 4 | 5 | import ( 6 | "encoding/json" 7 | "github.com/rotisserie/eris" 8 | ) 9 | 10 | // EventTypes enumerates the list of event types 11 | type EventTypes struct { 12 | All string 13 | {{range .}}{{.Name}} string 14 | {{end}} 15 | } 16 | 17 | // Events is the instance for grabbing event types 18 | var Events EventTypes 19 | 20 | func init() { 21 | Events.All = "all" 22 | {{range .}} Events.{{.Name}} = "{{.Event}}" 23 | {{end}} 24 | } 25 | 26 | // DecodeEvent converts a JSON-encoded event to an ARI event. 27 | func DecodeEvent(data []byte) (Event,error) { 28 | // Decode the event type 29 | var typer Message 30 | err := json.Unmarshal(data, &typer) 31 | if err != nil { 32 | return nil, eris.Wrap(err, "failed to decode type") 33 | } 34 | if typer.Type == "" { 35 | return nil, eris.New("no type found") 36 | } 37 | 38 | switch typer.Type { 39 | {{range .}}case Events.{{.Name}}: 40 | var e {{.Name}} 41 | err = json.Unmarshal(data, &e) 42 | return &e, err 43 | {{end}} 44 | } 45 | return nil, eris.New("unhandled type: "+typer.Type) 46 | } 47 | 48 | {{range .}} 49 | // {{.Name}} - "{{.Description}}" 50 | type {{.Name}} struct { 51 | EventData `json:",inline"` 52 | 53 | // Header describes any transport-related metadata 54 | Header Header `json:"-"` 55 | 56 | {{range .Properties}}{{.Name}} {{.Type}} {{.Mapping}} {{.Description}} 57 | {{end}} } 58 | {{end}} 59 | 60 | -------------------------------------------------------------------------------- /_examples/twoapps/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "golang.org/x/exp/slog" 7 | 8 | "github.com/CyCoreSystems/ari/v6/client/native" 9 | ) 10 | 11 | func main() { 12 | // OPTIONAL: setup logging 13 | log := slog.New(slog.NewTextHandler(os.Stderr, nil)) 14 | 15 | log.Info("connecting") 16 | 17 | appA, err := native.Connect(&native.Options{ 18 | Application: "example", 19 | Logger: log.With("app", "exampleA"), 20 | Username: "admin", 21 | Password: "admin", 22 | URL: "http://localhost:8088/ari", 23 | WebsocketURL: "ws://localhost:8088/ari/events", 24 | }) 25 | if err != nil { 26 | log.Error("failed to build native ARI client", "error", err) 27 | return 28 | } 29 | defer appA.Close() 30 | 31 | log.Info("Connected A") 32 | 33 | appB, err := native.Connect(&native.Options{ 34 | Application: "exampleB", 35 | Logger: log.With("app", "exampleB"), 36 | Username: "admin", 37 | Password: "admin", 38 | URL: "http://localhost:8088/ari", 39 | WebsocketURL: "ws://localhost:8088/ari/events", 40 | }) 41 | if err != nil { 42 | log.Error("failed to build native ARI client", "error", err) 43 | return 44 | } 45 | defer appB.Close() 46 | 47 | log.Info("Connected B") 48 | 49 | infoA, err := appA.Asterisk().Info(nil) 50 | if err != nil { 51 | log.Error("Failed to get Asterisk Info", "error", err) 52 | return 53 | } 54 | 55 | log.Info("AppA AsteriskInfo", "info", infoA) 56 | 57 | infoB, err := appB.Asterisk().Info(nil) 58 | if err != nil { 59 | log.Error("Failed to get Asterisk Info", "error", err) 60 | return 61 | } 62 | 63 | log.Info("AppB AsteriskInfo", "info", infoB) 64 | } 65 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1761114652, 24 | "narHash": "sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d+dAiC3H+CDle4=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "01f116e4df6a15f4ccdffb1bcd41096869fb385c", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /_examples/play/README.md: -------------------------------------------------------------------------------- 1 | # Examples - Play 2 | 3 | This example ARI application listens for calls coming into the Stasis app 4 | "test" and then answers the line, plays a sound to the caller, and hangs up. 5 | 6 | ## Asterisk dialplan 7 | 8 | An example dialplan for extension `100` would be something like this: 9 | 10 | ```asterisk 11 | exten = 100,1,Stasis("test") 12 | ``` 13 | 14 | ## Asterisk ARI configuration 15 | 16 | In order for the example application to connect with Asterisk, a few settings 17 | must be enabled. 18 | 19 | `http.conf` settings: 20 | 21 | ```ini 22 | [general] 23 | enabled=yes 24 | bindaddr=127.0.0.1 25 | bindport=8088 26 | ``` 27 | 28 | `ari.conf` settings: 29 | 30 | ```ini 31 | [general] 32 | enabled = yes 33 | allowed_origins = * ; tighten this down later 34 | 35 | [admin] 36 | type = user 37 | read_only = no 38 | password_format = crypt 39 | password = $6$/ejLut/kmjN6E5.g$tXEeth2SQoVYSs0AG0wWIoB3XRJEqK9vm0JGxQHU7Q/IIR/Ln5Zho40fcPUv1n8jvOJWYMJg0/4fLdJpSB2du1 40 | ``` 41 | 42 | **NOTE**: to obtain an encrypted password, you can use the `ari mkpassword` 43 | command from Asterisk. In this case, the following was done: 44 | 45 | ``` 46 | # asterisk -rx "ari mkpasswd admin" 47 | ``` 48 | 49 | ## Runtime 50 | 51 | Now, execute the example application, and it should connect to Asterisk and 52 | register the "test" ARI application. 53 | 54 | You may verify that it is registered by running the `ari show apps` Asterisk 55 | command: 56 | 57 | ``` 58 | # asterisk -rx "ari show apps" 59 | ``` 60 | 61 | ## Call 62 | 63 | Now, make a call into your Asterisk box to extension 100, and you should hear 64 | the playback (assuming you have the Asterisk extra sounds installed). You 65 | should also see the call come in on your application's log. 66 | 67 | 68 | -------------------------------------------------------------------------------- /ext/play/play.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // AllDTMF is a string which contains all possible 10 | // DTMF digits. 11 | const AllDTMF = "0123456789ABCD*#" 12 | 13 | // NewPlay creates a new audio Options suitable for general audio playback 14 | func NewPlay(ctx context.Context, p ari.Player, opts ...OptionFunc) (*Options, error) { 15 | o := NewDefaultOptions() 16 | err := o.ApplyOptions(opts...) 17 | 18 | return o, err 19 | } 20 | 21 | // NewPrompt creates a new audio Options suitable for prompt-style playback-and-get-response situations 22 | func NewPrompt(ctx context.Context, p ari.Player, opts ...OptionFunc) (*Options, error) { 23 | o := NewPromptOptions() 24 | err := o.ApplyOptions(opts...) 25 | 26 | return o, err 27 | } 28 | 29 | // Play plays a set of media URIs. Pass these URIs in with the `URI` OptionFunc. 30 | func Play(ctx context.Context, p ari.Player, opts ...OptionFunc) Session { 31 | o, err := NewPlay(ctx, p, opts...) 32 | if err != nil { 33 | return errorSession(err) 34 | } 35 | 36 | return o.Play(ctx, p) 37 | } 38 | 39 | // Prompt plays the given media URIs and waits for a DTMF response. The 40 | // received DTMF is available as `DTMF` in the Result object. Further 41 | // customize the behaviour with Match type OptionFuncs. 42 | func Prompt(ctx context.Context, p ari.Player, opts ...OptionFunc) Session { 43 | o, err := NewPrompt(ctx, p, opts...) 44 | if err != nil { 45 | return errorSession(err) 46 | } 47 | 48 | return o.Play(ctx, p) 49 | } 50 | 51 | // Play starts a new Play Session from the existing Options 52 | func (o *Options) Play(ctx context.Context, p ari.Player) Session { 53 | s := newPlaySession(o) 54 | 55 | go s.play(ctx, p) 56 | 57 | return s 58 | } 59 | -------------------------------------------------------------------------------- /client/native/sound.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | 7 | "github.com/CyCoreSystems/ari/v6" 8 | ) 9 | 10 | // Sound provides the ARI Sound accessors for the native client 11 | type Sound struct { 12 | client *Client 13 | } 14 | 15 | // Data returns the details of a given ARI Sound 16 | // Equivalent to GET /sounds/{name} 17 | func (s *Sound) Data(key *ari.Key) (*ari.SoundData, error) { 18 | if key == nil || key.ID == "" { 19 | return nil, errors.New("sound key not supplied") 20 | } 21 | 22 | data := new(ari.SoundData) 23 | if err := s.client.get("/sounds/"+key.ID, data); err != nil { 24 | return nil, dataGetError(err, "sound", "%v", key.ID) 25 | } 26 | 27 | data.Key = s.client.stamp(key) 28 | 29 | return data, nil 30 | } 31 | 32 | // List returns available sounds limited by the provided filters. 33 | // valid filters are "lang", "format", and nil (no filter) 34 | // An empty filter returns all available sounds 35 | func (s *Sound) List(filters map[string]string, keyFilter *ari.Key) (sh []*ari.Key, err error) { 36 | sounds := []struct { 37 | Name string `json:"name"` 38 | }{} 39 | 40 | uri := "/sounds" 41 | 42 | if len(filters) > 0 { 43 | v := url.Values{} 44 | 45 | for key, val := range filters { 46 | v.Set(key, val) 47 | } 48 | 49 | uri += "?" + v.Encode() 50 | } 51 | 52 | if keyFilter == nil { 53 | keyFilter = s.client.stamp(ari.NewKey(ari.SoundKey, "")) 54 | } 55 | 56 | err = s.client.get(uri, &sounds) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | // Store whatever we received, even if incomplete or error 62 | for _, i := range sounds { 63 | k := s.client.stamp(ari.NewKey(ari.SoundKey, i.Name)) 64 | if keyFilter.Match(k) { 65 | sh = append(sh, k) 66 | } 67 | } 68 | 69 | return sh, err 70 | } 71 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import "golang.org/x/exp/slog" 4 | 5 | // Client represents a set of operations to interact 6 | // with an Asterisk ARI server. It is agnostic to transport 7 | // and implementation. 8 | type Client interface { 9 | // ApplicationName returns the ARI application name by which this client is connected 10 | ApplicationName() string 11 | 12 | // Bus returns the event bus of the client 13 | Bus() Bus 14 | 15 | // Connected indicates whether the Websocket is connected 16 | Connected() bool 17 | 18 | // Close shuts down the client 19 | Close() 20 | 21 | // 22 | // ARI Namespaces 23 | // 24 | 25 | // Application access the Application ARI namespace 26 | Application() Application 27 | 28 | // Asterisk accesses the Asterisk ARI namespace 29 | Asterisk() Asterisk 30 | 31 | // Bridge accesses the Bridge ARI namespace 32 | Bridge() Bridge 33 | 34 | // Channel accesses the Channel ARI namespace 35 | Channel() Channel 36 | 37 | // DeviceState accesses the DeviceState ARI namespace 38 | DeviceState() DeviceState 39 | 40 | // Endpoint accesses the Endpoint ARI namespace 41 | Endpoint() Endpoint 42 | 43 | // LiveRecording accesses the LiveRecording ARI namespace 44 | LiveRecording() LiveRecording 45 | 46 | // Mailbox accesses the Mailbox ARI namespace 47 | Mailbox() Mailbox 48 | 49 | // Playback accesses the Playback ARI namespace 50 | Playback() Playback 51 | 52 | // SetLogger sets the logger to be used for this client's internal logging. 53 | SetLogger(logger *slog.Logger) 54 | 55 | // Sound accesses the Sound ARI namespace 56 | Sound() Sound 57 | 58 | // StoredRecording accesses the StoredRecording ARI namespace 59 | StoredRecording() StoredRecording 60 | 61 | // TextMessage accesses the TextMessage ARI namespace 62 | TextMessage() TextMessage 63 | } 64 | -------------------------------------------------------------------------------- /modules.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // Modules is the communication path for interacting with the 4 | // asterisk modules resource 5 | type Modules interface { 6 | Get(key *Key) *ModuleHandle 7 | 8 | List(filter *Key) ([]*Key, error) 9 | 10 | Load(key *Key) error 11 | 12 | Reload(key *Key) error 13 | 14 | Unload(key *Key) error 15 | 16 | Data(key *Key) (*ModuleData, error) 17 | } 18 | 19 | // ModuleData is the data for an asterisk module 20 | type ModuleData struct { 21 | // Key is the cluster-unique identifier for this module 22 | Key *Key `json:"key"` 23 | 24 | Name string `json:"name"` 25 | Description string `json:"description"` 26 | SupportLevel string `json:"support_level"` 27 | UseCount int `json:"use_count"` 28 | Status string `json:"status"` 29 | } 30 | 31 | // ModuleHandle is the reference to an asterisk module 32 | type ModuleHandle struct { 33 | key *Key 34 | m Modules 35 | } 36 | 37 | // NewModuleHandle returns a new module handle 38 | func NewModuleHandle(key *Key, m Modules) *ModuleHandle { 39 | return &ModuleHandle{key, m} 40 | } 41 | 42 | // ID returns the identifier for the module 43 | func (mh *ModuleHandle) ID() string { 44 | return mh.key.ID 45 | } 46 | 47 | // Key returns the key for the module 48 | func (mh *ModuleHandle) Key() *Key { 49 | return mh.key 50 | } 51 | 52 | // Reload reloads the module 53 | func (mh *ModuleHandle) Reload() error { 54 | return mh.m.Reload(mh.key) 55 | } 56 | 57 | // Unload unloads the module 58 | func (mh *ModuleHandle) Unload() error { 59 | return mh.m.Unload(mh.key) 60 | } 61 | 62 | // Load loads the module 63 | func (mh *ModuleHandle) Load() error { 64 | return mh.m.Load(mh.key) 65 | } 66 | 67 | // Data gets the module data 68 | func (mh *ModuleHandle) Data() (*ModuleData, error) { 69 | return mh.m.Data(mh.key) 70 | } 71 | -------------------------------------------------------------------------------- /datetime.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // NOTE: near direct translation from ARI 2.0 10 | 11 | // DateFormat is the date format that ARI returns in the JSON bodies 12 | const DateFormat = "2006-01-02T15:04:05.000-0700" 13 | 14 | // DateTime is an alias type for attaching a custom 15 | // asterisk unmarshaller and marshaller for JSON 16 | type DateTime time.Time 17 | 18 | // MarshalJSON converts the given date object to ARIs date format 19 | func (dt DateTime) MarshalJSON() ([]byte, error) { 20 | a := []byte("\"" + time.Time(dt).Format(DateFormat) + "\"") 21 | return a, nil 22 | } 23 | 24 | // UnmarshalJSON parses the given date per ARIs date format 25 | func (dt *DateTime) UnmarshalJSON(data []byte) error { 26 | var stringDate string 27 | if err := json.Unmarshal(data, &stringDate); err != nil { 28 | return err 29 | } 30 | 31 | t, err := time.Parse(DateFormat, stringDate) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | *dt = (DateTime)(t) 37 | 38 | return nil 39 | } 40 | 41 | func (dt DateTime) String() string { 42 | t := (time.Time)(dt) 43 | return t.String() 44 | } 45 | 46 | // Duration support functions 47 | 48 | // DurationSec is a JSON type for duration in seconds 49 | type DurationSec time.Duration 50 | 51 | // MarshalJSON converts the duration into a JSON friendly format 52 | func (ds DurationSec) MarshalJSON() ([]byte, error) { 53 | return []byte(strconv.Itoa(int(time.Duration(ds) / time.Second))), nil 54 | } 55 | 56 | // UnmarshalJSON parses the data into the duration seconds object 57 | func (ds *DurationSec) UnmarshalJSON(data []byte) error { 58 | s, err := strconv.Atoi(string(data)) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | *ds = DurationSec(time.Duration(s) * time.Second) 64 | 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /client/native/playback.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // Playback provides the ARI Playback accessors for the native client 10 | type Playback struct { 11 | client *Client 12 | } 13 | 14 | // Get gets a lazy handle for the given playback identifier 15 | func (a *Playback) Get(key *ari.Key) *ari.PlaybackHandle { 16 | return ari.NewPlaybackHandle(a.client.stamp(key), a, nil) 17 | } 18 | 19 | // Data returns a playback's details. 20 | // (Equivalent to GET /playbacks/{playbackID}) 21 | func (a *Playback) Data(key *ari.Key) (*ari.PlaybackData, error) { 22 | if key == nil || key.ID == "" { 23 | return nil, errors.New("playback key not supplied") 24 | } 25 | 26 | data := new(ari.PlaybackData) 27 | if err := a.client.get("/playbacks/"+key.ID, data); err != nil { 28 | return nil, dataGetError(err, "playback", "%v", key.ID) 29 | } 30 | 31 | data.Key = a.client.stamp(key) 32 | 33 | return data, nil 34 | } 35 | 36 | // Control performs the given operation on the current playback. Available operations are: 37 | // - restart 38 | // - pause 39 | // - unpause 40 | // - reverse 41 | // - forward 42 | func (a *Playback) Control(key *ari.Key, op string) error { 43 | req := struct { 44 | Operation string `json:"operation"` 45 | }{ 46 | Operation: op, 47 | } 48 | 49 | return a.client.post("/playbacks/"+key.ID+"/control", nil, &req) 50 | } 51 | 52 | // Stop stops a playback session. 53 | // (Equivalent to DELETE /playbacks/{playbackID}) 54 | func (a *Playback) Stop(key *ari.Key) error { 55 | return a.client.del("/playbacks/"+key.ID, nil, "") 56 | } 57 | 58 | // Subscribe listens for ARI events for the given playback entity 59 | func (a *Playback) Subscribe(key *ari.Key, n ...string) ari.Subscription { 60 | return a.client.Bus().Subscribe(key, n...) 61 | } 62 | -------------------------------------------------------------------------------- /client/native/config.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "github.com/rotisserie/eris" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // Config provides the ARI Configuration accessors for a native client 10 | type Config struct { 11 | client *Client 12 | } 13 | 14 | // Get gets a lazy handle to a configuration object 15 | func (c *Config) Get(key *ari.Key) *ari.ConfigHandle { 16 | return ari.NewConfigHandle(key, c) 17 | } 18 | 19 | // Data retrieves the state of a configuration object 20 | func (c *Config) Data(key *ari.Key) (*ari.ConfigData, error) { 21 | if key == nil || key.ID == "" { 22 | return nil, eris.New("config key not supplied") 23 | } 24 | 25 | class, kind, name, err := ari.ParseConfigID(key.ID) 26 | if err != nil { 27 | return nil, eris.Wrap(err, "failed to parse configuration key") 28 | } 29 | 30 | data := &ari.ConfigData{ 31 | Key: c.client.stamp(key), 32 | Class: class, 33 | Type: kind, 34 | Name: name, 35 | } 36 | 37 | err = c.client.get("/asterisk/config/dynamic/"+key.ID, &data.Fields) 38 | if err != nil { 39 | return nil, dataGetError(err, "config", "%v", key.ID) 40 | } 41 | 42 | return data, nil 43 | } 44 | 45 | // Update updates the given configuration object 46 | func (c *Config) Update(key *ari.Key, tuples []ari.ConfigTuple) (err error) { 47 | class, kind, name, err := ari.ParseConfigID(key.ID) 48 | if err != nil { 49 | return eris.Wrap(err, "failed to parse key") 50 | } 51 | 52 | cfgList := ari.ConfigTupleList{} 53 | cfgList.Fields = append(cfgList.Fields, tuples...) 54 | 55 | return c.client.put("/asterisk/config/dynamic/"+class+"/"+kind+"/"+name, nil, &cfgList) 56 | } 57 | 58 | // Delete deletes the configuration object 59 | func (c *Config) Delete(key *ari.Key) error { 60 | class, kind, name, err := ari.ParseConfigID(key.ID) 61 | if err != nil { 62 | return eris.Wrap(err, "failed to parse key") 63 | } 64 | 65 | return c.client.del("/asterisk/config/dynamic/"+class+"/"+kind+"/"+name, nil, "") 66 | } 67 | -------------------------------------------------------------------------------- /client/native/mailbox.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | 7 | "github.com/CyCoreSystems/ari/v6" 8 | ) 9 | 10 | // Mailbox provides the ARI Mailbox accessors for the native client 11 | type Mailbox struct { 12 | client *Client 13 | } 14 | 15 | // Get gets a lazy handle for the mailbox name 16 | func (m *Mailbox) Get(key *ari.Key) *ari.MailboxHandle { 17 | return ari.NewMailboxHandle(m.client.stamp(key), m) 18 | } 19 | 20 | // List lists the mailboxes and returns a list of handles 21 | func (m *Mailbox) List(filter *ari.Key) (mx []*ari.Key, err error) { 22 | mailboxes := []struct { 23 | Name string `json:"name"` 24 | }{} 25 | 26 | if filter == nil { 27 | filter = ari.NodeKey(m.client.node, m.client.ApplicationName()) 28 | } 29 | 30 | err = m.client.get("/mailboxes", &mailboxes) 31 | 32 | for _, i := range mailboxes { 33 | k := m.client.stamp(ari.NewKey(ari.MailboxKey, i.Name)) 34 | if filter.Match(k) { 35 | mx = append(mx, k) 36 | } 37 | } 38 | 39 | return 40 | } 41 | 42 | // Data retrieves the state of the given mailbox 43 | func (m *Mailbox) Data(key *ari.Key) (*ari.MailboxData, error) { 44 | if key == nil || key.ID == "" { 45 | return nil, errors.New("mailbox key not supplied") 46 | } 47 | 48 | data := new(ari.MailboxData) 49 | if err := m.client.get("/mailboxes/"+key.ID, data); err != nil { 50 | return nil, dataGetError(err, "mailbox", "%v", key.ID) 51 | } 52 | 53 | data.Key = m.client.stamp(key) 54 | 55 | return data, nil 56 | } 57 | 58 | // Update updates the new and old message counts of the mailbox 59 | func (m *Mailbox) Update(key *ari.Key, oldMessages int, newMessages int) error { 60 | req := map[string]string{ 61 | "oldMessages": strconv.Itoa(oldMessages), 62 | "newMessages": strconv.Itoa(newMessages), 63 | } 64 | 65 | return m.client.put("/mailboxes/"+key.ID, nil, &req) 66 | } 67 | 68 | // Delete deletes the mailbox 69 | func (m *Mailbox) Delete(key *ari.Key) error { 70 | return m.client.del("/mailboxes/"+key.ID, nil, "") 71 | } 72 | -------------------------------------------------------------------------------- /_examples/play/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "golang.org/x/exp/slog" 8 | 9 | "github.com/CyCoreSystems/ari/v6" 10 | "github.com/CyCoreSystems/ari/v6/client/native" 11 | "github.com/CyCoreSystems/ari/v6/ext/play" 12 | ) 13 | 14 | var log = slog.New(slog.NewTextHandler(os.Stderr, nil)) 15 | 16 | func main() { 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | defer cancel() 19 | 20 | log.Info("Connecting to ARI") 21 | 22 | cl, err := native.Connect(&native.Options{ 23 | Application: "test", 24 | Logger: log.With("app", "test"), 25 | Username: "admin", 26 | Password: "admin", 27 | URL: "http://localhost:8088/ari", 28 | WebsocketURL: "ws://localhost:8088/ari/events", 29 | }) 30 | if err != nil { 31 | log.Error("Failed to build ARI client", "error", err) 32 | 33 | return 34 | } 35 | 36 | log.Info("Listening for new calls") 37 | 38 | sub := cl.Bus().Subscribe(nil, "StasisStart") 39 | 40 | for { 41 | select { 42 | case e := <-sub.Events(): 43 | v := e.(*ari.StasisStart) 44 | 45 | log.Info("Got stasis start", "channel", v.Channel.ID) 46 | 47 | go app(ctx, cl.Channel().Get(v.Key(ari.ChannelKey, v.Channel.ID))) 48 | case <-ctx.Done(): 49 | return 50 | } 51 | } 52 | } 53 | 54 | func app(ctx context.Context, h *ari.ChannelHandle) { 55 | defer h.Hangup() //nolint:errcheck 56 | 57 | ctx, cancel := context.WithCancel(ctx) 58 | defer cancel() 59 | 60 | log.Info("Running app", "channel", h.ID()) 61 | 62 | end := h.Subscribe(ari.Events.StasisEnd) 63 | defer end.Cancel() 64 | 65 | // End the app when the channel goes away 66 | go func() { 67 | <-end.Events() 68 | cancel() 69 | }() 70 | 71 | if err := h.Answer(); err != nil { 72 | log.Error("failed to answer call", "error", err) 73 | return 74 | } 75 | 76 | if err := play.Play(ctx, h, play.URI("sound:tt-monkeys")).Err(); err != nil { 77 | log.Error("failed to play sound", "error", err) 78 | return 79 | } 80 | 81 | log.Info("completed playback") 82 | } 83 | -------------------------------------------------------------------------------- /client/native/device.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // DeviceState provides the ARI DeviceState accessors for the native client 10 | type DeviceState struct { 11 | client *Client 12 | } 13 | 14 | // Get returns the lazy handle for the given device name 15 | func (ds *DeviceState) Get(key *ari.Key) *ari.DeviceStateHandle { 16 | return ari.NewDeviceStateHandle(ds.client.stamp(key), ds) 17 | } 18 | 19 | // List lists the current devices and returns a list of handles 20 | func (ds *DeviceState) List(filter *ari.Key) (dx []*ari.Key, err error) { 21 | type device struct { 22 | Name string `json:"name"` 23 | } 24 | 25 | if filter == nil { 26 | filter = ds.client.stamp(ari.NewKey(ari.DeviceStateKey, "")) 27 | } 28 | 29 | var devices []device 30 | 31 | if err = ds.client.get("/deviceStates", &devices); err != nil { 32 | return nil, err 33 | } 34 | 35 | for _, i := range devices { 36 | k := ds.client.stamp(ari.NewKey(ari.DeviceStateKey, i.Name)) 37 | if filter.Match(k) { 38 | dx = append(dx, k) 39 | } 40 | } 41 | 42 | return 43 | } 44 | 45 | // Data retrieves the current state of the device 46 | func (ds *DeviceState) Data(key *ari.Key) (*ari.DeviceStateData, error) { 47 | if key == nil || key.ID == "" { 48 | return nil, errors.New("device key not supplied") 49 | } 50 | 51 | data := new(ari.DeviceStateData) 52 | if err := ds.client.get("/deviceStates/"+key.ID, data); err != nil { 53 | return nil, dataGetError(err, "deviceState", "%v", key.ID) 54 | } 55 | 56 | data.Key = ds.client.stamp(key) 57 | 58 | return data, nil 59 | } 60 | 61 | // Update updates the state of the device 62 | func (ds *DeviceState) Update(key *ari.Key, state string) error { 63 | req := map[string]string{ 64 | "deviceState": state, 65 | } 66 | 67 | return ds.client.put("/deviceStates/"+key.ID, nil, &req) 68 | } 69 | 70 | // Delete deletes the device 71 | func (ds *DeviceState) Delete(key *ari.Key) error { 72 | return ds.client.del("/deviceStates/"+key.ID, nil, "") 73 | } 74 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import "context" 4 | 5 | // ChannelContextOptions describes the set of options to be used when creating a channel-bound context. 6 | type ChannelContextOptions struct { 7 | ctx context.Context 8 | cancel context.CancelFunc 9 | 10 | hangupOnEnd bool 11 | 12 | sub Subscription 13 | } 14 | 15 | // ChannelContextOptionFunc describes a function which modifies channel context options. 16 | type ChannelContextOptionFunc func(o *ChannelContextOptions) 17 | 18 | // ChannelContext returns a context which is closed when the provided channel leaves the ARI application or the parent context is closed. The parent context is optional, and if it is `nil`, a new background context will be created. 19 | func ChannelContext(h *ChannelHandle, opts ...ChannelContextOptionFunc) (context.Context, context.CancelFunc) { 20 | o := new(ChannelContextOptions) 21 | 22 | for _, opt := range opts { 23 | opt(o) 24 | } 25 | 26 | if o.ctx == nil { 27 | o.ctx, o.cancel = context.WithCancel(context.Background()) 28 | } 29 | 30 | if o.sub == nil { 31 | o.sub = h.Subscribe(Events.StasisEnd) 32 | } 33 | 34 | go func() { 35 | defer o.cancel() 36 | 37 | select { 38 | case <-o.ctx.Done(): 39 | case <-o.sub.Events(): 40 | } 41 | 42 | if o.hangupOnEnd { 43 | h.Hangup() // nolint 44 | } 45 | }() 46 | 47 | return o.ctx, o.cancel 48 | } 49 | 50 | // WithParentContext requests that the generated channel context be created from the given parent context. 51 | func WithParentContext(parent context.Context) ChannelContextOptionFunc { 52 | return func(o *ChannelContextOptions) { 53 | if parent != nil { 54 | o.ctx, o.cancel = context.WithCancel(parent) 55 | } 56 | } 57 | } 58 | 59 | // HangupOnEnd indicates that the channel should be terminated when the channel context is terminated. Note that this also provides an easy way to create a time scope on a channel by putting a deadline on the parent context. 60 | func HangupOnEnd() ChannelContextOptionFunc { 61 | return func(o *ChannelContextOptions) { 62 | o.hangupOnEnd = true 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/arimocks/Sender.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery; DO NOT EDIT. 2 | // github.com/vektra/mockery 3 | // template: testify 4 | 5 | package arimocks 6 | 7 | import ( 8 | "github.com/CyCoreSystems/ari/v6" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // NewSender creates a new instance of Sender. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 13 | // The first argument is typically a *testing.T value. 14 | func NewSender(t interface { 15 | mock.TestingT 16 | Cleanup(func()) 17 | }) *Sender { 18 | mock := &Sender{} 19 | mock.Mock.Test(t) 20 | 21 | t.Cleanup(func() { mock.AssertExpectations(t) }) 22 | 23 | return mock 24 | } 25 | 26 | // Sender is an autogenerated mock type for the Sender type 27 | type Sender struct { 28 | mock.Mock 29 | } 30 | 31 | type Sender_Expecter struct { 32 | mock *mock.Mock 33 | } 34 | 35 | func (_m *Sender) EXPECT() *Sender_Expecter { 36 | return &Sender_Expecter{mock: &_m.Mock} 37 | } 38 | 39 | // Send provides a mock function for the type Sender 40 | func (_mock *Sender) Send(e ari.Event) { 41 | _mock.Called(e) 42 | return 43 | } 44 | 45 | // Sender_Send_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Send' 46 | type Sender_Send_Call struct { 47 | *mock.Call 48 | } 49 | 50 | // Send is a helper method to define mock.On call 51 | // - e ari.Event 52 | func (_e *Sender_Expecter) Send(e interface{}) *Sender_Send_Call { 53 | return &Sender_Send_Call{Call: _e.mock.On("Send", e)} 54 | } 55 | 56 | func (_c *Sender_Send_Call) Run(run func(e ari.Event)) *Sender_Send_Call { 57 | _c.Call.Run(func(args mock.Arguments) { 58 | var arg0 ari.Event 59 | if args[0] != nil { 60 | arg0 = args[0].(ari.Event) 61 | } 62 | run( 63 | arg0, 64 | ) 65 | }) 66 | return _c 67 | } 68 | 69 | func (_c *Sender_Send_Call) Return() *Sender_Send_Call { 70 | _c.Call.Return() 71 | return _c 72 | } 73 | 74 | func (_c *Sender_Send_Call) RunAndReturn(run func(e ari.Event)) *Sender_Send_Call { 75 | _c.Run(run) 76 | return _c 77 | } 78 | -------------------------------------------------------------------------------- /client/native/modules.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // Modules provides the ARI modules accessors for a native client 10 | type Modules struct { 11 | client *Client 12 | } 13 | 14 | // Get obtains a lazy handle to an asterisk module 15 | func (m *Modules) Get(key *ari.Key) *ari.ModuleHandle { 16 | return ari.NewModuleHandle(m.client.stamp(key), m) 17 | } 18 | 19 | // List lists the modules and returns lists of handles 20 | func (m *Modules) List(filter *ari.Key) (ret []*ari.Key, err error) { 21 | if filter == nil { 22 | filter = ari.NodeKey(m.client.appName, m.client.node) 23 | } 24 | 25 | modules := []struct { 26 | Name string `json:"name"` 27 | }{} 28 | 29 | err = m.client.get("/asterisk/modules", &modules) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | for _, i := range modules { 35 | k := m.client.stamp(ari.NewKey(ari.ModuleKey, i.Name)) 36 | if filter.Match(k) { 37 | if filter.Dialog != "" { 38 | k.Dialog = filter.Dialog 39 | } 40 | 41 | ret = append(ret, k) 42 | } 43 | } 44 | 45 | return 46 | } 47 | 48 | // Load loads the named asterisk module 49 | func (m *Modules) Load(key *ari.Key) error { 50 | return m.client.post("/asterisk/modules/"+key.ID, nil, nil) 51 | } 52 | 53 | // Reload reloads the named asterisk module 54 | func (m *Modules) Reload(key *ari.Key) error { 55 | return m.client.put("/asterisk/modules/"+key.ID, nil, nil) 56 | } 57 | 58 | // Unload unloads the named asterisk module 59 | func (m *Modules) Unload(key *ari.Key) error { 60 | return m.client.del("/asterisk/modules/"+key.ID, nil, "") 61 | } 62 | 63 | // Data retrieves the state of the named asterisk module 64 | func (m *Modules) Data(key *ari.Key) (*ari.ModuleData, error) { 65 | if key == nil || key.ID == "" { 66 | return nil, errors.New("module key not supplied") 67 | } 68 | 69 | data := new(ari.ModuleData) 70 | if err := m.client.get("/asterisk/modules/"+key.ID, data); err != nil { 71 | return nil, dataGetError(err, "module", "%v", key.ID) 72 | } 73 | 74 | data.Key = m.client.stamp(key) 75 | 76 | return data, nil 77 | } 78 | -------------------------------------------------------------------------------- /device.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // DeviceState represents a communication path interacting with an 4 | // Asterisk server for device state resources 5 | type DeviceState interface { 6 | Get(key *Key) *DeviceStateHandle 7 | 8 | List(filter *Key) ([]*Key, error) 9 | 10 | Data(key *Key) (*DeviceStateData, error) 11 | 12 | Update(key *Key, state string) error 13 | 14 | Delete(key *Key) error 15 | } 16 | 17 | // DeviceStateData is the device state for the device 18 | type DeviceStateData struct { 19 | // Key is the cluster-unique identifier for this device state 20 | Key *Key `json:"key"` 21 | 22 | // Name is the name of the device 23 | Name string `json:"name"` 24 | 25 | // State is the state of the device 26 | State string `json:"state"` 27 | } 28 | 29 | // DeviceStateHandle is a representation of a device state 30 | // that can be interacted with 31 | type DeviceStateHandle struct { 32 | key *Key 33 | d DeviceState 34 | } 35 | 36 | // NewDeviceStateHandle creates a new deviceState handle 37 | func NewDeviceStateHandle(key *Key, d DeviceState) *DeviceStateHandle { 38 | return &DeviceStateHandle{ 39 | key: key, 40 | d: d, 41 | } 42 | } 43 | 44 | // ID returns the identifier for the device 45 | func (dsh *DeviceStateHandle) ID() string { 46 | return dsh.key.ID 47 | } 48 | 49 | // Key returns the key for the device 50 | func (dsh *DeviceStateHandle) Key() *Key { 51 | return dsh.key 52 | } 53 | 54 | // Data gets the device state 55 | func (dsh *DeviceStateHandle) Data() (d *DeviceStateData, err error) { 56 | d, err = dsh.d.Data(dsh.key) 57 | return 58 | } 59 | 60 | // Update updates the device state, implicitly creating it if not exists 61 | func (dsh *DeviceStateHandle) Update(state string) (err error) { 62 | err = dsh.d.Update(dsh.key, state) 63 | return 64 | } 65 | 66 | // Delete deletes the device state 67 | func (dsh *DeviceStateHandle) Delete() (err error) { 68 | err = dsh.d.Delete(dsh.key) 69 | // NOTE: if err is not nil, 70 | // we could replace 'd' with a version of it 71 | // that always returns ErrNotFound. Not required, as the 72 | // handle could "come back" at any moment via an 'Update' 73 | return 74 | } 75 | -------------------------------------------------------------------------------- /recording.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import "time" 4 | 5 | // Recording is a namespace for the recording types 6 | type Recording struct { 7 | Stored StoredRecording 8 | Live LiveRecording 9 | } 10 | 11 | // Recorder describes an interface of something which can Record 12 | type Recorder interface { 13 | // Record starts a recording, using the provided options, and returning a handle for the live recording 14 | Record(string, *RecordingOptions) (*LiveRecordingHandle, error) 15 | 16 | // StageRecord stages a recording, using the provided options, and returning a handle for the live recording. The recording will actually be started only when Exec() is called. 17 | StageRecord(string, *RecordingOptions) (*LiveRecordingHandle, error) 18 | 19 | // Subscribe subscribes to events from the Recorder 20 | Subscribe(n ...string) Subscription 21 | } 22 | 23 | // RecordingOptions describes the set of options available when making a recording. 24 | type RecordingOptions struct { 25 | // Format is the file format/encoding to which the recording should be stored. 26 | // This will usually be one of: slin, ulaw, alaw, wav, gsm. 27 | // If not specified, this will default to slin. 28 | Format string 29 | 30 | // MaxDuration is the maximum duration of the recording, after which the recording will 31 | // automatically stop. If not set, there is no maximum. 32 | MaxDuration time.Duration 33 | 34 | // MaxSilence is the maximum duration of detected to be found before terminating the recording. 35 | MaxSilence time.Duration 36 | 37 | // Exists determines what should happen if the given recording already exists. 38 | // Valid values are: "fail", "overwrite", or "append". 39 | // If not specified, it will default to "fail" 40 | Exists string 41 | 42 | // Beep indicates whether a beep should be played to the recorded 43 | // party at the beginning of the recording. 44 | Beep bool 45 | 46 | // Terminate indicates whether the recording should be terminated on 47 | // receipt of a DTMF digit. 48 | // valid options are: "none", "any", "*", and "#" 49 | // If not specified, it will default to "none" (never terminate on DTMF). 50 | Terminate string 51 | } 52 | -------------------------------------------------------------------------------- /mailbox.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // Mailbox is the communication path to an Asterisk server for 4 | // operating on mailbox resources 5 | type Mailbox interface { 6 | // Get gets a handle to the mailbox for further operations 7 | Get(key *Key) *MailboxHandle 8 | 9 | // List lists the mailboxes in asterisk 10 | List(filter *Key) ([]*Key, error) 11 | 12 | // Data gets the current state of the mailbox 13 | Data(key *Key) (*MailboxData, error) 14 | 15 | // Update updates the state of the mailbox, or creates if does not exist 16 | Update(key *Key, oldMessages int, newMessages int) error 17 | 18 | // Delete deletes the mailbox 19 | Delete(key *Key) error 20 | } 21 | 22 | // MailboxData respresents the state of an Asterisk (voice) mailbox 23 | type MailboxData struct { 24 | // Key is the cluster-unique identifier for this mailbox 25 | Key *Key `json:"key"` 26 | 27 | Name string `json:"name"` 28 | NewMessages int `json:"new_messages"` // Number of new (unread) messages 29 | OldMessages int `json:"old_messages"` // Number of old (read) messages 30 | } 31 | 32 | // A MailboxHandle is a handle to a mailbox instance attached to an 33 | // ari transport 34 | type MailboxHandle struct { 35 | key *Key 36 | m Mailbox 37 | } 38 | 39 | // NewMailboxHandle creates a new mailbox handle given the name and mailbox transport 40 | func NewMailboxHandle(key *Key, m Mailbox) *MailboxHandle { 41 | return &MailboxHandle{ 42 | key: key, 43 | m: m, 44 | } 45 | } 46 | 47 | // ID returns the identifier for the mailbox handle 48 | func (mh *MailboxHandle) ID() string { 49 | return mh.key.ID 50 | } 51 | 52 | // Key returns the key for the mailbox handle 53 | func (mh *MailboxHandle) Key() *Key { 54 | return mh.key 55 | } 56 | 57 | // Data gets the current state of the mailbox 58 | func (mh *MailboxHandle) Data() (*MailboxData, error) { 59 | return mh.m.Data(mh.key) 60 | } 61 | 62 | // Update updates the state of the mailbox, or creates if does not exist 63 | func (mh *MailboxHandle) Update(oldMessages int, newMessages int) error { 64 | return mh.m.Update(mh.key, oldMessages, newMessages) 65 | } 66 | 67 | // Delete deletes the mailbox 68 | func (mh *MailboxHandle) Delete() error { 69 | return mh.m.Delete(mh.key) 70 | } 71 | -------------------------------------------------------------------------------- /bus.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // Bus is an event bus for ARI events. It receives and 9 | // redistributes events based on a subscription model. 10 | type Bus interface { 11 | Close() 12 | Sender 13 | Subscriber 14 | } 15 | 16 | // A Sender is an entity which can send event bus messages 17 | type Sender interface { 18 | Send(e Event) 19 | } 20 | 21 | // A Subscriber is an entity which can create ARI event subscriptions 22 | type Subscriber interface { 23 | Subscribe(key *Key, n ...string) Subscription 24 | } 25 | 26 | // A Subscription is a subscription on series of ARI events 27 | type Subscription interface { 28 | // Events returns a channel on which events related to this subscription are sent. 29 | Events() <-chan Event 30 | 31 | // Cancel terminates the subscription 32 | Cancel() 33 | } 34 | 35 | // Once listens for the first event of the provided types, 36 | // returning a channel which supplies that event. 37 | func Once(ctx context.Context, bus Bus, key *Key, eTypes ...string) <-chan Event { 38 | s := bus.Subscribe(key, eTypes...) 39 | 40 | ret := make(chan Event) 41 | 42 | // Stop subscription after one event 43 | go func() { 44 | select { 45 | case ret <- <-s.Events(): 46 | case <-ctx.Done(): 47 | } 48 | 49 | close(ret) 50 | 51 | s.Cancel() 52 | }() 53 | 54 | return ret 55 | } 56 | 57 | // NewNullSubscription returns a subscription which never returns any events 58 | func NewNullSubscription() *NullSubscription { 59 | return &NullSubscription{ 60 | ch: make(chan Event), 61 | } 62 | } 63 | 64 | // NullSubscription is a subscription which never returns any events. 65 | type NullSubscription struct { 66 | ch chan Event 67 | closed bool 68 | mu sync.RWMutex 69 | } 70 | 71 | // Events implements the Subscription interface 72 | func (n *NullSubscription) Events() <-chan Event { 73 | if n.ch == nil { 74 | n.mu.Lock() 75 | n.closed = false 76 | n.ch = make(chan Event) 77 | n.mu.Unlock() 78 | } 79 | 80 | return n.ch 81 | } 82 | 83 | // Cancel implements the Subscription interface 84 | func (n *NullSubscription) Cancel() { 85 | if n.closed { 86 | return 87 | } 88 | 89 | n.mu.Lock() 90 | 91 | n.closed = true 92 | if n.ch != nil { 93 | close(n.ch) 94 | } 95 | 96 | n.mu.Unlock() 97 | } 98 | -------------------------------------------------------------------------------- /_examples/record/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "golang.org/x/exp/slog" 8 | 9 | "github.com/CyCoreSystems/ari/v6" 10 | "github.com/CyCoreSystems/ari/v6/client/native" 11 | "github.com/CyCoreSystems/ari/v6/ext/record" 12 | ) 13 | 14 | var log = slog.New(slog.NewTextHandler(os.Stderr, nil)) 15 | 16 | func main() { 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | defer cancel() 19 | 20 | log.Info("Connecting to ARI") 21 | 22 | cl, err := native.Connect(&native.Options{ 23 | Application: "test", 24 | Logger: log, 25 | Username: "admin", 26 | Password: "admin", 27 | URL: "http://localhost:8088/ari", 28 | WebsocketURL: "ws://localhost:8088/ari/events", 29 | }) 30 | if err != nil { 31 | log.Error("Failed to build ARI client", "error", err) 32 | 33 | return 34 | } 35 | 36 | // setup app 37 | 38 | log.Info("Listening for new calls") 39 | 40 | sub := cl.Bus().Subscribe(nil, "StasisStart") 41 | 42 | for { 43 | select { 44 | case e := <-sub.Events(): 45 | v := e.(*ari.StasisStart) 46 | 47 | log.Info("Got stasis start", "channel", v.Channel.ID) 48 | 49 | go app(ctx, cl.Channel().Get(v.Key(ari.ChannelKey, v.Channel.ID))) 50 | case <-ctx.Done(): 51 | return 52 | } 53 | } 54 | } 55 | 56 | func app(ctx context.Context, h *ari.ChannelHandle) { 57 | defer h.Hangup() //nolint:errcheck 58 | 59 | ctx, cancel := context.WithCancel(ctx) 60 | defer cancel() 61 | 62 | log.Info("Running app", "channel", h.ID()) 63 | 64 | end := h.Subscribe(ari.Events.StasisEnd) 65 | defer end.Cancel() 66 | 67 | // End the app when the channel goes away 68 | go func() { 69 | <-end.Events() 70 | cancel() 71 | }() 72 | 73 | if err := h.Answer(); err != nil { 74 | log.Error("failed to answer call", "error", err) 75 | return 76 | } 77 | 78 | res, err := record.Record(ctx, h, 79 | record.TerminateOn("any"), 80 | record.IfExists("overwrite"), 81 | record.WithLogger(log.With("app", "recorder")), 82 | ).Result() 83 | if err != nil { 84 | log.Error("failed to record", "error", err) 85 | return 86 | } 87 | 88 | if err = res.Save("test-recording"); err != nil { 89 | log.Error("failed to save recording", "error", err) 90 | } 91 | 92 | log.Info("completed recording") 93 | 94 | h.Hangup() //nolint:errcheck 95 | } 96 | -------------------------------------------------------------------------------- /client/arimocks/Matcher.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery; DO NOT EDIT. 2 | // github.com/vektra/mockery 3 | // template: testify 4 | 5 | package arimocks 6 | 7 | import ( 8 | "github.com/CyCoreSystems/ari/v6" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // NewMatcher creates a new instance of Matcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 13 | // The first argument is typically a *testing.T value. 14 | func NewMatcher(t interface { 15 | mock.TestingT 16 | Cleanup(func()) 17 | }) *Matcher { 18 | mock := &Matcher{} 19 | mock.Mock.Test(t) 20 | 21 | t.Cleanup(func() { mock.AssertExpectations(t) }) 22 | 23 | return mock 24 | } 25 | 26 | // Matcher is an autogenerated mock type for the Matcher type 27 | type Matcher struct { 28 | mock.Mock 29 | } 30 | 31 | type Matcher_Expecter struct { 32 | mock *mock.Mock 33 | } 34 | 35 | func (_m *Matcher) EXPECT() *Matcher_Expecter { 36 | return &Matcher_Expecter{mock: &_m.Mock} 37 | } 38 | 39 | // Match provides a mock function for the type Matcher 40 | func (_mock *Matcher) Match(o *ari.Key) bool { 41 | ret := _mock.Called(o) 42 | 43 | if len(ret) == 0 { 44 | panic("no return value specified for Match") 45 | } 46 | 47 | var r0 bool 48 | if returnFunc, ok := ret.Get(0).(func(*ari.Key) bool); ok { 49 | r0 = returnFunc(o) 50 | } else { 51 | r0 = ret.Get(0).(bool) 52 | } 53 | return r0 54 | } 55 | 56 | // Matcher_Match_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Match' 57 | type Matcher_Match_Call struct { 58 | *mock.Call 59 | } 60 | 61 | // Match is a helper method to define mock.On call 62 | // - o *ari.Key 63 | func (_e *Matcher_Expecter) Match(o interface{}) *Matcher_Match_Call { 64 | return &Matcher_Match_Call{Call: _e.mock.On("Match", o)} 65 | } 66 | 67 | func (_c *Matcher_Match_Call) Run(run func(o *ari.Key)) *Matcher_Match_Call { 68 | _c.Call.Run(func(args mock.Arguments) { 69 | var arg0 *ari.Key 70 | if args[0] != nil { 71 | arg0 = args[0].(*ari.Key) 72 | } 73 | run( 74 | arg0, 75 | ) 76 | }) 77 | return _c 78 | } 79 | 80 | func (_c *Matcher_Match_Call) Return(b bool) *Matcher_Match_Call { 81 | _c.Call.Return(b) 82 | return _c 83 | } 84 | 85 | func (_c *Matcher_Match_Call) RunAndReturn(run func(o *ari.Key) bool) *Matcher_Match_Call { 86 | _c.Call.Return(run) 87 | return _c 88 | } 89 | -------------------------------------------------------------------------------- /logging.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // Logging represents a communication path to an 4 | // Asterisk server for working with logging resources 5 | type Logging interface { 6 | // Create creates a new log. The levels are a comma-separated list of 7 | // logging levels on which this channel should operate. The name of the 8 | // channel should be the key's ID. 9 | Create(key *Key, levels string) (*LogHandle, error) 10 | 11 | // Data retrives the data for a logging channel 12 | Data(key *Key) (*LogData, error) 13 | 14 | // Data retrives the data for a logging channel 15 | Get(key *Key) *LogHandle 16 | 17 | // List the logs 18 | List(filter *Key) ([]*Key, error) 19 | 20 | // Rotate rotates the log 21 | Rotate(key *Key) error 22 | 23 | // Delete deletes the log 24 | Delete(key *Key) error 25 | } 26 | 27 | // LogData represents the log data 28 | type LogData struct { 29 | // Key is the cluster-unique identifier for this logging channel 30 | Key *Key `json:"key"` 31 | 32 | // Name is the name of the logging channel 33 | Name string `json:"channel"` 34 | 35 | // Levels is a comma-separated list of logging levels for this channel 36 | Levels string `json:"levels"` 37 | 38 | // Type indicates the type of logs for this channel 39 | Types string `json:"types"` 40 | 41 | // Status indicates whether this logging channel is enabled 42 | Status string `json:"status"` 43 | } 44 | 45 | // NewLogHandle builds a new log handle given the `Key` and `Logging` client 46 | func NewLogHandle(key *Key, l Logging) *LogHandle { 47 | return &LogHandle{ 48 | key: key, 49 | c: l, 50 | } 51 | } 52 | 53 | // LogHandle provides an interface to manipulate a logging channel 54 | type LogHandle struct { 55 | key *Key 56 | c Logging 57 | } 58 | 59 | // ID returns the ID (name) of the logging channel 60 | func (l *LogHandle) ID() string { 61 | return l.key.ID 62 | } 63 | 64 | // Key returns the Key of the logging channel 65 | func (l *LogHandle) Key() *Key { 66 | return l.key 67 | } 68 | 69 | // Data returns the data for the logging channel 70 | func (l *LogHandle) Data() (*LogData, error) { 71 | return l.c.Data(l.key) 72 | } 73 | 74 | // Rotate causes the logging channel's logfiles to be rotated 75 | func (l *LogHandle) Rotate() error { 76 | return l.c.Rotate(l.key) 77 | } 78 | 79 | // Delete removes the logging channel from Asterisk 80 | func (l *LogHandle) Delete() error { 81 | return l.c.Delete(l.key) 82 | } 83 | -------------------------------------------------------------------------------- /client/native/endpoint.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // Endpoint provides the ARI Endpoint accessors for the native client 10 | type Endpoint struct { 11 | client *Client 12 | } 13 | 14 | // Get gets a lazy handle for the endpoint entity 15 | func (e *Endpoint) Get(key *ari.Key) *ari.EndpointHandle { 16 | return ari.NewEndpointHandle(e.client.stamp(key), e) 17 | } 18 | 19 | // List lists the current endpoints and returns a list of handles 20 | func (e *Endpoint) List(filter *ari.Key) (ex []*ari.Key, err error) { 21 | endpoints := []struct { 22 | Tech string `json:"technology"` 23 | Resource string `json:"resource"` 24 | }{} 25 | 26 | if filter == nil { 27 | filter = ari.NodeKey(e.client.ApplicationName(), e.client.node) 28 | } 29 | 30 | if err = e.client.get("/endpoints", &endpoints); err != nil { 31 | return nil, err 32 | } 33 | 34 | for _, i := range endpoints { 35 | k := e.client.stamp(ari.NewEndpointKey(i.Tech, i.Resource)) 36 | if filter.Match(k) { 37 | ex = append(ex, k) 38 | } 39 | } 40 | 41 | return 42 | } 43 | 44 | // ListByTech lists the current endpoints with the given technology and 45 | // returns a list of handles. 46 | func (e *Endpoint) ListByTech(tech string, filter *ari.Key) (ex []*ari.Key, err error) { 47 | endpoints := []struct { 48 | Tech string `json:"technology"` 49 | Resource string `json:"resource"` 50 | }{} 51 | 52 | if filter == nil { 53 | filter = ari.NodeKey(e.client.ApplicationName(), e.client.node) 54 | } 55 | 56 | if err = e.client.get("/endpoints/"+tech, &endpoints); err != nil { 57 | return nil, err 58 | } 59 | 60 | for _, i := range endpoints { 61 | k := e.client.stamp(ari.NewEndpointKey(i.Tech, i.Resource)) 62 | if filter.Match(k) { 63 | ex = append(ex, k) 64 | } 65 | } 66 | 67 | return 68 | } 69 | 70 | // Data retrieves the current state of the endpoint 71 | func (e *Endpoint) Data(key *ari.Key) (*ari.EndpointData, error) { 72 | if key == nil || key.ID == "" { 73 | return nil, errors.New("endpoint key not supplied") 74 | } 75 | 76 | if key.Kind != ari.EndpointKey { 77 | return nil, errors.New("wrong key type") 78 | } 79 | 80 | data := new(ari.EndpointData) 81 | if err := e.client.get("/endpoints/"+key.ID, data); err != nil { 82 | return nil, dataGetError(err, "endpoint", "%s", key.ID) 83 | } 84 | 85 | data.Key = e.client.stamp(key) 86 | 87 | return data, nil 88 | } 89 | -------------------------------------------------------------------------------- /client/arimocks/DTMFSender.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery; DO NOT EDIT. 2 | // github.com/vektra/mockery 3 | // template: testify 4 | 5 | package arimocks 6 | 7 | import ( 8 | "github.com/CyCoreSystems/ari/v6" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // NewDTMFSender creates a new instance of DTMFSender. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 13 | // The first argument is typically a *testing.T value. 14 | func NewDTMFSender(t interface { 15 | mock.TestingT 16 | Cleanup(func()) 17 | }) *DTMFSender { 18 | mock := &DTMFSender{} 19 | mock.Mock.Test(t) 20 | 21 | t.Cleanup(func() { mock.AssertExpectations(t) }) 22 | 23 | return mock 24 | } 25 | 26 | // DTMFSender is an autogenerated mock type for the DTMFSender type 27 | type DTMFSender struct { 28 | mock.Mock 29 | } 30 | 31 | type DTMFSender_Expecter struct { 32 | mock *mock.Mock 33 | } 34 | 35 | func (_m *DTMFSender) EXPECT() *DTMFSender_Expecter { 36 | return &DTMFSender_Expecter{mock: &_m.Mock} 37 | } 38 | 39 | // SendDTMF provides a mock function for the type DTMFSender 40 | func (_mock *DTMFSender) SendDTMF(dtmf string, opts *ari.DTMFOptions) { 41 | _mock.Called(dtmf, opts) 42 | return 43 | } 44 | 45 | // DTMFSender_SendDTMF_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendDTMF' 46 | type DTMFSender_SendDTMF_Call struct { 47 | *mock.Call 48 | } 49 | 50 | // SendDTMF is a helper method to define mock.On call 51 | // - dtmf string 52 | // - opts *ari.DTMFOptions 53 | func (_e *DTMFSender_Expecter) SendDTMF(dtmf interface{}, opts interface{}) *DTMFSender_SendDTMF_Call { 54 | return &DTMFSender_SendDTMF_Call{Call: _e.mock.On("SendDTMF", dtmf, opts)} 55 | } 56 | 57 | func (_c *DTMFSender_SendDTMF_Call) Run(run func(dtmf string, opts *ari.DTMFOptions)) *DTMFSender_SendDTMF_Call { 58 | _c.Call.Run(func(args mock.Arguments) { 59 | var arg0 string 60 | if args[0] != nil { 61 | arg0 = args[0].(string) 62 | } 63 | var arg1 *ari.DTMFOptions 64 | if args[1] != nil { 65 | arg1 = args[1].(*ari.DTMFOptions) 66 | } 67 | run( 68 | arg0, 69 | arg1, 70 | ) 71 | }) 72 | return _c 73 | } 74 | 75 | func (_c *DTMFSender_SendDTMF_Call) Return() *DTMFSender_SendDTMF_Call { 76 | _c.Call.Return() 77 | return _c 78 | } 79 | 80 | func (_c *DTMFSender_SendDTMF_Call) RunAndReturn(run func(dtmf string, opts *ari.DTMFOptions)) *DTMFSender_SendDTMF_Call { 81 | _c.Run(run) 82 | return _c 83 | } 84 | -------------------------------------------------------------------------------- /client/native/asterisk.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rotisserie/eris" 7 | 8 | "github.com/CyCoreSystems/ari/v6" 9 | ) 10 | 11 | // Asterisk provides the ARI Asterisk accessors for a native client 12 | type Asterisk struct { 13 | client *Client 14 | } 15 | 16 | // Logging provides the ARI Asterisk Logging accessors for a native client 17 | func (a *Asterisk) Logging() ari.Logging { 18 | return &Logging{a.client} 19 | } 20 | 21 | // Modules provides the ARI Asterisk Modules accessors for a native client 22 | func (a *Asterisk) Modules() ari.Modules { 23 | return &Modules{a.client} 24 | } 25 | 26 | // Config provides the ARI Asterisk Config accessors for a native client 27 | func (a *Asterisk) Config() ari.Config { 28 | return &Config{a.client} 29 | } 30 | 31 | /* 32 | conn *Conn 33 | logging ari.Logging 34 | modules ari.Modules 35 | config ari.Config 36 | } 37 | */ 38 | 39 | // Info returns various data about the Asterisk system 40 | // Equivalent to GET /asterisk/info 41 | func (a *Asterisk) Info(key *ari.Key) (*ari.AsteriskInfo, error) { 42 | var m ari.AsteriskInfo 43 | 44 | return &m, eris.Wrap( 45 | a.client.get("/asterisk/info", &m), 46 | "failed to get asterisk info", 47 | ) 48 | } 49 | 50 | // AsteriskVariables provides the ARI Variables accessors for server-level variables 51 | type AsteriskVariables struct { 52 | client *Client 53 | } 54 | 55 | // Variables returns the variables interface for the Asterisk server 56 | func (a *Asterisk) Variables() ari.AsteriskVariables { 57 | return &AsteriskVariables{a.client} 58 | } 59 | 60 | // Get returns the value of the given global variable 61 | // Equivalent to GET /asterisk/variable 62 | func (a *AsteriskVariables) Get(key *ari.Key) (string, error) { 63 | var m struct { 64 | Value string `json:"value"` 65 | } 66 | 67 | err := a.client.get(fmt.Sprintf("/asterisk/variable?variable=%s", key.ID), &m) 68 | if err != nil { 69 | return "", eris.Wrapf(err, "Error getting asterisk variable '%v'", key.ID) 70 | } 71 | 72 | return m.Value, nil 73 | } 74 | 75 | // Set sets a global channel variable 76 | // (Equivalent to POST /asterisk/variable) 77 | func (a *AsteriskVariables) Set(key *ari.Key, value string) (err error) { 78 | req := struct { 79 | Variable string `json:"variable"` 80 | Value string `json:"value,omitempty"` 81 | }{ 82 | Variable: key.ID, 83 | Value: value, 84 | } 85 | 86 | return eris.Wrapf( 87 | a.client.post("/asterisk/variable", nil, &req), 88 | "Error setting asterisk variable '%s' to '%s'", key.ID, value, 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /ext/keyfilter/keyfilter.go: -------------------------------------------------------------------------------- 1 | package keyfilter 2 | 3 | import "github.com/CyCoreSystems/ari/v6" 4 | 5 | // Kind filters a list of keys by a particular Kind 6 | func Kind(kind string, in []*ari.Key) (out []*ari.Key) { 7 | for _, k := range in { 8 | if k.Kind == kind { 9 | out = append(out, k) 10 | } 11 | } 12 | 13 | return 14 | } 15 | 16 | // Applications returns the Application keys from the given list of keys 17 | func Applications(in []*ari.Key) (out []*ari.Key) { 18 | return Kind(ari.ApplicationKey, in) 19 | } 20 | 21 | // Bridges returns the Bridge keys from the given list of keys 22 | func Bridges(in []*ari.Key) (out []*ari.Key) { 23 | return Kind(ari.BridgeKey, in) 24 | } 25 | 26 | // Channels returns the Channel keys from the given list of keys 27 | func Channels(in []*ari.Key) (out []*ari.Key) { 28 | return Kind(ari.ChannelKey, in) 29 | } 30 | 31 | // DeviceStates returns the DeviceState keys from the given list of keys 32 | func DeviceStates(in []*ari.Key) (out []*ari.Key) { 33 | return Kind(ari.DeviceStateKey, in) 34 | } 35 | 36 | // Endpoints returns the Endpoint keys from the given list of keys 37 | func Endpoints(in []*ari.Key) (out []*ari.Key) { 38 | return Kind(ari.EndpointKey, in) 39 | } 40 | 41 | // LiveRecordings returns the LiveRecording keys from the given list of keys 42 | func LiveRecordings(in []*ari.Key) (out []*ari.Key) { 43 | return Kind(ari.LiveRecordingKey, in) 44 | } 45 | 46 | // Loggings returns the Logging keys from the given list of keys 47 | func Loggings(in []*ari.Key) (out []*ari.Key) { 48 | return Kind(ari.LoggingKey, in) 49 | } 50 | 51 | // Mailboxes returns the Mailbox keys from the given list of keys 52 | func Mailboxes(in []*ari.Key) (out []*ari.Key) { 53 | return Kind(ari.MailboxKey, in) 54 | } 55 | 56 | // Modules returns the Module keys from the given list of keys 57 | func Modules(in []*ari.Key) (out []*ari.Key) { 58 | return Kind(ari.ModuleKey, in) 59 | } 60 | 61 | // Playbacks returns the Playback keys from the given list of keys 62 | func Playbacks(in []*ari.Key) (out []*ari.Key) { 63 | return Kind(ari.PlaybackKey, in) 64 | } 65 | 66 | // Sounds returns the Sound keys from the given list of keys 67 | func Sounds(in []*ari.Key) (out []*ari.Key) { 68 | return Kind(ari.SoundKey, in) 69 | } 70 | 71 | // StoredRecordings returns the StoredRecording keys from the given list of keys 72 | func StoredRecordings(in []*ari.Key) (out []*ari.Key) { 73 | return Kind(ari.StoredRecordingKey, in) 74 | } 75 | 76 | // Variables returns the Variable keys from the given list of keys 77 | func Variables(in []*ari.Key) (out []*ari.Key) { 78 | return Kind(ari.VariableKey, in) 79 | } 80 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // Config represents a transport to the asterisk 10 | // config ARI resource. 11 | type Config interface { 12 | // Get gets the reference to a config object 13 | Get(key *Key) *ConfigHandle 14 | 15 | // Data gets the data for the config object 16 | Data(key *Key) (*ConfigData, error) 17 | 18 | // Update creates or updates the given tuples 19 | Update(key *Key, tuples []ConfigTuple) error 20 | 21 | // Delete deletes the dynamic configuration object. 22 | Delete(key *Key) error 23 | } 24 | 25 | // ConfigData contains the data for a given configuration object 26 | type ConfigData struct { 27 | // Key is the cluster-unique identifier for this configuration 28 | Key *Key `json:"key"` 29 | 30 | Class string 31 | Type string 32 | Name string 33 | 34 | Fields []ConfigTuple 35 | } 36 | 37 | // ID returns the ID of the ConfigData structure 38 | func (cd *ConfigData) ID() string { 39 | return fmt.Sprintf("%s/%s/%s", cd.Class, cd.Type, cd.Name) 40 | } 41 | 42 | // ConfigTupleList wrap a list for asterisk ari require. 43 | type ConfigTupleList struct { 44 | Fields []ConfigTuple `json:"fields"` 45 | } 46 | 47 | // ConfigTuple is the key-value pair that defines a configuration entry 48 | type ConfigTuple struct { 49 | Attribute string `json:"attribute"` 50 | Value string `json:"value"` 51 | } 52 | 53 | // A ConfigHandle is a reference to a Config object 54 | // on the asterisk service 55 | type ConfigHandle struct { 56 | key *Key 57 | 58 | c Config 59 | } 60 | 61 | // NewConfigHandle builds a new config handle 62 | func NewConfigHandle(key *Key, c Config) *ConfigHandle { 63 | return &ConfigHandle{ 64 | key: key, 65 | c: c, 66 | } 67 | } 68 | 69 | // ID returns the unique identifier for the config object 70 | func (h *ConfigHandle) ID() string { 71 | return h.key.ID 72 | } 73 | 74 | // Data gets the current data for the config handle 75 | func (h *ConfigHandle) Data() (*ConfigData, error) { 76 | return h.c.Data(h.key) 77 | } 78 | 79 | // Update creates or updates the given config tuples 80 | func (h *ConfigHandle) Update(tuples []ConfigTuple) error { 81 | return h.c.Update(h.key, tuples) 82 | } 83 | 84 | // Delete deletes the dynamic configuration object 85 | func (h *ConfigHandle) Delete() error { 86 | return h.c.Delete(h.key) 87 | } 88 | 89 | // ParseConfigID parses the provided Config ID into its Class, Type, and ID components 90 | func ParseConfigID(input string) (class, kind, id string, err error) { 91 | pieces := strings.Split(input, "/") 92 | if len(pieces) < 3 { 93 | err = errors.New("invalid input ID") 94 | return 95 | } 96 | 97 | return pieces[0], pieces[1], pieces[2], nil 98 | } 99 | -------------------------------------------------------------------------------- /client/native/application.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rotisserie/eris" 7 | 8 | "github.com/CyCoreSystems/ari/v6" 9 | ) 10 | 11 | // Application is a native implementation of ARI's Application functions 12 | type Application struct { 13 | client *Client 14 | } 15 | 16 | // Get returns a managed handle to an ARI application 17 | func (a *Application) Get(key *ari.Key) *ari.ApplicationHandle { 18 | return ari.NewApplicationHandle(a.client.stamp(key), a) 19 | } 20 | 21 | // List returns the list of applications managed by asterisk 22 | func (a *Application) List(filter *ari.Key) (ax []*ari.Key, err error) { 23 | if filter == nil { 24 | filter = ari.NewKey(ari.ApplicationKey, "") 25 | } 26 | 27 | apps := []struct { 28 | Name string `json:"name"` 29 | }{} 30 | 31 | err = a.client.get("/applications", &apps) 32 | 33 | for _, i := range apps { 34 | k := a.client.stamp(ari.NewKey(ari.ApplicationKey, i.Name)) 35 | if filter.Match(k) { 36 | ax = append(ax, k) 37 | } 38 | } 39 | 40 | err = eris.Wrap(err, "Error listing applications") 41 | 42 | return 43 | } 44 | 45 | // Data returns the details of a given ARI application 46 | // Equivalent to GET /applications/{applicationName} 47 | func (a *Application) Data(key *ari.Key) (*ari.ApplicationData, error) { 48 | if key == nil || key.ID == "" { 49 | return nil, eris.New("application key not supplied") 50 | } 51 | 52 | data := new(ari.ApplicationData) 53 | if err := a.client.get("/applications/"+key.ID, data); err != nil { 54 | return nil, dataGetError(err, "application", "%v", key.ID) 55 | } 56 | 57 | data.Key = a.client.stamp(key) 58 | 59 | return data, nil 60 | } 61 | 62 | // Subscribe subscribes the given application to an event source 63 | // Equivalent to POST /applications/{applicationName}/subscription 64 | func (a *Application) Subscribe(key *ari.Key, eventSource string) error { 65 | req := struct { 66 | EventSource string `json:"eventSource"` 67 | }{ 68 | EventSource: eventSource, 69 | } 70 | 71 | err := a.client.post("/applications/"+key.ID+"/subscription", nil, &req) 72 | 73 | return eris.Wrapf(err, "Error subscribing application '%v' for event source '%v'", key.ID, eventSource) 74 | } 75 | 76 | // Unsubscribe unsubscribes (removes a subscription to) a given 77 | // ARI application from the provided event source 78 | // Equivalent to DELETE /applications/{applicationName}/subscription 79 | func (a *Application) Unsubscribe(key *ari.Key, eventSource string) error { 80 | name := key.ID 81 | err := a.client.del("/applications/"+name+"/subscription", nil, fmt.Sprintf("eventSource=%s", eventSource)) 82 | 83 | return eris.Wrapf(err, "Error unsubscribing application '%v' for event source '%v'", name, eventSource) 84 | } 85 | -------------------------------------------------------------------------------- /asterisk.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // Asterisk represents a communication path for 4 | // the Asterisk server for system-level resources 5 | type Asterisk interface { 6 | // Info gets data about the asterisk system 7 | Info(key *Key) (*AsteriskInfo, error) 8 | 9 | // Variables returns the global asterisk variables 10 | Variables() AsteriskVariables 11 | 12 | // Logging returns the interface for working with asterisk logs 13 | Logging() Logging 14 | 15 | // Modules returns the interface for working with asterisk modules 16 | Modules() Modules 17 | 18 | // Config returns the interface for working with dynamic configuration 19 | Config() Config 20 | } 21 | 22 | // AsteriskInfo describes a running asterisk system 23 | type AsteriskInfo struct { 24 | BuildInfo BuildInfo `json:"build"` 25 | ConfigInfo ConfigInfo `json:"config"` 26 | StatusInfo StatusInfo `json:"status"` 27 | SystemInfo SystemInfo `json:"system"` 28 | } 29 | 30 | // BuildInfo describes information about how Asterisk was built 31 | type BuildInfo struct { 32 | Date string `json:"date"` 33 | Kernel string `json:"kernel"` 34 | Machine string `json:"machine"` 35 | Options string `json:"options"` 36 | Os string `json:"os"` 37 | User string `json:"user"` 38 | } 39 | 40 | // ConfigInfo describes information about the Asterisk configuration 41 | type ConfigInfo struct { 42 | DefaultLanguage string `json:"default_language"` 43 | MaxChannels int `json:"max_channels,omitempty"` // omitempty denotes an optional field, meaning the field may not be present if no value is assigned. 44 | MaxLoad float64 `json:"max_load,omitempty"` 45 | MaxOpenFiles int `json:"max_open_files,omitempty"` 46 | Name string `json:"name"` // Asterisk system name 47 | SetID SetID `json:"setid"` // Effective user/group id under which Asterisk is running 48 | } 49 | 50 | // SetID describes a userid/groupid pair 51 | type SetID struct { 52 | Group string `json:"group"` // group id (not name? why string?) 53 | User string `json:"user"` // user id (not name? why string?) 54 | } 55 | 56 | // StatusInfo describes the state of an Asterisk system 57 | type StatusInfo struct { 58 | LastReloadTime DateTime `json:"last_reload_time"` 59 | StartupTime DateTime `json:"startup_time"` 60 | } 61 | 62 | // SystemInfo describes information about the Asterisk system 63 | type SystemInfo struct { 64 | EntityID string `json:"entity_id"` 65 | Version string `json:"version"` 66 | } 67 | 68 | // AsteriskVariables is an interface to interact with Asterisk global variables 69 | type AsteriskVariables interface { 70 | // Get returns the value of the given variable; the ID field of the Key is the variable name 71 | Get(key *Key) (string, error) 72 | 73 | // Set sets the variable; the ID field of the Key is the variable name 74 | Set(key *Key, value string) error 75 | } 76 | -------------------------------------------------------------------------------- /client/native/logging.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "github.com/rotisserie/eris" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // Logging provides the ARI Logging accessors for a native client 10 | type Logging struct { 11 | client *Client 12 | } 13 | 14 | // Create creates a logging level 15 | func (l *Logging) Create(key *ari.Key, levels string) (*ari.LogHandle, error) { 16 | req := struct { 17 | Levels string `json:"configuration"` 18 | }{ 19 | Levels: levels, 20 | } 21 | 22 | err := l.client.post("/asterisk/logging/"+key.ID, nil, &req) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | return l.Get(key), nil 28 | } 29 | 30 | // Get returns a logging channel handle 31 | func (l *Logging) Get(key *ari.Key) *ari.LogHandle { 32 | return ari.NewLogHandle(l.client.stamp(key), l) 33 | } 34 | 35 | func (l *Logging) getLoggingChannels() ([]*ari.LogData, error) { 36 | var ld []*ari.LogData 37 | 38 | err := l.client.get("/asterisk/logging", &ld) 39 | 40 | return ld, err 41 | } 42 | 43 | // Data returns the data of a logging channel 44 | func (l *Logging) Data(key *ari.Key) (*ari.LogData, error) { 45 | if key == nil || key.ID == "" { 46 | return nil, eris.New("logging key not supplied") 47 | } 48 | 49 | logChannels, err := l.getLoggingChannels() 50 | if err != nil { 51 | return nil, eris.Wrap(err, "failed to get list of logging channels") 52 | } 53 | 54 | for _, i := range logChannels { 55 | if i.Name == key.ID { 56 | i.Key = l.client.stamp(key) 57 | return i, nil 58 | } 59 | } 60 | 61 | return nil, eris.New("not found") 62 | } 63 | 64 | // List lists the logging entities 65 | func (l *Logging) List(filter *ari.Key) ([]*ari.Key, error) { 66 | ld, err := l.getLoggingChannels() 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | if filter == nil { 72 | filter = ari.NodeKey(l.client.ApplicationName(), l.client.node) 73 | } 74 | 75 | var ret []*ari.Key 76 | 77 | for _, i := range ld { 78 | k := ari.NewKey(ari.LoggingKey, i.Name, ari.WithApp(l.client.ApplicationName()), ari.WithNode(l.client.node)) 79 | if filter.Match(k) { 80 | ret = append(ret, k) 81 | } 82 | } 83 | 84 | return ret, nil 85 | } 86 | 87 | // Rotate rotates the given log 88 | func (l *Logging) Rotate(key *ari.Key) error { 89 | name := key.ID 90 | if name == "" { 91 | return eris.New("Not allowed to rotate unnamed channels") 92 | } 93 | 94 | return l.client.put("/asterisk/logging/"+name+"/rotate", nil, nil) 95 | } 96 | 97 | // Delete deletes the named log 98 | func (l *Logging) Delete(key *ari.Key) error { 99 | name := key.ID 100 | if name == "" { 101 | return eris.New("Not allowed to delete unnamed channels") 102 | } 103 | 104 | return l.client.del("/asterisk/logging/"+name, nil, "") 105 | } 106 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "mono devshell"; 3 | inputs = { 4 | #nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11"; 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils, ... }@inputs: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = nixpkgs.legacyPackages.${system}; 13 | 14 | write-mailmap = pkgs.buildGoModule rec { 15 | name = "write_mailmap"; 16 | src = pkgs.fetchFromGitHub { 17 | owner = "CyCoreSystems"; 18 | repo = "write_mailmap"; 19 | rev = "v0.3.0"; 20 | hash = "sha256-LzLLEtsWLeIOnlY1pygAOhTsGiWfISnuVF/jeoHHzaw="; 21 | }; 22 | 23 | # There are no upstream packages, so vendor hash is null. 24 | vendorHash = null; 25 | }; 26 | 27 | gci = pkgs.buildGoModule rec { 28 | name = "gci"; 29 | src = pkgs.fetchFromGitHub { 30 | owner = "daixiang0"; 31 | repo = "gci"; 32 | rev = "v0.10.1"; 33 | sha256 = "sha256-/YR61lovuYw+GEeXIgvyPbesz2epmQVmSLWjWwKT4Ag="; 34 | }; 35 | 36 | # Switch to fake vendor sha for upgrades: 37 | #vendorSha256 = pkgs.lib.fakeSha256; 38 | vendorHash = "sha256-g7htGfU6C2rzfu8hAn6SGr0ZRwB8ZzSf9CgHYmdupE8="; 39 | }; 40 | 41 | cclint = pkgs.writeScriptBin "lint" '' 42 | cd $(git rev-parse --show-toplevel) 43 | write_mailmap > CONTRIBUTORS 44 | gofumpt -w . 45 | gci write --skip-generated -s standard -s default -s "Prefix(github.com/CyCoreSystems)" . 46 | golangci-lint run 47 | golangci-lint run ./_examples/bridge 48 | golangci-lint run ./_examples/helloworld 49 | golangci-lint run ./_examples/play 50 | golangci-lint run ./_examples/record 51 | golangci-lint run ./_examples/stasisStart 52 | golangci-lint run ./_examples/twoapps 53 | golangci-lint run ./client/native 54 | golangci-lint run ./ext/audiouri 55 | golangci-lint run ./ext/bridgemon 56 | golangci-lint run ./ext/keyfilter 57 | golangci-lint run ./ext/play 58 | golangci-lint run ./ext/record 59 | golangci-lint run ./rid 60 | golangci-lint run ./stdbus 61 | ''; 62 | 63 | ccmocks = pkgs.writeScriptBin "gen-mocks" '' 64 | rm -Rf client/arimocks 65 | mockery 66 | ''; 67 | in 68 | { 69 | devShells.default = pkgs.mkShell { 70 | packages = with pkgs; [ 71 | buf 72 | cclint 73 | ccmocks 74 | gci 75 | go-tools 76 | write-mailmap 77 | ]; 78 | }; 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /client/native/storedRecording.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // StoredRecording provides the ARI StoredRecording accessors for the native client 10 | type StoredRecording struct { 11 | client *Client 12 | } 13 | 14 | // List lists the current stored recordings and returns a list of handles 15 | func (sr *StoredRecording) List(filter *ari.Key) (sx []*ari.Key, err error) { 16 | var recs []struct { 17 | Name string `json:"name"` 18 | } 19 | 20 | if filter == nil { 21 | filter = sr.client.stamp(ari.NewKey(ari.StoredRecordingKey, "")) 22 | } 23 | 24 | err = sr.client.get("/recordings/stored", &recs) 25 | 26 | for _, rec := range recs { 27 | k := sr.client.stamp(ari.NewKey(ari.StoredRecordingKey, rec.Name)) 28 | if filter.Match(k) { 29 | sx = append(sx, k) 30 | } 31 | } 32 | 33 | return 34 | } 35 | 36 | // Get gets a lazy handle for the given stored recording name 37 | func (sr *StoredRecording) Get(key *ari.Key) *ari.StoredRecordingHandle { 38 | return ari.NewStoredRecordingHandle(key, sr, nil) 39 | } 40 | 41 | // Data retrieves the state of the stored recording 42 | func (sr *StoredRecording) Data(key *ari.Key) (*ari.StoredRecordingData, error) { 43 | if key == nil || key.ID == "" { 44 | return nil, errors.New("storedRecording key not supplied") 45 | } 46 | 47 | data := new(ari.StoredRecordingData) 48 | if err := sr.client.get("/recordings/stored/"+key.ID, data); err != nil { 49 | return nil, dataGetError(err, "storedRecording", "%v", key.ID) 50 | } 51 | 52 | data.Key = sr.client.stamp(key) 53 | 54 | return data, nil 55 | } 56 | 57 | // Copy copies a stored recording and returns the new handle 58 | func (sr *StoredRecording) Copy(key *ari.Key, dest string) (*ari.StoredRecordingHandle, error) { 59 | h, err := sr.StageCopy(key, dest) 60 | if err != nil { 61 | // NOTE: return the handle even on failure so that it can be used to 62 | // delete the existing stored recording, should the Copy fail. 63 | // ARI provides no facility to force-copy a recording. 64 | return h, err 65 | } 66 | 67 | return h, h.Exec() 68 | } 69 | 70 | // StageCopy creates a `StoredRecordingHandle` with a `Copy` operation staged. 71 | func (sr *StoredRecording) StageCopy(key *ari.Key, dest string) (*ari.StoredRecordingHandle, error) { 72 | var resp struct { 73 | Name string `json:"name"` 74 | } 75 | 76 | req := struct { 77 | Dest string `json:"destinationRecordingName"` 78 | }{ 79 | Dest: dest, 80 | } 81 | 82 | destKey := sr.client.stamp(ari.NewKey(ari.StoredRecordingKey, dest)) 83 | 84 | return ari.NewStoredRecordingHandle(destKey, sr, func(h *ari.StoredRecordingHandle) error { 85 | return sr.client.post("/recordings/stored/"+key.ID+"/copy", &resp, &req) 86 | }), nil 87 | } 88 | 89 | // Delete deletes the stored recording 90 | func (sr *StoredRecording) Delete(key *ari.Key) error { 91 | return sr.client.del("/recordings/stored/"+key.ID, nil, "") 92 | } 93 | -------------------------------------------------------------------------------- /key_test.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import "testing" 4 | 5 | func TestKeyMatch(t *testing.T) { 6 | // two empty keys should match 7 | ok := NewKey("", "").Match(NewKey("", "")) 8 | if !ok { 9 | t.Errorf("Two empty keys should match") 10 | } 11 | 12 | ok = AppKey("app").Match(NewKey("", "")) 13 | if !ok { 14 | t.Errorf("App key should match any subkey") 15 | } 16 | 17 | ok = AppKey("app").Match(AppKey("app2")) 18 | if ok { 19 | t.Errorf("Two separate app keys should not match") 20 | } 21 | 22 | ok = NodeKey("app", "node").Match(NewKey("", "")) 23 | if !ok { 24 | t.Errorf("Node key should match any subkey") 25 | } 26 | 27 | ok = NewKey("application", "id1").Match(NewKey("application", "id1")) 28 | if !ok { 29 | t.Errorf("Application/id1 should match") 30 | } 31 | 32 | ok = NewKey("application", "").Match(NewKey("application", "id1")) 33 | if !ok { 34 | t.Errorf("Application/* should match application/id") 35 | } 36 | 37 | ok = NewKey("channel", "id1").Match(NewKey("channel", "id2")) 38 | if ok { 39 | t.Errorf("Differing IDs should not match") 40 | } 41 | } 42 | 43 | func TestKeysFilter(t *testing.T) { 44 | keys := Keys{ 45 | NewKey(ApplicationKey, "app1"), 46 | NewKey(ChannelKey, "ch1"), 47 | NewKey(BridgeKey, "br1"), 48 | } 49 | 50 | newKeys := keys.Filter(KindKey(ApplicationKey)) 51 | 52 | if len(newKeys) != 1 { 53 | t.Errorf("Expected filters keys by app to be of length 1, got %d", len(newKeys)) 54 | } else { 55 | if newKeys[0].Kind != ApplicationKey && newKeys[0].ID != "app1" { 56 | t.Errorf("Unexpected first index %v", newKeys[0]) 57 | } 58 | } 59 | 60 | newKeys = keys.Without(KindKey(ApplicationKey)) 61 | 62 | if len(newKeys) != 2 { 63 | t.Errorf("Expected without keys by app to be of length 2, got %d", len(newKeys)) 64 | } else { 65 | if newKeys[0].Kind != ChannelKey && newKeys[0].ID != "ch1" { 66 | t.Errorf("Unexpected first index %v", newKeys[0]) 67 | } 68 | 69 | if newKeys[1].Kind != BridgeKey && newKeys[1].ID != "br1" { 70 | t.Errorf("Unexpected second index %v", newKeys[1]) 71 | } 72 | } 73 | 74 | newKeys = keys.Filter(KindKey(ChannelKey), KindKey(BridgeKey)) 75 | 76 | if len(newKeys) != 2 { 77 | t.Errorf("Expected without keys by app to be of length 2, got %d", len(newKeys)) 78 | } else { 79 | if newKeys[0].Kind != ChannelKey && newKeys[0].ID != "ch1" { 80 | t.Errorf("Unexpected first index %v", newKeys[0]) 81 | } 82 | 83 | if newKeys[1].Kind != BridgeKey && newKeys[1].ID != "br1" { 84 | t.Errorf("Unexpected second index %v", newKeys[1]) 85 | } 86 | } 87 | 88 | newKeys = keys.Filter(KindKey(ChannelKey), KindKey(BridgeKey)).Without(MatchFunc(func(k *Key) bool { 89 | return k.ID == "br1" 90 | })) 91 | 92 | if len(newKeys) != 1 { 93 | t.Errorf("Expected without keys by app to be of length 2, got %d", len(newKeys)) 94 | } else { 95 | if newKeys[0].Kind != ChannelKey && newKeys[0].ID != "ch1" { 96 | t.Errorf("Unexpected first index %v", newKeys[0]) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /ari.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package asterisk.ari; 4 | 5 | option go_package = "ari"; 6 | 7 | import "google/protobuf/timestamp.proto"; 8 | 9 | // Key identifies a unique resource in the system 10 | message Key { 11 | // Kind indicates the type of resource the Key points to. e.g., "channel", 12 | // "bridge", etc. 13 | string kind = 1; 14 | 15 | // ID indicates the unique identifier of the resource 16 | string id = 2; 17 | 18 | // Node indicates the unique identifier of the Asterisk node on which the 19 | // resource exists or will be created 20 | string node = 3; 21 | 22 | // Dialog indicates a named scope of the resource, for receiving events 23 | string dialog = 4; 24 | 25 | // App indiciates the ARI application that this key is bound to. 26 | string app = 5; 27 | } 28 | 29 | // CallerID describes the name and number which identifies the caller to other endpoints 30 | message CallerID { 31 | // Name is the name of the party 32 | string name = 1; 33 | 34 | // Number is the number of the party 35 | string number = 2; 36 | } 37 | 38 | // ChannelData describes the data for a specific channel 39 | message ChannelData { 40 | // Key is the key of the channel 41 | Key key = 1; 42 | 43 | // Id is the unique ID for this channel (AMI-style) 44 | string id = 2; 45 | 46 | // Name is the name of this channel (tect/name-id) 47 | string name = 3; 48 | 49 | // State is the current state of the channel 50 | string state = 4; 51 | 52 | // Accountcode is the account code assigned to the channel 53 | string accountcode = 5; 54 | 55 | // Caller is the callerID of the calling endpoint 56 | CallerID caller = 6; 57 | 58 | // Connected is the callerID of the connected line, if applicable 59 | CallerID connected = 7; 60 | 61 | // Creationtime is the time at which the channel was created 62 | google.protobuf.Timestamp creationtime = 8; 63 | 64 | // Dialplan is the current location of the channel in the dialplan 65 | DialplanCEP dialplan = 9; 66 | 67 | // Language is the default spoken language for this channel 68 | string language = 10; 69 | 70 | // ChannelVars is the list of channel variables set on this channel 71 | map channel_vars = 11; 72 | 73 | // ProtocolId is the protocol id from the underlying channel driver 74 | // For chan_sip and PJSIP this will be the SIP packets Call-ID value 75 | // Empty if not applicable or implemented by the driver 76 | string protocol_id = 12; 77 | } 78 | 79 | // Dialplan describes a location in the Asterisk dialplan 80 | message DialplanCEP { 81 | 82 | // Context describes the section in the dialplan 83 | string context = 1; 84 | 85 | // Exten describes the label in the section of the dialplan 86 | string exten = 2; 87 | 88 | // Priority indicates the index at the label in the section of the dialplan 89 | int64 priority = 3; 90 | 91 | // AppName indicates the current dialplan application name 92 | string app_name = 4; 93 | 94 | // AppData indicates all data parameters passed to the dialplan AppName 95 | string app_data = 5; 96 | } 97 | -------------------------------------------------------------------------------- /client/native/liveRecording.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // LiveRecording provides the ARI LiveRecording accessors for the native client 10 | type LiveRecording struct { 11 | client *Client 12 | } 13 | 14 | // Get gets a lazy handle for the live recording name 15 | func (lr *LiveRecording) Get(key *ari.Key) (h *ari.LiveRecordingHandle) { 16 | h = ari.NewLiveRecordingHandle(lr.client.stamp(key), lr, nil) 17 | return 18 | } 19 | 20 | // Data retrieves the state of the live recording 21 | func (lr *LiveRecording) Data(key *ari.Key) (d *ari.LiveRecordingData, err error) { 22 | if key == nil || key.ID == "" { 23 | return nil, errors.New("liveRecording key not supplied") 24 | } 25 | 26 | data := new(ari.LiveRecordingData) 27 | if err := lr.client.get("/recordings/live/"+key.ID, data); err != nil { 28 | return nil, dataGetError(err, "liveRecording", "%v", key.ID) 29 | } 30 | 31 | data.Key = lr.client.stamp(key) 32 | 33 | return data, nil 34 | } 35 | 36 | // Stop stops the live recording. 37 | // 38 | // NOTE: if the recording is already stopped, this will return an error. 39 | func (lr *LiveRecording) Stop(key *ari.Key) error { 40 | if key == nil || key.ID == "" { 41 | return errors.New("liveRecording key not supplied") 42 | } 43 | 44 | return lr.client.post("/recordings/live/"+key.ID+"/stop", nil, nil) 45 | } 46 | 47 | // Pause pauses the live recording (TODO: does it error if the live recording is already paused) 48 | func (lr *LiveRecording) Pause(key *ari.Key) error { 49 | return lr.client.post("/recordings/live/"+key.ID+"/pause", nil, nil) 50 | } 51 | 52 | // Resume resumes the live recording (TODO: does it error if the live recording is already resumed) 53 | func (lr *LiveRecording) Resume(key *ari.Key) error { 54 | return lr.client.del("/recordings/live/"+key.ID+"/pause", nil, "") 55 | } 56 | 57 | // Mute mutes the live recording (TODO: does it error if the live recording is already muted) 58 | func (lr *LiveRecording) Mute(key *ari.Key) error { 59 | return lr.client.post("/recordings/live/"+key.ID+"/mute", nil, nil) 60 | } 61 | 62 | // Unmute unmutes the live recording (TODO: does it error if the live recording is already muted) 63 | func (lr *LiveRecording) Unmute(key *ari.Key) error { 64 | return lr.client.del("/recordings/live/"+key.ID+"/mute", nil, "") 65 | } 66 | 67 | // Scrap removes a live recording (TODO: describe difference between scrap and delete) 68 | func (lr *LiveRecording) Scrap(key *ari.Key) error { 69 | return lr.client.del("/recordings/live/"+key.ID, nil, "") 70 | } 71 | 72 | // Stored returns the StoredRecording handle for the given LiveRecording 73 | func (lr *LiveRecording) Stored(key *ari.Key) *ari.StoredRecordingHandle { 74 | return ari.NewStoredRecordingHandle( 75 | lr.client.stamp(key.New(ari.StoredRecordingKey, key.ID)), 76 | lr.client.StoredRecording(), 77 | nil, 78 | ) 79 | } 80 | 81 | // Subscribe is a shim to enable recording handles to subscribe to their 82 | // underlying bridge/channel for events. It should not be called directly. 83 | func (lr *LiveRecording) Subscribe(key *ari.Key, n ...string) ari.Subscription { 84 | return lr.client.Bus().Subscribe(key, n...) 85 | } 86 | -------------------------------------------------------------------------------- /ext/play/sequence.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/rotisserie/eris" 8 | 9 | "github.com/CyCoreSystems/ari/v6" 10 | "github.com/CyCoreSystems/ari/v6/rid" 11 | ) 12 | 13 | // sequence represents an audio sequence playback session 14 | type sequence struct { 15 | cancel context.CancelFunc 16 | s *playSession 17 | 18 | done chan struct{} 19 | } 20 | 21 | func (s *sequence) Done() <-chan struct{} { 22 | return s.done 23 | } 24 | 25 | func (s *sequence) Stop() { 26 | if s.cancel != nil { 27 | s.cancel() 28 | } 29 | } 30 | 31 | func newSequence(s *playSession) *sequence { 32 | return &sequence{ 33 | s: s, 34 | done: make(chan struct{}), 35 | } 36 | } 37 | 38 | func (s *sequence) Play(ctx context.Context, p ari.Player, playbackCounter int) { 39 | ctx, cancel := context.WithCancel(ctx) 40 | s.cancel = cancel 41 | 42 | defer cancel() 43 | defer close(s.done) 44 | 45 | if playbackCounter > 0 && !s.s.o.invalidPrependUriList.Empty() { 46 | for u := s.s.o.invalidPrependUriList.First(); u != ""; u = s.s.o.invalidPrependUriList.Next() { 47 | pb, err := p.StagePlay(rid.New(rid.Playback), u) 48 | if err != nil { 49 | s.s.result.Status = Failed 50 | s.s.result.Error = eris.Wrap(err, "failed to stage playback") 51 | 52 | return 53 | } 54 | 55 | s.s.result.Status, err = playStaged(ctx, pb, s.s.o.playbackStartTimeout) 56 | if err != nil { 57 | s.s.result.Error = eris.Wrap(err, "failure in playback") 58 | 59 | return 60 | } 61 | } 62 | } 63 | 64 | for u := s.s.o.uriList.First(); u != ""; u = s.s.o.uriList.Next() { 65 | pb, err := p.StagePlay(rid.New(rid.Playback), u) 66 | if err != nil { 67 | s.s.result.Status = Failed 68 | s.s.result.Error = eris.Wrap(err, "failed to stage playback") 69 | 70 | return 71 | } 72 | 73 | s.s.result.Status, err = playStaged(ctx, pb, s.s.o.playbackStartTimeout) 74 | if err != nil { 75 | s.s.result.Error = eris.Wrap(err, "failure in playback") 76 | 77 | return 78 | } 79 | } 80 | } 81 | 82 | // playStaged executes a staged playback, waiting for its completion 83 | func playStaged(ctx context.Context, h *ari.PlaybackHandle, timeout time.Duration) (Status, error) { 84 | started := h.Subscribe(ari.Events.PlaybackStarted) 85 | defer started.Cancel() 86 | 87 | finished := h.Subscribe(ari.Events.PlaybackFinished) 88 | defer finished.Cancel() 89 | 90 | if timeout == 0 { 91 | timeout = DefaultPlaybackStartTimeout 92 | } 93 | 94 | if err := h.Exec(); err != nil { 95 | return Failed, eris.Wrap(err, "failed to start playback") 96 | } 97 | 98 | defer h.Stop() // nolint: errcheck 99 | 100 | select { 101 | case <-ctx.Done(): 102 | return Cancelled, nil 103 | case <-started.Events(): 104 | case <-finished.Events(): 105 | return Finished, nil 106 | case <-time.After(timeout): 107 | return Timeout, eris.New("timeout waiting for playback to start") 108 | } 109 | 110 | // Wait for playback to complete 111 | select { 112 | case <-ctx.Done(): 113 | return Cancelled, nil 114 | case <-finished.Events(): 115 | return Finished, nil 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /storedRecording.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // StoredRecording represents a communication path interacting with an Asterisk 4 | // server for stored recording resources 5 | type StoredRecording interface { 6 | // List lists the recordings 7 | List(filter *Key) ([]*Key, error) 8 | 9 | // Get gets the Recording by type 10 | Get(key *Key) *StoredRecordingHandle 11 | 12 | // data gets the data for the stored recording 13 | Data(key *Key) (*StoredRecordingData, error) 14 | 15 | // Copy copies the recording to the destination name 16 | // 17 | // NOTE: because ARI offers no forced-copy, Copy should always return the 18 | // StoredRecordingHandle of the destination, even if the Copy fails. Doing so 19 | // allows the user to Delete the existing StoredRecording before retrying. 20 | Copy(key *Key, dest string) (*StoredRecordingHandle, error) 21 | 22 | // Delete deletes the recording 23 | Delete(key *Key) error 24 | } 25 | 26 | // StoredRecordingData is the data for a stored recording 27 | type StoredRecordingData struct { 28 | // Key is the cluster-unique identifier for this stored recording 29 | Key *Key `json:"key"` 30 | 31 | Format string `json:"format"` 32 | Name string `json:"name"` 33 | } 34 | 35 | // ID returns the identifier for the stored recording. 36 | func (d StoredRecordingData) ID() string { 37 | return d.Name // TODO: does the identifier include the Format and Name? 38 | } 39 | 40 | // A StoredRecordingHandle is a reference to a stored recording that can be operated on 41 | type StoredRecordingHandle struct { 42 | key *Key 43 | s StoredRecording 44 | exec func(a *StoredRecordingHandle) error 45 | executed bool 46 | } 47 | 48 | // NewStoredRecordingHandle creates a new stored recording handle 49 | func NewStoredRecordingHandle(key *Key, s StoredRecording, exec func(a *StoredRecordingHandle) error) *StoredRecordingHandle { 50 | return &StoredRecordingHandle{ 51 | key: key, 52 | s: s, 53 | exec: exec, 54 | } 55 | } 56 | 57 | // ID returns the identifier for the stored recording 58 | func (s *StoredRecordingHandle) ID() string { 59 | return s.key.ID 60 | } 61 | 62 | // Key returns the Key for the stored recording 63 | func (s *StoredRecordingHandle) Key() *Key { 64 | return s.key 65 | } 66 | 67 | // Exec executes any staged operations 68 | func (s *StoredRecordingHandle) Exec() (err error) { 69 | if !s.executed { 70 | s.executed = true 71 | if s.exec != nil { 72 | err = s.exec(s) 73 | s.exec = nil 74 | } 75 | } 76 | 77 | return 78 | } 79 | 80 | // Data gets the data for the stored recording 81 | func (s *StoredRecordingHandle) Data() (*StoredRecordingData, error) { 82 | return s.s.Data(s.key) 83 | } 84 | 85 | // Copy copies the stored recording. 86 | // 87 | // NOTE: because ARI offers no forced-copy, this should always return the 88 | // StoredRecordingHandle of the destination, even if the Copy fails. Doing so 89 | // allows the user to Delete the existing StoredRecording before retrying. 90 | func (s *StoredRecordingHandle) Copy(dest string) (*StoredRecordingHandle, error) { 91 | return s.s.Copy(s.key, dest) 92 | } 93 | 94 | // Delete deletes the recording 95 | func (s *StoredRecordingHandle) Delete() error { 96 | return s.s.Delete(s.key) 97 | } 98 | -------------------------------------------------------------------------------- /ext/play/example_test.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/CyCoreSystems/ari/v6" 9 | "github.com/CyCoreSystems/ari/v6/client/arimocks" 10 | ) 11 | 12 | func ExamplePlay() { 13 | c := &arimocks.Client{} 14 | key := ari.NewKey(ari.ChannelKey, "exampleChannel") 15 | h := ari.NewChannelHandle(key, c.Channel(), nil) 16 | 17 | res, err := Play(context.TODO(), h, 18 | URI("sound:tt-monkeys", "sound:vm-goodbye"), 19 | ).Result() 20 | if err != nil { 21 | fmt.Println("Failed to play audio", err) 22 | return 23 | } 24 | 25 | if len(res.DTMF) > 0 { 26 | fmt.Println("Got a DTMF during playback:", res.DTMF) 27 | } 28 | } 29 | 30 | func ExamplePlay_async() { 31 | c := &arimocks.Client{} 32 | key := ari.NewKey(ari.ChannelKey, "exampleChannel") 33 | h := ari.NewChannelHandle(key, c.Channel(), nil) 34 | 35 | bridgeSub := h.Subscribe(ari.Events.ChannelEnteredBridge) 36 | defer bridgeSub.Cancel() 37 | 38 | sess := Play(context.TODO(), h, 39 | URI("characters:ded", "sound:tt-monkeys", 40 | "number:192846", "digits:43"), 41 | ) 42 | 43 | select { 44 | case <-bridgeSub.Events(): 45 | fmt.Println("Channel entered bridge during playback") 46 | case <-sess.Done(): 47 | if sess.Err() != nil { 48 | fmt.Println("Prompt failed", sess.Err()) 49 | } else { 50 | fmt.Println("Prompt complete") 51 | } 52 | } 53 | } 54 | 55 | func ExamplePrompt() { 56 | c := &arimocks.Client{} 57 | key := ari.NewKey(ari.ChannelKey, "exampleChannel") 58 | h := ari.NewChannelHandle(key, c.Channel(), nil) 59 | 60 | res, err := Prompt(context.TODO(), h, 61 | URI("tone:1004/250", "sound:vm-enter-num-to-call", 62 | "sound:astcc-followed-by-pound"), 63 | MatchHash(), // match any digits until hash 64 | Replays(3), // repeat prompt up to three times, if no match 65 | ).Result() 66 | if err != nil { 67 | fmt.Println("Failed to play", err) 68 | return 69 | } 70 | 71 | if res.MatchResult == Complete { 72 | fmt.Println("Got valid, terminated DTMF entry", res.DTMF) 73 | } // hash is automatically trimmed from res.DTMF 74 | } 75 | 76 | func ExamplePrompt_custom() { 77 | db := mockDB{} 78 | c := &arimocks.Client{} 79 | key := ari.NewKey(ari.ChannelKey, "exampleChannel") 80 | h := ari.NewChannelHandle(key, c.Channel(), nil) 81 | 82 | res, err := Prompt(context.TODO(), h, 83 | URI("sound:agent-user"), 84 | MatchFunc(func(in string) (string, MatchResult) { 85 | // This is a custom match function which will 86 | // be run each time a DTMF digit is received 87 | pat := strings.TrimSuffix(in, "#") 88 | 89 | user := db.Lookup(pat) 90 | if user == "" { 91 | if pat != in { 92 | // pattern was hash-terminated but no match 93 | // was found, so there is no match possible 94 | return pat, Invalid 95 | } 96 | return in, Incomplete 97 | } 98 | return pat, Complete 99 | }), 100 | ).Result() 101 | if err != nil { 102 | fmt.Println("Failed to play prompt", err) 103 | return 104 | } 105 | 106 | if res.MatchResult == Complete { 107 | fmt.Println("Got valid user", res.DTMF) 108 | } 109 | } 110 | 111 | type mockDB struct{} 112 | 113 | func (m *mockDB) Lookup(user string) string { 114 | return "" 115 | } 116 | -------------------------------------------------------------------------------- /client/arimocks/Subscriber.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery; DO NOT EDIT. 2 | // github.com/vektra/mockery 3 | // template: testify 4 | 5 | package arimocks 6 | 7 | import ( 8 | "github.com/CyCoreSystems/ari/v6" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // NewSubscriber creates a new instance of Subscriber. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 13 | // The first argument is typically a *testing.T value. 14 | func NewSubscriber(t interface { 15 | mock.TestingT 16 | Cleanup(func()) 17 | }) *Subscriber { 18 | mock := &Subscriber{} 19 | mock.Mock.Test(t) 20 | 21 | t.Cleanup(func() { mock.AssertExpectations(t) }) 22 | 23 | return mock 24 | } 25 | 26 | // Subscriber is an autogenerated mock type for the Subscriber type 27 | type Subscriber struct { 28 | mock.Mock 29 | } 30 | 31 | type Subscriber_Expecter struct { 32 | mock *mock.Mock 33 | } 34 | 35 | func (_m *Subscriber) EXPECT() *Subscriber_Expecter { 36 | return &Subscriber_Expecter{mock: &_m.Mock} 37 | } 38 | 39 | // Subscribe provides a mock function for the type Subscriber 40 | func (_mock *Subscriber) Subscribe(key *ari.Key, n ...string) ari.Subscription { 41 | // string 42 | _va := make([]interface{}, len(n)) 43 | for _i := range n { 44 | _va[_i] = n[_i] 45 | } 46 | var _ca []interface{} 47 | _ca = append(_ca, key) 48 | _ca = append(_ca, _va...) 49 | ret := _mock.Called(_ca...) 50 | 51 | if len(ret) == 0 { 52 | panic("no return value specified for Subscribe") 53 | } 54 | 55 | var r0 ari.Subscription 56 | if returnFunc, ok := ret.Get(0).(func(*ari.Key, ...string) ari.Subscription); ok { 57 | r0 = returnFunc(key, n...) 58 | } else { 59 | if ret.Get(0) != nil { 60 | r0 = ret.Get(0).(ari.Subscription) 61 | } 62 | } 63 | return r0 64 | } 65 | 66 | // Subscriber_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe' 67 | type Subscriber_Subscribe_Call struct { 68 | *mock.Call 69 | } 70 | 71 | // Subscribe is a helper method to define mock.On call 72 | // - key *ari.Key 73 | // - n ...string 74 | func (_e *Subscriber_Expecter) Subscribe(key interface{}, n ...interface{}) *Subscriber_Subscribe_Call { 75 | return &Subscriber_Subscribe_Call{Call: _e.mock.On("Subscribe", 76 | append([]interface{}{key}, n...)...)} 77 | } 78 | 79 | func (_c *Subscriber_Subscribe_Call) Run(run func(key *ari.Key, n ...string)) *Subscriber_Subscribe_Call { 80 | _c.Call.Run(func(args mock.Arguments) { 81 | var arg0 *ari.Key 82 | if args[0] != nil { 83 | arg0 = args[0].(*ari.Key) 84 | } 85 | var arg1 []string 86 | variadicArgs := make([]string, len(args)-1) 87 | for i, a := range args[1:] { 88 | if a != nil { 89 | variadicArgs[i] = a.(string) 90 | } 91 | } 92 | arg1 = variadicArgs 93 | run( 94 | arg0, 95 | arg1..., 96 | ) 97 | }) 98 | return _c 99 | } 100 | 101 | func (_c *Subscriber_Subscribe_Call) Return(subscription ari.Subscription) *Subscriber_Subscribe_Call { 102 | _c.Call.Return(subscription) 103 | return _c 104 | } 105 | 106 | func (_c *Subscriber_Subscribe_Call) RunAndReturn(run func(key *ari.Key, n ...string) ari.Subscription) *Subscriber_Subscribe_Call { 107 | _c.Call.Return(run) 108 | return _c 109 | } 110 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at sys@cycoresys.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /application.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // Application represents a communication path interacting with an Asterisk 4 | // server for application-level resources 5 | type Application interface { 6 | // List returns the list of applications in Asterisk, optionally using the key for filtering 7 | List(*Key) ([]*Key, error) 8 | 9 | // Get returns a handle to the application for further interaction 10 | Get(key *Key) *ApplicationHandle 11 | 12 | // Data returns the applications data 13 | Data(key *Key) (*ApplicationData, error) 14 | 15 | // Subscribe subscribes the given application to an event source 16 | // event source may be one of: 17 | // - channel: 18 | // - bridge: 19 | // - endpoint:/ (e.g. SIP/102) 20 | // - deviceState: 21 | Subscribe(key *Key, eventSource string) error 22 | 23 | // Unsubscribe unsubscribes (removes a subscription to) a given 24 | // ARI application from the provided event source 25 | // Equivalent to DELETE /applications/{applicationName}/subscription 26 | Unsubscribe(key *Key, eventSource string) error 27 | } 28 | 29 | // ApplicationData describes the data for a Stasis (Ari) application 30 | type ApplicationData struct { 31 | // Key is the unique identifier for this application instance in the cluster 32 | Key *Key `json:"key"` 33 | 34 | BridgeIDs []string `json:"bridge_ids"` // Subscribed BridgeIds 35 | ChannelIDs []string `json:"channel_ids"` // Subscribed ChannelIds 36 | DeviceNames []string `json:"device_names"` // Subscribed Device names 37 | EndpointIDs []string `json:"endpoint_ids"` // Subscribed Endpoints (tech/resource format) 38 | Name string `json:"name"` // Name of the application 39 | } 40 | 41 | // ApplicationHandle provides a wrapper to an Application interface for 42 | // operations on a specific application 43 | type ApplicationHandle struct { 44 | key *Key 45 | a Application 46 | } 47 | 48 | // NewApplicationHandle creates a new handle to the application name 49 | func NewApplicationHandle(key *Key, app Application) *ApplicationHandle { 50 | return &ApplicationHandle{ 51 | key: key, 52 | a: app, 53 | } 54 | } 55 | 56 | // ID returns the identifier for the application 57 | func (ah *ApplicationHandle) ID() string { 58 | return ah.key.ID 59 | } 60 | 61 | // Key returns the key of the application 62 | func (ah *ApplicationHandle) Key() *Key { 63 | return ah.key 64 | } 65 | 66 | // Data retrives the data for the application 67 | func (ah *ApplicationHandle) Data() (ad *ApplicationData, err error) { 68 | ad, err = ah.a.Data(ah.key) 69 | return 70 | } 71 | 72 | // Subscribe subscribes the application to an event source 73 | // event source may be one of: 74 | // - channel: 75 | // - bridge: 76 | // - endpoint:/ (e.g. SIP/102) 77 | // - deviceState: 78 | func (ah *ApplicationHandle) Subscribe(eventSource string) (err error) { 79 | err = ah.a.Subscribe(ah.key, eventSource) 80 | return 81 | } 82 | 83 | // Unsubscribe unsubscribes (removes a subscription to) a given 84 | // ARI application from the provided event source 85 | // Equivalent to DELETE /applications/{applicationName}/subscription 86 | func (ah *ApplicationHandle) Unsubscribe(eventSource string) (err error) { 87 | err = ah.a.Unsubscribe(ah.key, eventSource) 88 | return 89 | } 90 | 91 | // Match returns true fo the event matches the application 92 | func (ah *ApplicationHandle) Match(e Event) bool { 93 | return e.GetApplication() == ah.key.ID 94 | } 95 | -------------------------------------------------------------------------------- /endpoint.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | // EndpointIDSeparator seperates the ID components of the endpoint ID 9 | const EndpointIDSeparator = "|" // TODO: confirm separator isn't terrible 10 | 11 | // Endpoint represents a communication path to an Asterisk server 12 | // for endpoint resources 13 | type Endpoint interface { 14 | // List lists the endpoints 15 | List(filter *Key) ([]*Key, error) 16 | 17 | // List available endpoints for a given endpoint technology 18 | ListByTech(tech string, filter *Key) ([]*Key, error) 19 | 20 | // Get returns a handle to the endpoint for further operations 21 | Get(key *Key) *EndpointHandle 22 | 23 | // Data returns the state of the endpoint 24 | Data(key *Key) (*EndpointData, error) 25 | } 26 | 27 | // NewEndpointKey returns the key for the given endpoint 28 | func NewEndpointKey(tech, resource string, opts ...KeyOptionFunc) *Key { 29 | return NewKey(EndpointKey, endpointKeyID(tech, resource), opts...) 30 | } 31 | 32 | func endpointKeyID(tech, resource string) string { 33 | return tech + "/" + resource 34 | } 35 | 36 | // EndpointData describes an external device which may offer or accept calls 37 | // to or from Asterisk. Devices are defined by a technology/resource pair. 38 | // 39 | // Allowed states: 'unknown', 'offline', 'online' 40 | type EndpointData struct { 41 | // Key is the cluster-unique identifier for this Endpoint 42 | Key *Key `json:"key"` 43 | 44 | ChannelIDs []string `json:"channel_ids"` // List of channel Ids which are associated with this endpoint 45 | Resource string `json:"resource"` // The endpoint's resource name 46 | State string `json:"state,omitempty"` // The state of the endpoint 47 | Technology string `json:"technology"` // The technology of the endpoint (e.g. SIP, PJSIP, DAHDI, etc) 48 | } 49 | 50 | // ID returns the ID for the endpoint 51 | func (ed *EndpointData) ID() string { 52 | return ed.Technology + EndpointIDSeparator + ed.Resource 53 | } 54 | 55 | // FromEndpointID converts the endpoint ID to the tech, resource pair. 56 | func FromEndpointID(id string) (tech string, resource string, err error) { 57 | items := strings.Split(id, EndpointIDSeparator) 58 | if len(items) < 2 { 59 | err = errors.New("Endpoint ID is not in tech" + EndpointIDSeparator + "resource format") 60 | return 61 | } 62 | 63 | if len(items) > 2 { 64 | // huge programmer error here, we want to handle it 65 | // tempted to panic here... 66 | err = errors.New("EndpointIDSeparator is conflicting with tech and resource identifiers") 67 | return 68 | } 69 | 70 | tech = items[0] 71 | resource = items[1] 72 | 73 | return 74 | } 75 | 76 | // NewEndpointHandle creates a new EndpointHandle 77 | func NewEndpointHandle(key *Key, e Endpoint) *EndpointHandle { 78 | return &EndpointHandle{ 79 | key: key, 80 | e: e, 81 | } 82 | } 83 | 84 | // An EndpointHandle is a reference to an endpoint attached to 85 | // a transport to an asterisk server 86 | type EndpointHandle struct { 87 | key *Key 88 | e Endpoint 89 | } 90 | 91 | // ID returns the identifier for the endpoint 92 | func (eh *EndpointHandle) ID() string { 93 | return eh.key.ID 94 | } 95 | 96 | // Key returns the key for the endpoint 97 | func (eh *EndpointHandle) Key() *Key { 98 | return eh.key 99 | } 100 | 101 | // Data returns the state of the endpoint 102 | func (eh *EndpointHandle) Data() (*EndpointData, error) { 103 | return eh.e.Data(eh.key) 104 | } 105 | -------------------------------------------------------------------------------- /playback.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // Playback represents a communication path for interacting 4 | // with an Asterisk server for playback resources 5 | type Playback interface { 6 | // Get gets the handle to the given playback ID 7 | Get(key *Key) *PlaybackHandle 8 | 9 | // Data gets the playback data 10 | Data(key *Key) (*PlaybackData, error) 11 | 12 | // Control performs the given operation on the current playback. Available operations are: 13 | // - restart 14 | // - pause 15 | // - unpause 16 | // - reverse 17 | // - forward 18 | Control(key *Key, op string) error 19 | 20 | // Stop stops the playback 21 | Stop(key *Key) error 22 | 23 | // Subscribe subscribes on the playback events 24 | Subscribe(key *Key, n ...string) Subscription 25 | } 26 | 27 | // A Player is an entity which can play an audio URI 28 | type Player interface { 29 | // Play plays the audio using the given playback ID and media URI 30 | Play(string, ...string) (*PlaybackHandle, error) 31 | 32 | // StagePlay stages a `Play` operation 33 | StagePlay(string, ...string) (*PlaybackHandle, error) 34 | 35 | // Subscribe subscribes the player to events 36 | Subscribe(n ...string) Subscription 37 | } 38 | 39 | // PlaybackData represents the state of a playback 40 | type PlaybackData struct { 41 | // Key is the cluster-unique identifier for this playback 42 | Key *Key `json:"key"` 43 | 44 | ID string `json:"id"` // Unique ID for this playback session 45 | Language string `json:"language,omitempty"` 46 | MediaURI string `json:"media_uri"` // URI for the media which is to be played 47 | State string `json:"state"` // State of the playback operation 48 | TargetURI string `json:"target_uri"` // URI of the channel or bridge on which the media should be played (follows format of 'type':'name') 49 | } 50 | 51 | // PlaybackHandle is the handle for performing playback operations 52 | type PlaybackHandle struct { 53 | key *Key 54 | p Playback 55 | exec func(pb *PlaybackHandle) error 56 | executed bool 57 | } 58 | 59 | // NewPlaybackHandle builds a handle to the playback id 60 | func NewPlaybackHandle(key *Key, pb Playback, exec func(pb *PlaybackHandle) error) *PlaybackHandle { 61 | return &PlaybackHandle{ 62 | key: key, 63 | p: pb, 64 | exec: exec, 65 | } 66 | } 67 | 68 | // ID returns the identifier for the playback 69 | func (ph *PlaybackHandle) ID() string { 70 | return ph.key.ID 71 | } 72 | 73 | // Key returns the Key for the playback 74 | func (ph *PlaybackHandle) Key() *Key { 75 | return ph.key 76 | } 77 | 78 | // Data gets the playback data 79 | func (ph *PlaybackHandle) Data() (*PlaybackData, error) { 80 | return ph.p.Data(ph.key) 81 | } 82 | 83 | // Control performs the given operation 84 | func (ph *PlaybackHandle) Control(op string) error { 85 | return ph.p.Control(ph.key, op) 86 | } 87 | 88 | // Stop stops the playback 89 | func (ph *PlaybackHandle) Stop() error { 90 | return ph.p.Stop(ph.key) 91 | } 92 | 93 | // Subscribe subscribes the list of channel events 94 | func (ph *PlaybackHandle) Subscribe(n ...string) Subscription { 95 | if ph == nil { 96 | return nil 97 | } 98 | 99 | return ph.p.Subscribe(ph.key, n...) 100 | } 101 | 102 | // Exec executes any staged operations 103 | func (ph *PlaybackHandle) Exec() (err error) { 104 | if !ph.executed { 105 | ph.executed = true 106 | if ph.exec != nil { 107 | err = ph.exec(ph) 108 | ph.exec = nil 109 | } 110 | } 111 | 112 | return 113 | } 114 | -------------------------------------------------------------------------------- /client/arimocks/Subscription.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery; DO NOT EDIT. 2 | // github.com/vektra/mockery 3 | // template: testify 4 | 5 | package arimocks 6 | 7 | import ( 8 | "github.com/CyCoreSystems/ari/v6" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // NewSubscription creates a new instance of Subscription. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 13 | // The first argument is typically a *testing.T value. 14 | func NewSubscription(t interface { 15 | mock.TestingT 16 | Cleanup(func()) 17 | }) *Subscription { 18 | mock := &Subscription{} 19 | mock.Mock.Test(t) 20 | 21 | t.Cleanup(func() { mock.AssertExpectations(t) }) 22 | 23 | return mock 24 | } 25 | 26 | // Subscription is an autogenerated mock type for the Subscription type 27 | type Subscription struct { 28 | mock.Mock 29 | } 30 | 31 | type Subscription_Expecter struct { 32 | mock *mock.Mock 33 | } 34 | 35 | func (_m *Subscription) EXPECT() *Subscription_Expecter { 36 | return &Subscription_Expecter{mock: &_m.Mock} 37 | } 38 | 39 | // Cancel provides a mock function for the type Subscription 40 | func (_mock *Subscription) Cancel() { 41 | _mock.Called() 42 | return 43 | } 44 | 45 | // Subscription_Cancel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Cancel' 46 | type Subscription_Cancel_Call struct { 47 | *mock.Call 48 | } 49 | 50 | // Cancel is a helper method to define mock.On call 51 | func (_e *Subscription_Expecter) Cancel() *Subscription_Cancel_Call { 52 | return &Subscription_Cancel_Call{Call: _e.mock.On("Cancel")} 53 | } 54 | 55 | func (_c *Subscription_Cancel_Call) Run(run func()) *Subscription_Cancel_Call { 56 | _c.Call.Run(func(args mock.Arguments) { 57 | run() 58 | }) 59 | return _c 60 | } 61 | 62 | func (_c *Subscription_Cancel_Call) Return() *Subscription_Cancel_Call { 63 | _c.Call.Return() 64 | return _c 65 | } 66 | 67 | func (_c *Subscription_Cancel_Call) RunAndReturn(run func()) *Subscription_Cancel_Call { 68 | _c.Run(run) 69 | return _c 70 | } 71 | 72 | // Events provides a mock function for the type Subscription 73 | func (_mock *Subscription) Events() <-chan ari.Event { 74 | ret := _mock.Called() 75 | 76 | if len(ret) == 0 { 77 | panic("no return value specified for Events") 78 | } 79 | 80 | var r0 <-chan ari.Event 81 | if returnFunc, ok := ret.Get(0).(func() <-chan ari.Event); ok { 82 | r0 = returnFunc() 83 | } else { 84 | if ret.Get(0) != nil { 85 | r0 = ret.Get(0).(<-chan ari.Event) 86 | } 87 | } 88 | return r0 89 | } 90 | 91 | // Subscription_Events_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Events' 92 | type Subscription_Events_Call struct { 93 | *mock.Call 94 | } 95 | 96 | // Events is a helper method to define mock.On call 97 | func (_e *Subscription_Expecter) Events() *Subscription_Events_Call { 98 | return &Subscription_Events_Call{Call: _e.mock.On("Events")} 99 | } 100 | 101 | func (_c *Subscription_Events_Call) Run(run func()) *Subscription_Events_Call { 102 | _c.Call.Run(func(args mock.Arguments) { 103 | run() 104 | }) 105 | return _c 106 | } 107 | 108 | func (_c *Subscription_Events_Call) Return(eventCh <-chan ari.Event) *Subscription_Events_Call { 109 | _c.Call.Return(eventCh) 110 | return _c 111 | } 112 | 113 | func (_c *Subscription_Events_Call) RunAndReturn(run func() <-chan ari.Event) *Subscription_Events_Call { 114 | _c.Call.Return(run) 115 | return _c 116 | } 117 | -------------------------------------------------------------------------------- /_examples/stasisStart/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "sync" 8 | 9 | "golang.org/x/exp/slog" 10 | 11 | "github.com/CyCoreSystems/ari/v6" 12 | "github.com/CyCoreSystems/ari/v6/client/native" 13 | ) 14 | 15 | var log = slog.New(slog.NewTextHandler(os.Stderr, nil)) 16 | 17 | func main() { 18 | ctx, cancel := context.WithCancel(context.Background()) 19 | defer cancel() 20 | 21 | log.Info("Connecting to ARI") 22 | 23 | cl, err := native.Connect(&native.Options{ 24 | Application: "test", 25 | Logger: log.With("app", "test"), 26 | Username: "admin", 27 | Password: "admin", 28 | URL: "http://localhost:8088/ari", 29 | WebsocketURL: "ws://localhost:8088/ari/events", 30 | }) 31 | if err != nil { 32 | log.Error("Failed to build ARI client", "error", err) 33 | 34 | return 35 | } 36 | 37 | // setup app 38 | 39 | log.Info("Starting listener app") 40 | 41 | go listenApp(ctx, cl, channelHandler) 42 | 43 | // start call start listener 44 | 45 | log.Info("Starting HTTP Handler") 46 | 47 | http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 | // make call 49 | log.Info("Make sample call") 50 | 51 | h, err := createCall(cl) 52 | if err != nil { 53 | log.Error("Failed to create call", "error", err) 54 | 55 | w.WriteHeader(http.StatusBadGateway) 56 | 57 | w.Write([]byte("Failed to create call: " + err.Error())) //nolint:errcheck 58 | 59 | return 60 | } 61 | 62 | w.WriteHeader(http.StatusOK) 63 | 64 | w.Write([]byte(h.ID())) //nolint:errcheck 65 | })) 66 | 67 | log.Info("Listening for requests on port 9990") 68 | 69 | http.ListenAndServe(":9990", nil) //nolint:errcheck 70 | } 71 | 72 | func listenApp(ctx context.Context, cl ari.Client, handler func(cl ari.Client, h *ari.ChannelHandle)) { 73 | sub := cl.Bus().Subscribe(nil, "StasisStart") 74 | end := cl.Bus().Subscribe(nil, "StasisEnd") 75 | 76 | for { 77 | select { 78 | case e := <-sub.Events(): 79 | v := e.(*ari.StasisStart) 80 | 81 | log.Info("Got stasis start", "channel", v.Channel.ID) 82 | 83 | go handler(cl, cl.Channel().Get(v.Key(ari.ChannelKey, v.Channel.ID))) 84 | case <-end.Events(): 85 | log.Info("Got stasis end") 86 | case <-ctx.Done(): 87 | return 88 | } 89 | } 90 | } 91 | 92 | func createCall(cl ari.Client) (h *ari.ChannelHandle, err error) { 93 | h, err = cl.Channel().Create(nil, ari.ChannelCreateRequest{ 94 | Endpoint: "Local/1000", 95 | App: "example", 96 | }) 97 | 98 | return 99 | } 100 | 101 | func channelHandler(cl ari.Client, h *ari.ChannelHandle) { 102 | log.Info("Running channel handler") 103 | 104 | stateChange := h.Subscribe(ari.Events.ChannelStateChange) 105 | defer stateChange.Cancel() 106 | 107 | data, err := h.Data() 108 | if err != nil { 109 | log.Error("Error getting data", "error", err) 110 | return 111 | } 112 | 113 | log.Info("Channel State", "state", data.State) 114 | 115 | var wg sync.WaitGroup 116 | 117 | wg.Add(1) 118 | 119 | go func() { 120 | log.Info("Waiting for channel events") 121 | 122 | defer wg.Done() 123 | 124 | defer stateChange.Cancel() 125 | 126 | for ev := range stateChange.Events() { 127 | if ev == nil { 128 | return 129 | } 130 | 131 | log.Info("Got state change request") 132 | 133 | data, err = h.Data() 134 | if err != nil { 135 | log.Error("Error getting data", "error", err) 136 | continue 137 | } 138 | 139 | log.Info("New Channel State", "state", data.State) 140 | 141 | if data.State == "Up" { 142 | return 143 | } 144 | } 145 | }() 146 | 147 | h.Answer() //nolint:errcheck 148 | 149 | wg.Wait() 150 | 151 | h.Hangup() //nolint:errcheck 152 | } 153 | -------------------------------------------------------------------------------- /stdbus/bus_test.go: -------------------------------------------------------------------------------- 1 | package stdbus 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/CyCoreSystems/ari/v6" 8 | ) 9 | 10 | var dtmfTestEventData = ` 11 | { 12 | "channel": { 13 | "id": "9ae755c1-28a1-11e7-a1b1-0a580a480105", 14 | "dialplan": { 15 | "priority": 1, 16 | "context": "default", 17 | "exten": "9ae88e27-28a1-11e7-ba20-0a580a480707" 18 | }, 19 | "creationtime": "2017-04-24T03:53:41.188+0000", 20 | "name": "Local/9ae88e27-28a1-11e7-ba20-0a580a480707@default-0000008b;1", 21 | "state": "Up", 22 | "connected": { 23 | "name": "", 24 | "number": "" 25 | }, 26 | "caller": { 27 | "name": "", 28 | "number": "" 29 | }, 30 | "accountcode": "", 31 | "language": "en" 32 | }, 33 | "duration_ms": 240, 34 | "type": "ChannelDtmfReceived", 35 | "application": "sdp", 36 | "timestamp": "2017-04-24T03:53:42.155+0000", 37 | "digit": "1", 38 | "asterisk_id": "42:01:0a:64:00:06" 39 | } 40 | ` 41 | 42 | var dtmfTestEvent ari.Event 43 | 44 | func init() { 45 | var err error 46 | 47 | dtmfTestEvent, err = ari.DecodeEvent([]byte(dtmfTestEventData)) 48 | if err != nil { 49 | panic("failed to construct dtmf test event: " + err.Error()) 50 | } 51 | } 52 | 53 | func TestSubscribe(t *testing.T) { 54 | b := &bus{ 55 | subs: []*subscription{}, 56 | } 57 | 58 | defer b.Close() 59 | 60 | sub := b.Subscribe(nil, ari.Events.ChannelDtmfReceived) 61 | 62 | if len(b.subs) != 1 { 63 | t.Error("failed to add subscription to bus") 64 | } 65 | 66 | sub.Cancel() 67 | } 68 | 69 | func TestClose(t *testing.T) { 70 | defer func() { 71 | if r := recover(); r != nil { 72 | t.Error("Close caused a panic") 73 | } 74 | }() 75 | 76 | b := New() 77 | sub := b.Subscribe(nil, ari.Events.ChannelDtmfReceived) 78 | sub.Cancel() 79 | sub.Cancel() 80 | 81 | sub2 := b.Subscribe(nil, ari.Events.ChannelDestroyed).(*subscription) 82 | 83 | b.Close() 84 | b.Close() 85 | 86 | if !sub2.closed { 87 | t.Error("subscription was not marked as closed") 88 | return 89 | } 90 | 91 | select { 92 | case _, ok := <-sub2.C: 93 | if ok { 94 | t.Error("subscription channel is not closed") 95 | return 96 | } 97 | default: 98 | } 99 | } 100 | 101 | func TestEvents(t *testing.T) { 102 | b := New() 103 | defer b.Close() 104 | 105 | sub := b.Subscribe(nil, ari.Events.All) 106 | defer sub.Cancel() 107 | 108 | b.Send(dtmfTestEvent) 109 | 110 | select { 111 | case <-time.After(time.Millisecond): 112 | t.Error("failed to receive event") 113 | return 114 | case e, ok := <-sub.Events(): 115 | t.Log("event received") 116 | 117 | if !ok { 118 | t.Error("events channel was closed") 119 | return 120 | } 121 | 122 | if e == nil { 123 | t.Error("received empty event") 124 | return 125 | } 126 | 127 | dtmf, ok := e.(*ari.ChannelDtmfReceived) 128 | if !ok { 129 | t.Errorf("event is not a DTMF received event") 130 | return 131 | } 132 | 133 | if dtmf.Channel.ID != "9ae755c1-28a1-11e7-a1b1-0a580a480105" { 134 | t.Errorf("Failed to parse channel subentity on DTMF event") 135 | return 136 | } 137 | } 138 | } 139 | 140 | func TestEventsMultipleKeys(t *testing.T) { 141 | b := New() 142 | defer b.Close() 143 | 144 | sub := b.Subscribe(nil, ari.Events.All) 145 | defer sub.Cancel() 146 | 147 | multiKeyEvent := ari.BridgeCreated{ 148 | Bridge: ari.BridgeData{ 149 | ID: "A", 150 | ChannelIDs: []string{"x", "y"}, 151 | }, 152 | } 153 | 154 | keys := multiKeyEvent.Keys() 155 | if len(keys) != 5 { 156 | t.Errorf("Expected BridgeCreated.Keys() to be 2, got '%v'", len(keys)) 157 | } 158 | 159 | b.Send(&multiKeyEvent) 160 | 161 | eventCount := 0 162 | 163 | L: 164 | for { 165 | select { 166 | case <-time.After(time.Millisecond): 167 | break L 168 | case <-sub.Events(): 169 | eventCount++ 170 | } 171 | } 172 | 173 | if eventCount != 1 { 174 | t.Errorf("Expected 1 event to be sent, got %v", eventCount) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /datetime_test.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | // test data 13 | 14 | var dtMarshalTests = []struct { 15 | Input dtTest 16 | Output string 17 | HasError bool 18 | }{ 19 | {dtTest{DateTime(time.Date(2005, 0o2, 0o4, 13, 12, 6, 0, time.UTC))}, `{"dt":"2005-02-04T13:12:06.000+0000"}`, false}, 20 | } 21 | 22 | var dtUnmarshalTests = []struct { 23 | Input string 24 | Output dtTest 25 | HasError bool 26 | }{ 27 | {`{"dt":"2005-02-04T13:12:06.000+0000"}`, dtTest{DateTime(time.Date(2005, 0o2, 0o4, 13, 12, 6, 0, time.UTC))}, false}, 28 | {`{"dt":"2x05-02-04T13:12:06.000+0000"}`, dtTest{}, true}, 29 | {`{"dt": 0 }`, dtTest{}, true}, 30 | } 31 | 32 | var dsUnmarshalTests = []struct { 33 | Input string 34 | Output dsTest 35 | HasError bool 36 | }{ 37 | {`{"ds":4}`, dsTest{DurationSec(4 * time.Second)}, false}, 38 | {`{"ds":40}`, dsTest{DurationSec(40 * time.Second)}, false}, 39 | {`{"ds":"4"}`, dsTest{}, true}, 40 | {`{"ds":""}`, dsTest{}, true}, 41 | {`{"ds":"xzsad"}`, dsTest{}, true}, 42 | } 43 | 44 | var dsMarshalTests = []struct { 45 | Input dsTest 46 | Output string 47 | HasError bool 48 | }{ 49 | {dsTest{DurationSec(4 * time.Second)}, `{"ds":4}`, false}, 50 | {dsTest{DurationSec(40 * time.Second)}, `{"ds":40}`, false}, 51 | } 52 | 53 | // test runners 54 | func TestDateTimeMarshal(t *testing.T) { 55 | for _, tx := range dtMarshalTests { 56 | ret := runTestMarshal(tx.Input, tx.Output, tx.HasError) 57 | if ret != "" { 58 | t.Error(ret) 59 | } 60 | } 61 | } 62 | 63 | func TestDateTimeUnmarshal(t *testing.T) { 64 | for _, tx := range dtUnmarshalTests { 65 | var out dtTest 66 | 67 | ret := runTestUnmarshal(&out, tx.Input, &tx.Output, tx.HasError) 68 | if ret != "" { 69 | t.Error(ret) 70 | } 71 | } 72 | } 73 | 74 | func TestDurationSecsMarshal(t *testing.T) { 75 | for _, tx := range dsMarshalTests { 76 | ret := runTestMarshal(tx.Input, tx.Output, tx.HasError) 77 | if ret != "" { 78 | t.Error(ret) 79 | } 80 | } 81 | } 82 | 83 | func TestDurationSecsUnmarshal(t *testing.T) { 84 | for _, tx := range dsUnmarshalTests { 85 | var out dsTest 86 | if ret := runTestUnmarshal(&out, tx.Input, &tx.Output, tx.HasError); ret != "" { 87 | t.Error(ret) 88 | } 89 | } 90 | } 91 | 92 | // generalized test functions 93 | 94 | func runTestMarshal(input any, output string, hasError bool) (ret string) { 95 | var ( 96 | buf bytes.Buffer 97 | failed bool 98 | ) 99 | 100 | err := json.NewEncoder(&buf).Encode(input) 101 | 102 | out := strings.TrimSpace(buf.String()) 103 | 104 | failed = failed || (err == nil && hasError) 105 | failed = failed || (out != output) 106 | 107 | if failed { 108 | ret = fmt.Sprintf("Marshal(%s) => '%s', 'err != nil => %v'; expected '%s', 'err != nil => %v'.", input, out, err != nil, output, hasError) 109 | } 110 | 111 | return 112 | } 113 | 114 | func runTestUnmarshal(out eq, input string, output eq, hasError bool) (ret string) { 115 | err := json.NewDecoder(strings.NewReader(input)).Decode(&out) 116 | 117 | failed := false 118 | failed = failed || (err == nil && hasError) 119 | failed = failed || (!out.Equal(output)) 120 | 121 | if failed { 122 | ret = fmt.Sprintf("Unmarshal(%s) => '%s', 'err != nil => %v'; expected '%s', 'err != nil => %v'.", input, out, err != nil, output, hasError) 123 | } 124 | 125 | return 126 | } 127 | 128 | // test structures 129 | 130 | type dtTest struct { 131 | Date DateTime `json:"dt"` 132 | } 133 | 134 | func (dt *dtTest) Equal(i interface{}) bool { 135 | o, ok := i.(*dtTest) 136 | return ok && time.Time(dt.Date).Equal(time.Time(o.Date)) 137 | } 138 | 139 | type eq interface { 140 | Equal(i interface{}) bool 141 | } 142 | 143 | type dsTest struct { 144 | Duration DurationSec `json:"ds"` 145 | } 146 | 147 | func (ds *dsTest) Equal(i interface{}) bool { 148 | o, ok := i.(*dsTest) 149 | return ok && time.Duration(ds.Duration) == time.Duration(o.Duration) 150 | } 151 | -------------------------------------------------------------------------------- /stdbus/bus.go: -------------------------------------------------------------------------------- 1 | package stdbus 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // subscriptionEventBufferSize defines the number of events that each 10 | // subscription will queue before accepting more events. 11 | var subscriptionEventBufferSize = 100 12 | 13 | // bus is an event bus for ARI events. It receives and 14 | // redistributes events based on a subscription 15 | // model. 16 | type bus struct { 17 | subs []*subscription // The list of subscriptions 18 | 19 | rwMux sync.RWMutex 20 | 21 | closed bool 22 | } 23 | 24 | // New creates and returns the event bus. 25 | func New() ari.Bus { 26 | b := &bus{ 27 | subs: []*subscription{}, 28 | } 29 | 30 | return b 31 | } 32 | 33 | // Close closes out all subscriptions in the bus. 34 | func (b *bus) Close() { 35 | if b.closed { 36 | return 37 | } 38 | 39 | b.closed = true 40 | 41 | for _, s := range b.subs { 42 | s.Cancel() 43 | } 44 | } 45 | 46 | // Send sends the message to the bus 47 | func (b *bus) Send(e ari.Event) { 48 | var matched bool 49 | 50 | b.rwMux.RLock() 51 | 52 | // Disseminate the message to the subscribers 53 | for _, s := range b.subs { 54 | matched = false 55 | for _, k := range e.Keys() { 56 | if matched { 57 | break 58 | } 59 | 60 | if s.key.Match(k) { 61 | matched = true 62 | 63 | for _, topic := range s.events { 64 | if topic == e.GetType() || topic == ari.Events.All { 65 | select { 66 | case s.C <- e: 67 | default: // never block 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | b.rwMux.RUnlock() 76 | } 77 | 78 | // Subscribe returns a subscription to the given list 79 | // of event types 80 | func (b *bus) Subscribe(key *ari.Key, eTypes ...string) ari.Subscription { 81 | s := newSubscription(b, key, eTypes...) 82 | b.add(s) 83 | 84 | return s 85 | } 86 | 87 | // add appends a new subscription to the bus 88 | func (b *bus) add(s *subscription) { 89 | b.rwMux.Lock() 90 | b.subs = append(b.subs, s) 91 | b.rwMux.Unlock() 92 | } 93 | 94 | // remove deletes the given subscription from the bus 95 | func (b *bus) remove(s *subscription) { 96 | b.rwMux.Lock() 97 | 98 | for i, si := range b.subs { 99 | if s == si { 100 | // Subs are pointers, so we have to explicitly remove them 101 | // to prevent memory leaks 102 | b.subs[i] = b.subs[len(b.subs)-1] // replace the current with the end 103 | b.subs[len(b.subs)-1] = nil // remove the end 104 | b.subs = b.subs[:len(b.subs)-1] // lop off the end 105 | 106 | break 107 | } 108 | } 109 | 110 | b.rwMux.Unlock() 111 | } 112 | 113 | // A Subscription is a wrapped channel for receiving 114 | // events from the ARI event bus. 115 | type subscription struct { 116 | key *ari.Key 117 | b *bus // reference to the event bus 118 | events []string // list of events to listen for 119 | 120 | mu sync.Mutex 121 | closed bool // channel closure protection flag 122 | C chan ari.Event // channel for sending events to the subscriber 123 | } 124 | 125 | // newSubscription creates a new, unattached subscription 126 | func newSubscription(b *bus, key *ari.Key, eTypes ...string) *subscription { 127 | return &subscription{ 128 | key: key, 129 | b: b, 130 | events: eTypes, 131 | C: make(chan ari.Event, subscriptionEventBufferSize), 132 | } 133 | } 134 | 135 | // Events returns the events channel 136 | func (s *subscription) Events() <-chan ari.Event { 137 | return s.C 138 | } 139 | 140 | // Cancel cancels the subscription and removes it from 141 | // the event bus. 142 | func (s *subscription) Cancel() { 143 | if s == nil { 144 | return 145 | } 146 | 147 | s.mu.Lock() 148 | 149 | if s.closed { 150 | s.mu.Unlock() 151 | return 152 | } 153 | 154 | s.closed = true 155 | 156 | s.mu.Unlock() 157 | 158 | // Remove the subscription from the bus 159 | if s.b != nil { 160 | s.b.remove(s) 161 | } 162 | 163 | // Close the subscription's deliver channel 164 | if s.C != nil { 165 | close(s.C) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /originate.go: -------------------------------------------------------------------------------- 1 | package ari 2 | 3 | // OriginateRequest defines the parameters for the creation of a new Asterisk channel 4 | type OriginateRequest struct { 5 | // Endpoint is the name of the Asterisk resource to be used to create the 6 | // channel. The format is tech/resource. 7 | // 8 | // Examples: 9 | // 10 | // - PJSIP/george 11 | // 12 | // - Local/party@mycontext 13 | // 14 | // - DAHDI/8005558282 15 | Endpoint string `json:"endpoint"` 16 | 17 | // Timeout specifies the number of seconds to wait for the channel to be 18 | // answered before giving up. Note that this is REQUIRED and the default is 19 | // to timeout immediately. Use a negative value to specify no timeout, but 20 | // be aware that this could result in an unlimited call, which could result 21 | // in a very unfriendly bill. 22 | Timeout int `json:"timeout,omitempty"` 23 | 24 | // CallerID specifies the Caller ID (name and number) to be set on the 25 | // newly-created channel. This is optional but recommended. The format is 26 | // `"Name" `, but most every component is optional. 27 | // 28 | // Examples: 29 | // 30 | // - "Jane" <100> 31 | // 32 | // - <102> 33 | // 34 | // - 8005558282 35 | // 36 | CallerID string `json:"callerId,omitempty"` 37 | 38 | // CEP (Context/Extension/Priority) is the location in the Asterisk dialplan 39 | // into which the newly created channel should be dropped. All of these are 40 | // required if the CEP is used. Exactly one of CEP or App/AppArgs must be 41 | // specified. 42 | Context string `json:"context,omitempty"` 43 | Extension string `json:"extension,omitempty"` 44 | Priority int64 `json:"priority,omitempty"` 45 | 46 | // The Label is the string form of Priority, if there is such a label in the 47 | // dialplan. Like CEP, Label may not be used if an ARI App is specified. 48 | // If both Label and Priority are specified, Label will take priority. 49 | Label string `json:"label,omitempty"` 50 | 51 | // App specifies the ARI application and its arguments into which 52 | // the newly-created channel should be placed. Exactly one of CEP or 53 | // App/AppArgs is required. 54 | App string `json:"app,omitempty"` 55 | 56 | // AppArgs defines the arguments to supply to the ARI application, if one is 57 | // defined. It is optional but only applicable for Originations which 58 | // specify an ARI App. 59 | AppArgs string `json:"appArgs,omitempty"` 60 | 61 | // Formats describes the (comma-delimited) set of codecs which should be 62 | // allowed for the created channel. This is an optional parameter, and if 63 | // an Originator is specified, this should be left blank so that Asterisk 64 | // derives the codecs from that Originator channel instead. 65 | // 66 | // Ex. "ulaw,slin16". 67 | // 68 | // The list of valid codecs can be found with Asterisk command "core show codecs". 69 | Formats string `json:"formats,omitempty"` 70 | 71 | // ChannelID specifies the unique ID to be used for the channel to be 72 | // created. It is optional, and if not specified, a time-based UUID will be 73 | // generated. 74 | ChannelID string `json:"channelId,omitempty"` // Optionally assign channel id 75 | 76 | // OtherChannelID specifies the unique ID of the second channel to be 77 | // created. This is only valid for the creation of Local channels, which 78 | // are always generated in pairs. It is optional, and if not specified, a 79 | // time-based UUID will be generated (again, only if the Origination is of a 80 | // Local channel). 81 | OtherChannelID string `json:"otherChannelId,omitempty"` 82 | 83 | // Originator is the channel for whom this Originate request is being made, if there is one. 84 | // It is used by Asterisk to set the right codecs (and possibly other parameters) such that 85 | // when the new channel is bridged to the Originator channel, there should be no transcoding. 86 | // This is a purely optional (but helpful, where applicable) field. 87 | Originator string `json:"originator,omitempty"` 88 | 89 | // Variables describes the set of channel variables to apply to the new channel. It is optional. 90 | Variables map[string]string `json:"variables,omitempty"` 91 | } 92 | -------------------------------------------------------------------------------- /client/native/request.go: -------------------------------------------------------------------------------- 1 | package native 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/rotisserie/eris" 11 | ) 12 | 13 | // MaxIdleConnections is the maximum number of idle web client 14 | // connections to maintain. 15 | var MaxIdleConnections = 20 16 | 17 | // RequestTimeout describes the maximum amount of time to wait 18 | // for a response to any request. 19 | var RequestTimeout = 2 * time.Second 20 | 21 | // RequestError describes an error with an error Code. 22 | type RequestError interface { 23 | error 24 | Code() int 25 | } 26 | 27 | type requestError struct { 28 | statusCode int 29 | text string 30 | } 31 | 32 | // Error returns the request error as a string. 33 | func (e *requestError) Error() string { 34 | return e.text 35 | } 36 | 37 | // Code returns the status code from the request. 38 | func (e *requestError) Code() int { 39 | return e.statusCode 40 | } 41 | 42 | // CodeFromError extracts and returns the code from an error, or 43 | // 0 if not found. 44 | func CodeFromError(err error) int { 45 | if reqerr, ok := err.(RequestError); ok { 46 | return reqerr.Code() 47 | } 48 | 49 | return 0 50 | } 51 | 52 | func maybeRequestError(resp *http.Response) RequestError { 53 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 54 | // 2xx response: All good. 55 | return nil 56 | } 57 | 58 | return &requestError{ 59 | text: "Non-2XX response: " + resp.Status, 60 | statusCode: resp.StatusCode, 61 | } 62 | } 63 | 64 | // MissingParams is an error message response emitted when a request 65 | // does not contain required parameters 66 | type MissingParams struct { 67 | // Message 68 | Type string `json:"type"` 69 | Params []string `json:"params"` // List of missing parameters which are required 70 | } 71 | 72 | // get calls the ARI server with a GET request 73 | func (c *Client) get(url string, resp interface{}) error { 74 | url = c.Options.URL + url 75 | 76 | return c.makeRequest("GET", url, resp, nil) 77 | } 78 | 79 | // post calls the ARI server with a POST request. 80 | func (c *Client) post(requestURL string, resp interface{}, req interface{}) error { 81 | url := c.Options.URL + requestURL 82 | return c.makeRequest("POST", url, resp, req) 83 | } 84 | 85 | // put calls the ARI server with a PUT request. 86 | func (c *Client) put(url string, resp interface{}, req interface{}) error { 87 | url = c.Options.URL + url 88 | 89 | return c.makeRequest("PUT", url, resp, req) 90 | } 91 | 92 | // del calls the ARI server with a DELETE request 93 | func (c *Client) del(url string, resp interface{}, req string) error { 94 | url = c.Options.URL + url 95 | if req != "" { 96 | url = url + "?" + req 97 | } 98 | 99 | return c.makeRequest("DELETE", url, resp, nil) 100 | } 101 | 102 | func (c *Client) makeRequest(method, url string, resp interface{}, req interface{}) (err error) { 103 | var reqBody io.Reader 104 | if req != nil { 105 | reqBody, err = structToRequestBody(req) 106 | if err != nil { 107 | return eris.Wrap(err, "failed to marshal request") 108 | } 109 | } 110 | 111 | var r *http.Request 112 | 113 | if r, err = http.NewRequest(method, url, reqBody); err != nil { 114 | return eris.Wrap(err, "failed to create request") 115 | } 116 | 117 | r.Header.Set("Content-Type", "application/json") 118 | 119 | if c.Options.Username != "" { 120 | r.SetBasicAuth(c.Options.Username, c.Options.Password) 121 | } 122 | 123 | ret, err := c.httpClient.Do(r) 124 | if err != nil { 125 | return eris.Wrap(err, "failed to make request") 126 | } 127 | 128 | defer ret.Body.Close() //nolint:errcheck 129 | 130 | if resp != nil { 131 | err = json.NewDecoder(ret.Body).Decode(resp) 132 | if err != nil { 133 | return eris.Wrap(err, "failed to decode response") 134 | } 135 | } 136 | 137 | return maybeRequestError(ret) 138 | } 139 | 140 | func structToRequestBody(req interface{}) (io.Reader, error) { 141 | buf := new(bytes.Buffer) 142 | 143 | if req != nil { 144 | if err := json.NewEncoder(buf).Encode(req); err != nil { 145 | return nil, err 146 | } 147 | } 148 | 149 | return buf, nil 150 | } 151 | -------------------------------------------------------------------------------- /ext/bridgemon/bridgemon.go: -------------------------------------------------------------------------------- 1 | package bridgemon 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/CyCoreSystems/ari/v6" 7 | ) 8 | 9 | // Monitor is a bridge monitor, which maintains bridge data. It monitors an ARI bridge for events and keeps an internal cache of the bridge's data. 10 | type Monitor struct { 11 | h *ari.BridgeHandle 12 | 13 | br *ari.BridgeData 14 | 15 | sub ari.Subscription 16 | closed bool 17 | 18 | watchers []chan *ari.BridgeData 19 | watcherMu sync.Mutex 20 | 21 | mu sync.Mutex 22 | } 23 | 24 | // New returns a new bridge monitor 25 | func New(h *ari.BridgeHandle) *Monitor { 26 | sub := h.Subscribe(ari.Events.BridgeDestroyed, ari.Events.ChannelEnteredBridge, ari.Events.ChannelLeftBridge) 27 | 28 | m := &Monitor{ 29 | h: h, 30 | sub: sub, 31 | } 32 | 33 | // Monitor bridge events to keep data in sync 34 | go m.monitor() 35 | 36 | // Attempt to load initial bridge data; this may fail if the bridge has only 37 | // been staged, so ignore errors here 38 | data, _ := h.Data() // nolint 39 | m.updateData(data) 40 | 41 | return m 42 | } 43 | 44 | func (m *Monitor) monitor() { 45 | defer m.Close() 46 | 47 | for v := range m.sub.Events() { 48 | if v == nil { 49 | continue 50 | } 51 | 52 | switch v.GetType() { 53 | case ari.Events.BridgeDestroyed: 54 | e, ok := v.(*ari.BridgeDestroyed) 55 | if !ok { 56 | continue 57 | } 58 | 59 | m.updateData(&e.Bridge) 60 | 61 | return // bridge is destroyed; there will be no more events 62 | case ari.Events.ChannelEnteredBridge: 63 | e, ok := v.(*ari.ChannelEnteredBridge) 64 | if !ok { 65 | continue 66 | } 67 | 68 | m.updateData(&e.Bridge) 69 | case ari.Events.ChannelLeftBridge: 70 | e, ok := v.(*ari.ChannelLeftBridge) 71 | if !ok { 72 | continue 73 | } 74 | 75 | m.updateData(&e.Bridge) 76 | } 77 | } 78 | } 79 | 80 | func (m *Monitor) updateData(data *ari.BridgeData) { 81 | if data == nil { 82 | return 83 | } 84 | 85 | // Populate the bridge key in the bridge data, since Asterisk does not populate this field. 86 | if data.Key == nil { 87 | data.Key = m.h.Key() 88 | } 89 | 90 | // Update the stored data 91 | m.mu.Lock() 92 | m.br = data 93 | m.mu.Unlock() 94 | 95 | // Distribute new data to any watchers 96 | m.watcherMu.Lock() 97 | 98 | for _, w := range m.watchers { 99 | select { 100 | case w <- data: 101 | default: 102 | } 103 | } 104 | 105 | m.watcherMu.Unlock() 106 | } 107 | 108 | // Data returns the current bridge data 109 | func (m *Monitor) Data() *ari.BridgeData { 110 | if m == nil { 111 | return nil 112 | } 113 | 114 | return m.br 115 | } 116 | 117 | // Handle returns the BridgeHandle which was used to create the bridge Monitor. 118 | func (m *Monitor) Handle() *ari.BridgeHandle { 119 | if m == nil { 120 | return nil 121 | } 122 | 123 | return m.h 124 | } 125 | 126 | // Key returns the key of the monitored bridge 127 | func (m *Monitor) Key() *ari.Key { 128 | if m == nil || m.h == nil { 129 | return nil 130 | } 131 | 132 | return m.h.Key() 133 | } 134 | 135 | // Watch returns a channel on which bridge data will be returned when events 136 | // occur. This channel will be closed when the bridge or the monitor is 137 | // destoyed. 138 | // 139 | // NOTE: the user should NEVER close this channel directly. 140 | func (m *Monitor) Watch() <-chan *ari.BridgeData { 141 | ch := make(chan *ari.BridgeData) 142 | 143 | m.mu.Lock() 144 | defer m.mu.Unlock() 145 | 146 | if m.closed { 147 | close(ch) 148 | return ch 149 | } 150 | 151 | m.watcherMu.Lock() 152 | m.watchers = append(m.watchers, ch) 153 | m.watcherMu.Unlock() 154 | 155 | return ch 156 | } 157 | 158 | // Close shuts down a bridge monitor 159 | func (m *Monitor) Close() { 160 | if m == nil { 161 | return 162 | } 163 | 164 | { 165 | m.mu.Lock() 166 | 167 | if !m.closed { 168 | m.closed = true 169 | if m.sub != nil { 170 | m.sub.Cancel() 171 | } 172 | } 173 | 174 | m.mu.Unlock() 175 | } 176 | 177 | { 178 | m.watcherMu.Lock() 179 | 180 | for _, w := range m.watchers { 181 | close(w) 182 | } 183 | 184 | m.watchers = nil 185 | 186 | m.watcherMu.Unlock() 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /_examples/bridge/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "sync" 7 | 8 | "github.com/rotisserie/eris" 9 | "golang.org/x/exp/slog" 10 | 11 | "github.com/CyCoreSystems/ari/v6" 12 | "github.com/CyCoreSystems/ari/v6/client/native" 13 | "github.com/CyCoreSystems/ari/v6/ext/play" 14 | "github.com/CyCoreSystems/ari/v6/rid" 15 | ) 16 | 17 | var ariApp = "test" 18 | 19 | var log = slog.New(slog.NewTextHandler(os.Stderr, nil)) 20 | 21 | var bridge *ari.BridgeHandle 22 | 23 | func main() { 24 | ctx, cancel := context.WithCancel(context.Background()) 25 | defer cancel() 26 | 27 | log.Info("Connecting to ARI") 28 | 29 | cl, err := native.Connect(&native.Options{ 30 | Application: ariApp, 31 | Logger: log.With("ari", "test"), 32 | Username: "admin", 33 | Password: "admin", 34 | URL: "http://localhost:8088/ari", 35 | WebsocketURL: "ws://localhost:8088/ari/events", 36 | }) 37 | if err != nil { 38 | log.Error("Failed to build ARI client", "error", err) 39 | 40 | return 41 | } 42 | 43 | log.Info("Starting listener app") 44 | 45 | log.Info("Listening for new calls") 46 | 47 | sub := cl.Bus().Subscribe(nil, "StasisStart") 48 | 49 | for { 50 | select { 51 | case e := <-sub.Events(): 52 | v := e.(*ari.StasisStart) 53 | 54 | log.Info("Got stasis start", "channel", v.Channel.ID) 55 | 56 | go app(ctx, cl, cl.Channel().Get(v.Key(ari.ChannelKey, v.Channel.ID))) 57 | case <-ctx.Done(): 58 | return 59 | } 60 | } 61 | } 62 | 63 | func app(ctx context.Context, cl ari.Client, h *ari.ChannelHandle) { 64 | log.Info("running app", "channel", h.Key().ID) 65 | 66 | if err := h.Answer(); err != nil { 67 | log.Warn("failed to answer call", "error", err) 68 | } 69 | 70 | if err := ensureBridge(ctx, cl, h.Key()); err != nil { 71 | log.Error("failed to manage bridge", "error", err) 72 | return 73 | } 74 | 75 | if err := bridge.AddChannel(h.Key().ID); err != nil { 76 | log.Error("failed to add channel to bridge", "error", err) 77 | return 78 | } 79 | 80 | log.Info("channel added to bridge") 81 | } 82 | 83 | func ensureBridge(ctx context.Context, cl ari.Client, src *ari.Key) (err error) { 84 | if bridge != nil { 85 | log.Debug("Bridge already exists") 86 | return nil 87 | } 88 | 89 | key := src.New(ari.BridgeKey, rid.New(rid.Bridge)) 90 | 91 | bridge, err = cl.Bridge().Create(key, "mixing", key.ID) 92 | if err != nil { 93 | bridge = nil 94 | return eris.Wrap(err, "failed to create bridge") 95 | } 96 | 97 | wg := new(sync.WaitGroup) 98 | 99 | wg.Add(1) 100 | 101 | go manageBridge(ctx, bridge, wg) 102 | 103 | wg.Wait() 104 | 105 | return nil 106 | } 107 | 108 | func manageBridge(ctx context.Context, h *ari.BridgeHandle, wg *sync.WaitGroup) { 109 | // Delete the bridge when we exit 110 | defer h.Delete() //nolint:errcheck 111 | 112 | destroySub := h.Subscribe(ari.Events.BridgeDestroyed) 113 | defer destroySub.Cancel() 114 | 115 | enterSub := h.Subscribe(ari.Events.ChannelEnteredBridge) 116 | defer enterSub.Cancel() 117 | 118 | leaveSub := h.Subscribe(ari.Events.ChannelLeftBridge) 119 | defer leaveSub.Cancel() 120 | 121 | wg.Done() 122 | 123 | for { 124 | select { 125 | case <-ctx.Done(): 126 | return 127 | case <-destroySub.Events(): 128 | log.Debug("bridge destroyed") 129 | return 130 | case e, ok := <-enterSub.Events(): 131 | if !ok { 132 | log.Error("channel entered subscription closed") 133 | return 134 | } 135 | 136 | v := e.(*ari.ChannelEnteredBridge) 137 | 138 | log.Debug("channel entered bridge", "channel", v.Channel.Name) 139 | 140 | go func() { 141 | if err := play.Play(ctx, h, play.URI("sound:confbridge-join")).Err(); err != nil { 142 | log.Error("failed to play join sound", "error", err) 143 | } 144 | }() 145 | case e, ok := <-leaveSub.Events(): 146 | if !ok { 147 | log.Error("channel left subscription closed") 148 | return 149 | } 150 | 151 | v := e.(*ari.ChannelLeftBridge) 152 | 153 | log.Debug("channel left bridge", "channel", v.Channel.Name) 154 | 155 | go func() { 156 | if err := play.Play(ctx, h, play.URI("sound:confbridge-leave")).Err(); err != nil { 157 | log.Error("failed to play leave sound", "error", err) 158 | } 159 | }() 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /client/arimocks/AsteriskVariables.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery; DO NOT EDIT. 2 | // github.com/vektra/mockery 3 | // template: testify 4 | 5 | package arimocks 6 | 7 | import ( 8 | "github.com/CyCoreSystems/ari/v6" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // NewAsteriskVariables creates a new instance of AsteriskVariables. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 13 | // The first argument is typically a *testing.T value. 14 | func NewAsteriskVariables(t interface { 15 | mock.TestingT 16 | Cleanup(func()) 17 | }) *AsteriskVariables { 18 | mock := &AsteriskVariables{} 19 | mock.Mock.Test(t) 20 | 21 | t.Cleanup(func() { mock.AssertExpectations(t) }) 22 | 23 | return mock 24 | } 25 | 26 | // AsteriskVariables is an autogenerated mock type for the AsteriskVariables type 27 | type AsteriskVariables struct { 28 | mock.Mock 29 | } 30 | 31 | type AsteriskVariables_Expecter struct { 32 | mock *mock.Mock 33 | } 34 | 35 | func (_m *AsteriskVariables) EXPECT() *AsteriskVariables_Expecter { 36 | return &AsteriskVariables_Expecter{mock: &_m.Mock} 37 | } 38 | 39 | // Get provides a mock function for the type AsteriskVariables 40 | func (_mock *AsteriskVariables) Get(key *ari.Key) (string, error) { 41 | ret := _mock.Called(key) 42 | 43 | if len(ret) == 0 { 44 | panic("no return value specified for Get") 45 | } 46 | 47 | var r0 string 48 | var r1 error 49 | if returnFunc, ok := ret.Get(0).(func(*ari.Key) (string, error)); ok { 50 | return returnFunc(key) 51 | } 52 | if returnFunc, ok := ret.Get(0).(func(*ari.Key) string); ok { 53 | r0 = returnFunc(key) 54 | } else { 55 | r0 = ret.Get(0).(string) 56 | } 57 | if returnFunc, ok := ret.Get(1).(func(*ari.Key) error); ok { 58 | r1 = returnFunc(key) 59 | } else { 60 | r1 = ret.Error(1) 61 | } 62 | return r0, r1 63 | } 64 | 65 | // AsteriskVariables_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' 66 | type AsteriskVariables_Get_Call struct { 67 | *mock.Call 68 | } 69 | 70 | // Get is a helper method to define mock.On call 71 | // - key *ari.Key 72 | func (_e *AsteriskVariables_Expecter) Get(key interface{}) *AsteriskVariables_Get_Call { 73 | return &AsteriskVariables_Get_Call{Call: _e.mock.On("Get", key)} 74 | } 75 | 76 | func (_c *AsteriskVariables_Get_Call) Run(run func(key *ari.Key)) *AsteriskVariables_Get_Call { 77 | _c.Call.Run(func(args mock.Arguments) { 78 | var arg0 *ari.Key 79 | if args[0] != nil { 80 | arg0 = args[0].(*ari.Key) 81 | } 82 | run( 83 | arg0, 84 | ) 85 | }) 86 | return _c 87 | } 88 | 89 | func (_c *AsteriskVariables_Get_Call) Return(s string, err error) *AsteriskVariables_Get_Call { 90 | _c.Call.Return(s, err) 91 | return _c 92 | } 93 | 94 | func (_c *AsteriskVariables_Get_Call) RunAndReturn(run func(key *ari.Key) (string, error)) *AsteriskVariables_Get_Call { 95 | _c.Call.Return(run) 96 | return _c 97 | } 98 | 99 | // Set provides a mock function for the type AsteriskVariables 100 | func (_mock *AsteriskVariables) Set(key *ari.Key, value string) error { 101 | ret := _mock.Called(key, value) 102 | 103 | if len(ret) == 0 { 104 | panic("no return value specified for Set") 105 | } 106 | 107 | var r0 error 108 | if returnFunc, ok := ret.Get(0).(func(*ari.Key, string) error); ok { 109 | r0 = returnFunc(key, value) 110 | } else { 111 | r0 = ret.Error(0) 112 | } 113 | return r0 114 | } 115 | 116 | // AsteriskVariables_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' 117 | type AsteriskVariables_Set_Call struct { 118 | *mock.Call 119 | } 120 | 121 | // Set is a helper method to define mock.On call 122 | // - key *ari.Key 123 | // - value string 124 | func (_e *AsteriskVariables_Expecter) Set(key interface{}, value interface{}) *AsteriskVariables_Set_Call { 125 | return &AsteriskVariables_Set_Call{Call: _e.mock.On("Set", key, value)} 126 | } 127 | 128 | func (_c *AsteriskVariables_Set_Call) Run(run func(key *ari.Key, value string)) *AsteriskVariables_Set_Call { 129 | _c.Call.Run(func(args mock.Arguments) { 130 | var arg0 *ari.Key 131 | if args[0] != nil { 132 | arg0 = args[0].(*ari.Key) 133 | } 134 | var arg1 string 135 | if args[1] != nil { 136 | arg1 = args[1].(string) 137 | } 138 | run( 139 | arg0, 140 | arg1, 141 | ) 142 | }) 143 | return _c 144 | } 145 | 146 | func (_c *AsteriskVariables_Set_Call) Return(err error) *AsteriskVariables_Set_Call { 147 | _c.Call.Return(err) 148 | return _c 149 | } 150 | 151 | func (_c *AsteriskVariables_Set_Call) RunAndReturn(run func(key *ari.Key, value string) error) *AsteriskVariables_Set_Call { 152 | _c.Call.Return(run) 153 | return _c 154 | } 155 | -------------------------------------------------------------------------------- /internal/eventgen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Generate event code from the events.json swagger 4 | 5 | import ( 6 | "encoding/json" 7 | "log" 8 | "os" 9 | "sort" 10 | "strings" 11 | "text/template" 12 | 13 | "golang.org/x/text/cases" 14 | "golang.org/x/text/language" 15 | ) 16 | 17 | var typeMappings map[string]string 18 | 19 | func init() { 20 | typeMappings = make(map[string]string) 21 | typeMappings["boolean"] = "bool" 22 | typeMappings["Channel"] = "ChannelData" 23 | typeMappings["List[string]"] = "[]string" 24 | typeMappings["Bridge"] = "BridgeData" 25 | typeMappings["Playback"] = "PlaybackData" 26 | typeMappings["LiveRecording"] = "LiveRecordingData" 27 | typeMappings["StoredRecording"] = "StoredRecordingData" 28 | typeMappings["Endpoint"] = "EndpointData" 29 | typeMappings["DeviceState"] = "DeviceStateData" 30 | typeMappings["TextMessage"] = "TextMessageData" 31 | typeMappings["object"] = "interface{}" 32 | } 33 | 34 | type event struct { 35 | Name string 36 | Event string 37 | Description string 38 | Properties propList 39 | } 40 | 41 | type prop struct { 42 | Name string 43 | JSONName string 44 | Mapping string 45 | Type string 46 | Description string 47 | Required bool 48 | } 49 | 50 | type propList []prop 51 | 52 | func (pl propList) Len() int { 53 | return len(pl) 54 | } 55 | 56 | func (pl propList) Less(l int, r int) bool { 57 | return pl[l].Name < pl[r].Name 58 | } 59 | 60 | func (pl propList) Swap(l int, r int) { 61 | tmp := pl[r] 62 | pl[r] = pl[l] 63 | pl[l] = tmp 64 | } 65 | 66 | type eventList []event 67 | 68 | func (el eventList) Len() int { 69 | return len(el) 70 | } 71 | 72 | func (el eventList) Less(l int, r int) bool { 73 | return el[l].Name < el[r].Name 74 | } 75 | 76 | func (el eventList) Swap(l int, r int) { 77 | tmp := el[r] 78 | el[r] = el[l] 79 | el[l] = tmp 80 | } 81 | 82 | func main() { 83 | if len(os.Args) < 3 { 84 | log.Fatalf("Usage: %s