├── .github └── workflows │ ├── go.yml │ ├── master.yaml │ └── release.yaml ├── .gitignore ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Dockerfile.goreleaser ├── LICENSE ├── Makefile ├── README.md ├── _examples ├── bridge │ └── main.go ├── play │ └── main.go ├── record │ └── main.go └── stasisStart │ ├── .gitignore │ └── main.go ├── ari-proxy.yaml ├── ci_release.sh ├── client ├── application.go ├── application_test.go ├── asterisk.go ├── asterisk_test.go ├── bridge.go ├── bridge_test.go ├── bus │ ├── bus.go │ └── bus_test.go ├── channel.go ├── channel_test.go ├── client.go ├── clientserver_test.go ├── cluster │ ├── cluster.go │ └── cluster_test.go ├── config.go ├── config_test.go ├── device.go ├── device_test.go ├── doc.go ├── endpoint.go ├── endpoint_test.go ├── errors.go ├── listen.go ├── liveRecording.go ├── liveRecording_test.go ├── logger.go ├── logging.go ├── logging_test.go ├── mailbox.go ├── mailbox_test.go ├── modules.go ├── modules_test.go ├── playback.go ├── playback_test.go ├── request.go ├── sound.go ├── sound_test.go ├── storedRecording.go └── subscription.go ├── cmd.go ├── go.mod ├── go.sum ├── internal └── integration │ ├── application.go │ ├── asterisk.go │ ├── bridge.go │ ├── channel.go │ ├── clientserver.go │ ├── config.go │ ├── device.go │ ├── doc.go │ ├── endpoint.go │ ├── liverecording.go │ ├── logging.go │ ├── mailbox.go │ ├── mock.go │ ├── modules.go │ ├── playback.go │ └── sound.go ├── main.go ├── messagebus ├── messagebus.go ├── nats.go ├── rabbitmq.go ├── rabbitmqsub.go └── response_forwarder.go ├── proxy ├── subjects.go └── types.go ├── server ├── application.go ├── application_test.go ├── asterisk.go ├── asterisk_test.go ├── bridge.go ├── bridge_test.go ├── channel.go ├── channel_test.go ├── clientserver_test.go ├── closegroup.go ├── config.go ├── config_test.go ├── device.go ├── device_test.go ├── dialog │ ├── manager.go │ └── manager_test.go ├── doc.go ├── endpoint.go ├── endpoint_test.go ├── events.go ├── handler.go ├── liveRecording.go ├── liveRecording_test.go ├── logging.go ├── logging_test.go ├── mailbox.go ├── mailbox_test.go ├── modules.go ├── modules_test.go ├── options.go ├── playback.go ├── playback_test.go ├── server.go ├── sound.go ├── sound_test.go └── storedRecording.go └── session ├── dialog.go ├── events.go ├── message.go ├── objects.go ├── objects_test.go ├── transport.go └── utils_test.go /.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 27 | run: go build -v ./... 28 | 29 | - name: Test 30 | run: go test -v ./... 31 | -------------------------------------------------------------------------------- /.github/workflows/master.yaml: -------------------------------------------------------------------------------- 1 | name: Master branch push 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | 9 | image: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: PrepareReg Names 14 | run: | 15 | echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV 16 | echo IMAGE_TAG=$(echo ${{ github.ref }} | tr '[:upper:]' '[:lower:]' | awk '{split($0,a,"/"); print a[3]}') >> $GITHUB_ENV 17 | 18 | - name: Set up Buildx 19 | id: buildx 20 | uses: docker/setup-buildx-action@v1 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: 1.17 26 | 27 | - name: GHCR Login 28 | uses: docker/login-action@v1 29 | with: 30 | registry: ghcr.io 31 | username: ${{ github.repository_owner }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: GHCR Push 35 | uses: docker/build-push-action@v2 36 | with: 37 | push: true 38 | tags: | 39 | ghcr.io/${{ env.IMAGE_REPOSITORY }}:${{ github.sha }} 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Set up Buildx 15 | id: buildx 16 | uses: docker/setup-buildx-action@v1 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: 1.21 22 | 23 | - name: Docker Hub Login 24 | uses: docker/login-action@v1 25 | with: 26 | username: ${{ secrets.DOCKER_USERNAME }} 27 | password: ${{ secrets.DOCKER_PASSWORD }} 28 | 29 | - name: GHCR Login 30 | uses: docker/login-action@v1 31 | with: 32 | registry: ghcr.io 33 | username: ${{ github.repository_owner }} 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | - name: GoReleaser 37 | uses: goreleaser/goreleaser-action@v5.0.0 38 | with: 39 | args: release --rm-dist 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /.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 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | *.swp 28 | 29 | /vendor/* 30 | /dist/ 31 | /ari-proxy 32 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: Asterisk ARI Proxy 2 | builds: 3 | - binary: ari-proxy 4 | env: 5 | - CGO_ENABLED=0 6 | goos: 7 | - windows 8 | - darwin 9 | - linux 10 | goarch: 11 | - amd64 12 | - arm64 13 | 14 | archives: 15 | - id: ari-proxy 16 | format: binary 17 | checksum: 18 | name_template: 'checksums.txt' 19 | snapshot: 20 | name_template: "{{ .Tag }}-next" 21 | changelog: 22 | sort: asc 23 | filters: 24 | exclude: 25 | - '^docs:' 26 | - '^test:' 27 | 28 | dockers: 29 | - image_templates: 30 | - 'cycoresystems/ari-proxy:{{ .Tag }}-amd64' 31 | - 'cycoresystems/ari-proxy:v{{ .Major }}-amd64' 32 | - 'cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}-amd64' 33 | - 'cycoresystems/ari-proxy:latest-amd64' 34 | - 'ghcr.io/cycoresystems/ari-proxy:{{ .Tag }}-amd64' 35 | - 'ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}-amd64' 36 | - 'ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}-amd64' 37 | - 'ghcr.io/cycoresystems/ari-proxy:latest-amd64' 38 | use: buildx 39 | goarch: amd64 40 | dockerfile: Dockerfile.goreleaser 41 | build_flag_templates: 42 | - "--platform=linux/amd64" 43 | - image_templates: 44 | - 'cycoresystems/ari-proxy:{{ .Tag }}-arm64v8' 45 | - 'cycoresystems/ari-proxy:v{{ .Major }}-arm64v8' 46 | - 'cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}-arm64v8' 47 | - 'cycoresystems/ari-proxy:latest-arm64v8' 48 | - 'ghcr.io/cycoresystems/ari-proxy:{{ .Tag }}-arm64v8' 49 | - 'ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}-arm64v8' 50 | - 'ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}-arm64v8' 51 | - 'ghcr.io/cycoresystems/ari-proxy:latest-arm64v8' 52 | use: buildx 53 | goarch: arm64 54 | dockerfile: Dockerfile.goreleaser 55 | build_flag_templates: 56 | - "--platform=linux/arm64/v8" 57 | docker_manifests: 58 | - name_template: 'cycoresystems/ari-proxy:{{ .Tag }}' 59 | image_templates: 60 | - cycoresystems/ari-proxy:{{ .Tag }}-amd64 61 | - cycoresystems/ari-proxy:{{ .Tag }}-arm64v8 62 | - name_template: 'ghcr.io/cycoresystems/ari-proxy:{{ .Tag }}' 63 | image_templates: 64 | - ghcr.io/cycoresystems/ari-proxy:{{ .Tag }}-amd64 65 | - ghcr.io/cycoresystems/ari-proxy:{{ .Tag }}-arm64v8 66 | - name_template: 'cycoresystems/ari-proxy:v{{ .Major }}' 67 | image_templates: 68 | - cycoresystems/ari-proxy:v{{ .Major }}-amd64 69 | - cycoresystems/ari-proxy:v{{ .Major }}-arm64v8 70 | - name_template: 'ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}' 71 | image_templates: 72 | - ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}-amd64 73 | - ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}-arm64v8 74 | - name_template: 'cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}' 75 | image_templates: 76 | - cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}-amd64 77 | - cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}-arm64v8 78 | - name_template: 'ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}' 79 | image_templates: 80 | - ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}-amd64 81 | - ghcr.io/cycoresystems/ari-proxy:v{{ .Major }}.{{ .Minor }}-arm64v8 82 | - name_template: 'cycoresystems/ari-proxy:latest' 83 | image_templates: 84 | - cycoresystems/ari-proxy:latest-amd64 85 | - cycoresystems/ari-proxy:latest-arm64v8 86 | - name_template: 'ghcr.io/cycoresystems/ari-proxy:latest' 87 | image_templates: 88 | - ghcr.io/cycoresystems/ari-proxy:latest-amd64 89 | - ghcr.io/cycoresystems/ari-proxy:latest-arm64v8 90 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine AS builder 2 | RUN apk add --no-cache git 3 | WORKDIR $GOPATH/src/github.com/CyCoreSystems/ari-proxy 4 | COPY . . 5 | RUN go get -d -v 6 | RUN go build -o /go/bin/app 7 | 8 | FROM alpine 9 | RUN apk add --no-cache ca-certificates 10 | COPY --from=builder /go/bin/app /go/bin/app 11 | ENTRYPOINT ["/go/bin/app"] 12 | -------------------------------------------------------------------------------- /Dockerfile.goreleaser: -------------------------------------------------------------------------------- 1 | FROM gcr.io/distroless/base 2 | COPY ari-proxy /go/bin/ari-proxy 3 | ENTRYPOINT ["/go/bin/ari-proxy"] 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 CyCore Systems, Inc. 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: dep check build test 3 | 4 | ci: check build test 5 | 6 | build: 7 | go build ./... 8 | go build 9 | 10 | check: 11 | go mod verify 12 | golangci-lint run 13 | 14 | dep: 15 | go mod tidy 16 | 17 | docker: all 18 | docker build -t cycoresystems/ari-proxy ./ 19 | docker push cycoresystems/ari-proxy 20 | 21 | test: 22 | go test ./... 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ari-proxy 2 | [![Build Status](https://travis-ci.org/CyCoreSystems/ari-proxy.png)](https://travis-ci.org/CyCoreSystems/ari-proxy) [![](https://godoc.org/github.com/CyCoreSystems/ari-proxy?status.svg)](https://godoc.org/github.com/CyCoreSystems/ari-proxy) 3 | 4 | Proxy for the Asterisk REST interface (ARI). 5 | 6 | The ARI proxy facilitates scaling of both applications and Asterisk, 7 | independently and with minimal coordination. Each Asterisk instance and ARI 8 | application pair runs an `ari-proxy` server instance, which talks to a common 9 | NATS or RabbitMQ cluster. Each client application talks to the same message bus. The 10 | clients automatically and continuously discover new Asterisk instances, so the 11 | only coordination needed is the common location of the message bus. 12 | 13 | The ARI proxy allows for: 14 | - Any number of applications running the ARI client 15 | - Any number of `ari-proxy` services running on any number of Asterisk 16 | instances 17 | - Simple call control throughout the cluster, regardless of which Asterisk 18 | instance is controlling the call 19 | - Simple call distribution regardless of the number of potential application 20 | services. (New calls are automatically sent to a single recipient 21 | application.) 22 | - Simple call event reception by any number of application clients. (No 23 | single-app lockout) 24 | 25 | Supported message buses: 26 | - [NATS](https://nats.io) 27 | - [RabbitMQ](https://rabbitmq.com) 28 | 29 | ## Proxy server 30 | 31 | 32 | Docker images are kept up to date with releases and are tagged accordingly. The 33 | `ari-proxy` does not expose any services, so no ports need to be opened for it. 34 | However, it does need to know how to connect to both Asterisk and the message 35 | bus. 36 | 37 | ``` 38 | docker run \ 39 | -e ARI_APPLICATION="my_test_app" \ 40 | -e ARI_USERNAME="demo-user" \ 41 | -e ARI_PASSWORD="supersecret" \ 42 | -e ARI_HTTP_URL="http://asterisk:8088/ari" \ 43 | -e ARI_WEBSOCKET_URL="ws://asterisk:8088/ari/events" \ 44 | -e MESSAGEBUS_URL="nats://nats:4222" \ 45 | cycoresystems/ari-proxy 46 | ``` 47 | 48 | Binary releases are available on the [releases page](https://github.com/CyCoreSystems/ari-proxy/releases). 49 | 50 | You can also install the server manually: 51 | 52 | ``` 53 | go install github.com/CyCoreSystems/ari-proxy/v5 54 | ``` 55 | 56 | ## Client library 57 | 58 | `ari-proxy` uses semantic versioning and standard Go modules. To use it in your 59 | own Go package, simply reference the 60 | `github.com/CyCoreSystems/ari-proxy/client/v5` package, and your dependency 61 | management tool should be able to manage it. 62 | 63 | ### Usage 64 | 65 | Connecting the client to NATS is simple: 66 | 67 | ```go 68 | import ( 69 | "github.com/CyCoreSystems/ari/v5" 70 | "github.com/CyCoreSystems/ari-proxy/v5/client" 71 | ) 72 | 73 | func connect(ctx context.Context, appName string) (ari.Client,error) { 74 | c, err := client.New(ctx, 75 | client.WithApplication(appName), 76 | client.WithURI("nats://natshost:4222"), 77 | ) 78 | } 79 | ``` 80 | 81 | Connecting the client to RabbitMQ is like: 82 | 83 | ```go 84 | import ( 85 | "github.com/CyCoreSystems/ari/v5" 86 | "github.com/CyCoreSystems/ari-proxy/v5/client" 87 | ) 88 | 89 | func connect(ctx context.Context, appName string) (ari.Client,error) { 90 | c, err := client.New(ctx, 91 | client.WithApplication(appName), 92 | client.WithURI("amqp://user:password@rabbitmqhost:5679/"), 93 | ) 94 | } 95 | ``` 96 | 97 | Configuration of the client can also be done with environment variables. 98 | `ARI_APPLICATION` can be used to set the ARI application name, and `MESSAGEBUS_URL` 99 | can be used to set the message bus URL. Doing so allows you to get a client connection 100 | simply with `client.New(ctx)`. 101 | 102 | Once an `ari.Client` is obtained, the client functions exactly as the native 103 | [ari](https://github.com/CyCoreSystems/ari) client. 104 | 105 | More documentation: 106 | 107 | * [ARI library docs](https://godoc.org/github.com/CyCoreSystems/ari) 108 | 109 | * [ARI client examples](https://github.com/CyCoreSystems/ari/tree/master/_examples) 110 | 111 | 112 | ### Context 113 | 114 | Note the use of the `context.Context` parameter. This can be useful to properly 115 | shutdown the client by a controlling context. This shutdown will also close any 116 | open subscriptions on the client. 117 | 118 | Layers of clients can be used efficiently with different contexts using the 119 | `New(context.Context)` function of each client instance. Subtended clients will 120 | be closed with their parents, use a common internal message bus connection, and can be 121 | severally closed by their individual contexts. This makes managing many active 122 | channels easy and efficient. 123 | 124 | ### Lifecycle 125 | 126 | There are two levels of client in use. The first is a connection, which is a 127 | long-lived network connection to the message bus. In general, the end user 128 | should not close this connection. However, it is available, if necessary, as 129 | `DefaultConn` and offers a `Close()` function for itself. 130 | 131 | The second level is the ARI client. Any number of ARI clients may make use of 132 | the same underlying connection, but each client maintains its own separate bus 133 | and subscription implementation. Thus, when a user closes its client, the 134 | connection is maintained, but all subscriptions are released. Users should 135 | always `Close()` their clients when done with them to avoid accumulating stale 136 | subscriptions. 137 | 138 | ### Clustering 139 | 140 | The ARI proxy works in a cluster setting by utilizing two coordinates: 141 | 142 | - The Asterisk ID 143 | - The ARI Application 144 | 145 | Between the two of these, we can uniquely address each ARI resource, regardless 146 | of where the client is located. These pieces of information are handled 147 | transparently and internally by the ARI proxy and the ARI proxy client to route 148 | commands and events where they should be sent. 149 | 150 | ### Message bus protocol details 151 | 152 | The protocol details described below are only necessary to know if you do not use the 153 | provided client and/or server. By using both components in this repository, the 154 | protocol details below are transparently handled for you. 155 | 156 | #### Subject structure 157 | 158 | The message bus subject prefix defaults to `ari.`, and all messages used by this proxy 159 | will be prefixed by that term. 160 | 161 | Next is added one of four resource classifications: 162 | 163 | - `event` - Messages from Asterisk to clients 164 | - `get` - Read-only requests from clients to Asterisk 165 | - `command` - Non-creation operational requests from clients to Asterisk 166 | - `create` - Creation-related requests from clients to Asterisk 167 | 168 | After the resource, the ARI application is appended. 169 | 170 | Finally, the Asterisk ID will be added to the end. Thus, the subject for an event for the 171 | ARI application "test" from the Asterisk box with ID "00:01:02:03:04:05" would 172 | look like: 173 | 174 | `ari.event.test.00:01:02:03:04:05` 175 | 176 | For a channel creation command to the same app and node: 177 | 178 | `ari.create.test.00:01:02:03:04:05` 179 | 180 | The Asterisk ID component of the subject is optional for commands. If a command 181 | does not include an Asterisk ID, any ARI proxy running the provided ARI 182 | application may respond to the request. (Thus, implicitly, each ARI proxy 183 | service listens to both its Asterisk ID-specific command subject and its generic 184 | ARI application command subject. In fact, each ARI proxy listens to each of the 185 | three levels. A request to `ari.command` will result in all ARI proxies 186 | responding.) 187 | 188 | This setup allows for a variable generalization in the listeners by using 189 | message bus 190 | wildcard subscriptions. For instance, if you want to receive all events for the 191 | "test" application regardless from which Asterisk machine they come, you would subscribe to: 192 | 193 | `ari.event.test.>` //NATS 194 | `ari.event.test.#` //RabbitMQ 195 | 196 | #### Dialogs 197 | 198 | Events may be further classified by the arbitrary "dialog" ID. If any command 199 | specifies a Dialog ID in its metadata, the ARI proxy will further classify 200 | events related to that dialog. Relationships are defined by the entity type on 201 | which the Dialog-infused command operates. 202 | 203 | Dialog-related events are published on their own message bus subject tree, 204 | `dialogevent`. Thus dialogs abstract ARI application and Asterisk ID. An event 205 | for dialog "testme123" would be published to: 206 | 207 | `ari.dialogevent.testme123` 208 | 209 | Keep in mind that regardless of dialog associations, all events are _also_ 210 | published to their appropriate canonical message bus subjects. Dialogs are intended as 211 | a mechanism to: 212 | 213 | - reduce client message traffic load 214 | - transcend ARI Applications and/or Asterisk nodes while maintaining logical 215 | separation of events 216 | 217 | #### Message delivery 218 | 219 | The means of a delivery for a generically-routed message depends on the type of 220 | message it is. 221 | 222 | - Events are always delivered to all listeners. 223 | - Read-only commands are delivered to all listeners. 224 | - Non-creation operation commands are delivered to all listeners. 225 | - Creation-related commands are delivered to only one listener. 226 | 227 | Thus, for efficiency, it is always recommended to use as precise a subject line 228 | as possible. 229 | 230 | #### Node discovery 231 | 232 | Each ARI proxy sends out a periodic ping announcing itself in the cluster. 233 | Clients may aggregate these pings to construct an expected map of machines in 234 | the cluster. Knowing this map allows the client to optimize its all-listener 235 | commands by cancelling the wait period if it receives responses from all nodes 236 | before the timeout has elapsed. 237 | 238 | ARI proxies listen to `ari.ping` and send announcements on `ari.announce`. The 239 | structure of the announcement is thus: 240 | 241 | ```json 242 | { 243 | "asterisk": "00:10:20:30:40:50", 244 | "application": "test" 245 | } 246 | ``` 247 | 248 | #### Payload structure 249 | 250 | For most requests, payloads exactly match their ARI library values. However, 251 | treatment of handlers is slightly different. 252 | 253 | Instead of a handler, an `Entity` or array of `Entity`s is returned. This 254 | response type contains the Metadata for the entity (ARI application, Asterisk 255 | ID, and optionally Dialog) as well as the unique ID of the entity. 256 | -------------------------------------------------------------------------------- /_examples/bridge/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/inconshreveable/log15" 8 | "github.com/rotisserie/eris" 9 | 10 | "github.com/CyCoreSystems/ari-proxy/v5/client" 11 | "github.com/CyCoreSystems/ari/v5" 12 | "github.com/CyCoreSystems/ari/v5/ext/play" 13 | "github.com/CyCoreSystems/ari/v5/rid" 14 | ) 15 | 16 | var ariApp = "test" 17 | 18 | var log = log15.New() 19 | 20 | var bridge *ari.BridgeHandle 21 | 22 | func main() { 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | defer cancel() 25 | 26 | // connect 27 | log.Info("Connecting to ARI") 28 | cl, err := client.New(ctx, client.WithApplication(ariApp), client.WithLogger(log)) 29 | if err != nil { 30 | log.Error("Failed to build ARI client", "error", err) 31 | return 32 | } 33 | 34 | // setup app 35 | 36 | log.Info("Starting listener app") 37 | 38 | err = client.Listen(ctx, cl, appStart(ctx, cl)) 39 | if err != nil { 40 | log.Error("failed to listen for new calls") 41 | } 42 | <-ctx.Done() 43 | 44 | return 45 | } 46 | 47 | func appStart(ctx context.Context, cl ari.Client) func(*ari.ChannelHandle, *ari.StasisStart) { 48 | return func(h *ari.ChannelHandle, startEvent *ari.StasisStart) { 49 | log.Info("running app", "channel", h.Key().ID) 50 | 51 | if err := h.Answer(); err != nil { 52 | log.Error("failed to answer call", "error", err) 53 | // return 54 | } 55 | 56 | if err := ensureBridge(ctx, cl, h.Key()); err != nil { 57 | log.Error("failed to manage bridge", "error", err) 58 | return 59 | } 60 | 61 | if err := bridge.AddChannel(h.Key().ID); err != nil { 62 | log.Error("failed to add channel to bridge", "error", err) 63 | return 64 | } 65 | 66 | log.Info("channel added to bridge") 67 | return 68 | } 69 | } 70 | 71 | type bridgeManager struct { 72 | h *ari.BridgeHandle 73 | } 74 | 75 | func ensureBridge(ctx context.Context, cl ari.Client, src *ari.Key) (err error) { 76 | if bridge != nil { 77 | log.Debug("Bridge already exists") 78 | return nil 79 | } 80 | 81 | key := src.New(ari.BridgeKey, rid.New(rid.Bridge)) 82 | bridge, err = cl.Bridge().Create(key, "mixing", key.ID) 83 | if err != nil { 84 | bridge = nil 85 | return eris.Wrap(err, "failed to create bridge") 86 | } 87 | 88 | wg := new(sync.WaitGroup) 89 | wg.Add(1) 90 | go manageBridge(ctx, bridge, wg) 91 | wg.Wait() 92 | 93 | return nil 94 | } 95 | 96 | func manageBridge(ctx context.Context, h *ari.BridgeHandle, wg *sync.WaitGroup) { 97 | // Delete the bridge when we exit 98 | defer h.Delete() 99 | 100 | destroySub := h.Subscribe(ari.Events.BridgeDestroyed) 101 | defer destroySub.Cancel() 102 | 103 | enterSub := h.Subscribe(ari.Events.ChannelEnteredBridge) 104 | defer enterSub.Cancel() 105 | 106 | leaveSub := h.Subscribe(ari.Events.ChannelLeftBridge) 107 | defer leaveSub.Cancel() 108 | 109 | wg.Done() 110 | for { 111 | select { 112 | case <-ctx.Done(): 113 | return 114 | case <-destroySub.Events(): 115 | log.Debug("bridge destroyed") 116 | return 117 | case e, ok := <-enterSub.Events(): 118 | if !ok { 119 | log.Error("channel entered subscription closed") 120 | return 121 | } 122 | v := e.(*ari.ChannelEnteredBridge) 123 | log.Debug("channel entered bridge", "channel", v.Channel.Name) 124 | go func() { 125 | if err := play.Play(ctx, h, play.URI("sound:confbridge-join")).Err(); err != nil { 126 | log.Error("failed to play join sound", "error", err) 127 | } 128 | }() 129 | case e, ok := <-leaveSub.Events(): 130 | if !ok { 131 | log.Error("channel left subscription closed") 132 | return 133 | } 134 | v := e.(*ari.ChannelLeftBridge) 135 | log.Debug("channel left bridge", "channel", v.Channel.Name) 136 | go func() { 137 | if err := play.Play(ctx, h, play.URI("sound:confbridge-leave")).Err(); err != nil { 138 | log.Error("failed to play leave sound", "error", err) 139 | } 140 | }() 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /_examples/play/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/inconshreveable/log15" 7 | 8 | "github.com/CyCoreSystems/ari-proxy/v5/client" 9 | "github.com/CyCoreSystems/ari/v5" 10 | "github.com/CyCoreSystems/ari/v5/ext/play" 11 | ) 12 | 13 | var ariApp = "test" 14 | 15 | var log = log15.New() 16 | 17 | func main() { 18 | ctx, cancel := context.WithCancel(context.Background()) 19 | defer cancel() 20 | 21 | // connect 22 | log.Info("Connecting to ARI") 23 | cl, err := client.New(ctx, client.WithApplication(ariApp), client.WithLogger(log)) 24 | if err != nil { 25 | log.Error("Failed to build ARI client", "error", err) 26 | return 27 | } 28 | 29 | // setup app 30 | 31 | log.Info("Starting listener app") 32 | 33 | err = client.Listen(ctx, cl, appStart(ctx)) 34 | if err != nil { 35 | log.Error("failed to listen for new calls") 36 | } 37 | <-ctx.Done() 38 | 39 | return 40 | } 41 | 42 | func appStart(ctx context.Context) func(*ari.ChannelHandle, *ari.StasisStart) { 43 | return func(h *ari.ChannelHandle, startEvent *ari.StasisStart) { 44 | defer h.Hangup() 45 | 46 | log.Info("running app", "channel", h.Key().ID) 47 | 48 | if err := h.Answer(); err != nil { 49 | log.Error("failed to answer call", "error", err) 50 | // return 51 | } 52 | 53 | if err := play.Play(ctx, h, play.URI("sound:tt-monkeys")).Err(); err != nil { 54 | log.Error("failed to play sound", "error", err) 55 | return 56 | } 57 | 58 | log.Info("completed playback") 59 | return 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /_examples/record/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/inconshreveable/log15" 7 | 8 | "github.com/CyCoreSystems/ari-proxy/v5/client" 9 | "github.com/CyCoreSystems/ari/v5" 10 | "github.com/CyCoreSystems/ari/v5/ext/record" 11 | ) 12 | 13 | var ariApp = "test" 14 | 15 | var log = log15.New() 16 | 17 | func main() { 18 | ctx, cancel := context.WithCancel(context.Background()) 19 | defer cancel() 20 | 21 | record.Logger = log 22 | 23 | // connect 24 | log.Info("Connecting to ARI") 25 | cl, err := client.New(ctx, client.WithApplication(ariApp), client.WithLogger(log)) 26 | if err != nil { 27 | log.Error("Failed to build ARI client", "error", err) 28 | return 29 | } 30 | 31 | // setup app 32 | 33 | log.Info("Starting listener app") 34 | 35 | err = client.Listen(ctx, cl, appStart(ctx)) 36 | if err != nil { 37 | log.Error("failed to listen for new calls") 38 | } 39 | <-ctx.Done() 40 | 41 | return 42 | } 43 | 44 | func appStart(ctx context.Context) func(*ari.ChannelHandle, *ari.StasisStart) { 45 | return func(h *ari.ChannelHandle, startEvent *ari.StasisStart) { 46 | defer h.Hangup() 47 | 48 | log.Info("running app", "channel", h.Key().ID) 49 | 50 | if err := h.Answer(); err != nil { 51 | log.Error("failed to answer call", "error", err) 52 | // return 53 | } 54 | 55 | res, err := record.Record(ctx, h, 56 | record.TerminateOn("any"), 57 | record.IfExists("overwrite"), 58 | ).Result() 59 | if err != nil { 60 | log.Error("failed to record", "error", err) 61 | return 62 | } 63 | 64 | if err = res.Save("test-recording"); err != nil { 65 | log.Error("failed to save recording", "error", err) 66 | } 67 | 68 | log.Info("completed recording") 69 | 70 | h.Hangup() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /_examples/stasisStart/.gitignore: -------------------------------------------------------------------------------- 1 | /stasisStart 2 | -------------------------------------------------------------------------------- /_examples/stasisStart/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "sync" 7 | 8 | "github.com/CyCoreSystems/ari-proxy/v5/client" 9 | "github.com/CyCoreSystems/ari/v5" 10 | "github.com/CyCoreSystems/ari/v5/client/native" 11 | 12 | "github.com/inconshreveable/log15" 13 | ) 14 | 15 | var ariApp = "test" 16 | 17 | var log = log15.New() 18 | 19 | func main() { 20 | ctx, cancel := context.WithCancel(context.Background()) 21 | defer cancel() 22 | 23 | // connect 24 | native.Logger = log 25 | 26 | log.Info("Connecting to ARI") 27 | cl, err := client.New(ctx, client.WithApplication(ariApp)) 28 | if err != nil { 29 | log.Error("Failed to build ARI client", "error", err) 30 | return 31 | } 32 | 33 | // setup app 34 | 35 | log.Info("Starting listener app") 36 | 37 | listenApp(ctx, cl, channelHandler) 38 | 39 | // start call start listener 40 | 41 | log.Info("Starting HTTP Handler") 42 | 43 | http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 | // make call 45 | log.Info("Make sample call") 46 | h, err := createCall(cl) 47 | if err != nil { 48 | log.Error("Failed to create call", "error", err) 49 | w.WriteHeader(http.StatusBadGateway) 50 | w.Write([]byte("Failed to create call: " + err.Error())) 51 | return 52 | } 53 | 54 | w.WriteHeader(http.StatusOK) 55 | w.Write([]byte(h.ID())) 56 | })) 57 | 58 | log.Info("Listening for requests on port 9990") 59 | http.ListenAndServe(":9990", nil) 60 | 61 | return 62 | } 63 | 64 | func listenApp(ctx context.Context, cl ari.Client, handler func(*ari.ChannelHandle, *ari.StasisStart)) { 65 | err := client.Listen(ctx, cl, func(ch *ari.ChannelHandle, v *ari.StasisStart) { 66 | log.Info("Got stasis start", "channel", v.Channel.ID) 67 | go handler(ch, v) 68 | }) 69 | if err != nil { 70 | log.Crit("failed to listen for new calls", "error", err) 71 | } 72 | return 73 | } 74 | 75 | func createCall(cl ari.Client) (h *ari.ChannelHandle, err error) { 76 | h, err = cl.Channel().Create(nil, ari.ChannelCreateRequest{ 77 | Endpoint: "Local/1000", 78 | App: ariApp, 79 | }) 80 | 81 | return 82 | } 83 | 84 | func channelHandler(h *ari.ChannelHandle, startEvent *ari.StasisStart) { 85 | log.Info("Running channel handler") 86 | 87 | // Subscribe to channel state changes 88 | stateChange := h.Subscribe(ari.Events.ChannelStateChange) 89 | defer stateChange.Cancel() 90 | 91 | // Subscribe to StasisEnd events (channel leaving ARI app) 92 | end := h.Subscribe(ari.Events.StasisEnd) 93 | defer end.Cancel() 94 | 95 | // Pull the current channel data 96 | data, err := h.Data() 97 | if err != nil { 98 | log.Error("Error getting data", "error", err) 99 | return 100 | } 101 | log.Info("Channel State", "state", data.State) 102 | 103 | var wg sync.WaitGroup 104 | 105 | wg.Add(1) 106 | go func() { 107 | log.Info("Waiting for channel events") 108 | 109 | defer wg.Done() 110 | 111 | for { 112 | select { 113 | case <-end.Events(): 114 | log.Info("Got stasis end") 115 | return 116 | case e := <-stateChange.Events(): 117 | v, ok := e.(*ari.ChannelStateChange) 118 | if !ok { 119 | log.Error("failed to interpret event as ChannelStateChange", "error", err) 120 | return 121 | } 122 | 123 | log.Info("New Channel State", "state", v.Channel.State) 124 | } 125 | } 126 | }() 127 | 128 | h.Answer() 129 | 130 | wg.Wait() 131 | 132 | h.Hangup() 133 | } 134 | -------------------------------------------------------------------------------- /ari-proxy.yaml: -------------------------------------------------------------------------------- 1 | ari: 2 | username: admin 3 | password: admin 4 | application: example 5 | http_url: http://localhost:8088/ari 6 | websocket_url: ws://localhost:8088/ari/events 7 | messagebus: 8 | url: nats://nats:4222 9 | -------------------------------------------------------------------------------- /ci_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Only run if we are inside Travis-CI 4 | if [ ! -e $CI ]; then 5 | echo "Logging in to Docker..." 6 | echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin 7 | 8 | # Create the release 9 | echo "Creating release..." 10 | goreleaser release 11 | fi 12 | 13 | -------------------------------------------------------------------------------- /client/application.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type application struct { 9 | c *Client 10 | } 11 | 12 | func (a *application) List(filter *ari.Key) ([]*ari.Key, error) { 13 | return a.c.listRequest(&proxy.Request{ 14 | Kind: "ApplicationList", 15 | Key: filter, 16 | }) 17 | } 18 | 19 | func (a *application) Data(key *ari.Key) (*ari.ApplicationData, error) { 20 | ret, err := a.c.dataRequest(&proxy.Request{ 21 | Kind: "ApplicationData", 22 | Key: key, 23 | }) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return ret.Application, nil 28 | } 29 | 30 | func (a *application) Get(key *ari.Key) *ari.ApplicationHandle { 31 | k, err := a.c.getRequest(&proxy.Request{ 32 | Kind: "ApplicationGet", 33 | Key: key, 34 | }) 35 | if err != nil { 36 | a.c.log.Warn("failed to make data request for application", "error", err) 37 | return ari.NewApplicationHandle(key, a) 38 | } 39 | return ari.NewApplicationHandle(k, a) 40 | } 41 | 42 | func (a *application) Subscribe(key *ari.Key, eventSource string) (err error) { 43 | return a.c.commandRequest(&proxy.Request{ 44 | Kind: "ApplicationSubscribe", 45 | Key: key, 46 | ApplicationSubscribe: &proxy.ApplicationSubscribe{ 47 | EventSource: eventSource, 48 | }, 49 | }) 50 | } 51 | 52 | func (a *application) Unsubscribe(key *ari.Key, eventSource string) (err error) { 53 | return a.c.commandRequest(&proxy.Request{ 54 | Kind: "ApplicationUnsubscribe", 55 | Key: key, 56 | ApplicationSubscribe: &proxy.ApplicationSubscribe{ 57 | EventSource: eventSource, 58 | }, 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /client/application_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestApplicationList(t *testing.T) { 10 | integration.TestApplicationList(t, &srv{}) 11 | } 12 | 13 | func TestApplicationData(t *testing.T) { 14 | integration.TestApplicationData(t, &srv{}) 15 | } 16 | 17 | func TestApplicationSubscribe(t *testing.T) { 18 | integration.TestApplicationSubscribe(t, &srv{}) 19 | } 20 | 21 | func TestApplicationUnsubscribe(t *testing.T) { 22 | integration.TestApplicationUnsubscribe(t, &srv{}) 23 | } 24 | 25 | func TestApplicationGet(t *testing.T) { 26 | integration.TestApplicationGet(t, &srv{}) 27 | } 28 | -------------------------------------------------------------------------------- /client/asterisk.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type asterisk struct { 9 | c *Client 10 | } 11 | 12 | func (a *asterisk) Config() ari.Config { 13 | return &config{a.c} 14 | } 15 | 16 | type asteriskVariables struct { 17 | c *Client 18 | } 19 | 20 | func (a *asterisk) Logging() ari.Logging { 21 | return &logging{a.c} 22 | } 23 | 24 | func (a *asterisk) Modules() ari.Modules { 25 | return &modules{a.c} 26 | } 27 | 28 | func (a *asterisk) Info(key *ari.Key) (*ari.AsteriskInfo, error) { 29 | resp, err := a.c.dataRequest(&proxy.Request{ 30 | Kind: "AsteriskInfo", 31 | Key: key, 32 | }) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return resp.Asterisk, nil 37 | } 38 | 39 | func (a *asterisk) Variables() ari.AsteriskVariables { 40 | return &asteriskVariables{a.c} 41 | } 42 | 43 | func (a *asteriskVariables) Get(key *ari.Key) (ret string, err error) { 44 | data, err := a.c.dataRequest(&proxy.Request{ 45 | Kind: "AsteriskVariableGet", 46 | Key: key, 47 | }) 48 | if err != nil { 49 | return "", err 50 | } 51 | return data.Variable, err 52 | } 53 | 54 | func (a *asteriskVariables) Set(key *ari.Key, val string) (err error) { 55 | return a.c.commandRequest(&proxy.Request{ 56 | Kind: "AsteriskVariableSet", 57 | Key: key, 58 | AsteriskVariableSet: &proxy.AsteriskVariableSet{ 59 | Value: val, 60 | }, 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /client/asterisk_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestAsteriskInfo(t *testing.T) { 10 | integration.TestAsteriskInfo(t, &srv{}) 11 | } 12 | 13 | func TestAsteriskVariablesGet(t *testing.T) { 14 | integration.TestAsteriskVariablesGet(t, &srv{}) 15 | } 16 | 17 | func TestAsteriskVariablesSet(t *testing.T) { 18 | integration.TestAsteriskVariablesSet(t, &srv{}) 19 | } 20 | -------------------------------------------------------------------------------- /client/bridge.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | "github.com/CyCoreSystems/ari/v5/rid" 7 | ) 8 | 9 | type bridge struct { 10 | c *Client 11 | } 12 | 13 | func (b *bridge) Create(key *ari.Key, btype, name string) (*ari.BridgeHandle, error) { 14 | k, err := b.c.createRequest(&proxy.Request{ 15 | Kind: "BridgeCreate", 16 | Key: key, 17 | BridgeCreate: &proxy.BridgeCreate{ 18 | Type: btype, 19 | Name: name, 20 | }, 21 | }) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return ari.NewBridgeHandle(k, b, nil), nil 26 | } 27 | 28 | func (b *bridge) StageCreate(key *ari.Key, btype, name string) (*ari.BridgeHandle, error) { 29 | k, err := b.c.createRequest(&proxy.Request{ 30 | Kind: "BridgeStageCreate", 31 | Key: key, 32 | BridgeCreate: &proxy.BridgeCreate{ 33 | Type: btype, 34 | Name: name, 35 | }, 36 | }) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return ari.NewBridgeHandle(k, b, func(h *ari.BridgeHandle) error { 41 | _, err := b.Create(k, btype, name) 42 | return err 43 | }), nil 44 | } 45 | 46 | func (b *bridge) Get(key *ari.Key) *ari.BridgeHandle { 47 | k, err := b.c.getRequest(&proxy.Request{ 48 | Kind: "BridgeGet", 49 | Key: key, 50 | }) 51 | if err != nil { 52 | b.c.log.Warn("failed to get bridge for handle", "error", err) 53 | return ari.NewBridgeHandle(key, b, nil) 54 | } 55 | return ari.NewBridgeHandle(k, b, nil) 56 | } 57 | 58 | func (b *bridge) List(filter *ari.Key) ([]*ari.Key, error) { 59 | return b.c.listRequest(&proxy.Request{ 60 | Kind: "BridgeList", 61 | Key: filter, 62 | }) 63 | } 64 | 65 | func (b *bridge) Data(key *ari.Key) (*ari.BridgeData, error) { 66 | resp, err := b.c.dataRequest(&proxy.Request{ 67 | Kind: "BridgeData", 68 | Key: key, 69 | }) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return resp.Bridge, nil 74 | } 75 | 76 | func (b *bridge) AddChannel(key *ari.Key, channelID string) error { 77 | return b.AddChannelWithOptions(key, channelID, nil) 78 | } 79 | 80 | func (b *bridge) AddChannelWithOptions(key *ari.Key, channelID string, options *ari.BridgeAddChannelOptions) error { 81 | if options == nil { 82 | options = new(ari.BridgeAddChannelOptions) 83 | } 84 | 85 | return b.c.commandRequest(&proxy.Request{ 86 | Kind: "BridgeAddChannel", 87 | Key: key, 88 | BridgeAddChannel: &proxy.BridgeAddChannel{ 89 | Channel: channelID, 90 | AbsorbDTMF: options.AbsorbDTMF, 91 | Mute: options.Mute, 92 | Role: options.Role, 93 | }, 94 | }) 95 | } 96 | 97 | func (b *bridge) RemoveChannel(key *ari.Key, channelID string) error { 98 | return b.c.commandRequest(&proxy.Request{ 99 | Kind: "BridgeRemoveChannel", 100 | Key: key, 101 | BridgeRemoveChannel: &proxy.BridgeRemoveChannel{ 102 | Channel: channelID, 103 | }, 104 | }) 105 | } 106 | 107 | func (b *bridge) Delete(key *ari.Key) error { 108 | return b.c.commandRequest(&proxy.Request{ 109 | Kind: "BridgeDelete", 110 | Key: key, 111 | }) 112 | } 113 | 114 | func (b *bridge) MOH(key *ari.Key, class string) error { 115 | return b.c.commandRequest(&proxy.Request{ 116 | Kind: "BridgeMOH", 117 | Key: key, 118 | BridgeMOH: &proxy.BridgeMOH{ 119 | Class: class, 120 | }, 121 | }) 122 | } 123 | 124 | func (b *bridge) StopMOH(key *ari.Key) error { 125 | return b.c.commandRequest(&proxy.Request{ 126 | Kind: "BridgeStopMOH", 127 | Key: key, 128 | }) 129 | } 130 | 131 | func (b *bridge) Play(key *ari.Key, id string, uri string) (*ari.PlaybackHandle, error) { 132 | if id == "" { 133 | id = rid.New(rid.Playback) 134 | } 135 | k, err := b.c.createRequest(&proxy.Request{ 136 | Kind: "BridgePlay", 137 | Key: key, 138 | BridgePlay: &proxy.BridgePlay{ 139 | MediaURI: uri, 140 | PlaybackID: id, 141 | }, 142 | }) 143 | if err != nil { 144 | return nil, err 145 | } 146 | return ari.NewPlaybackHandle(k.New(ari.PlaybackKey, id), b.c.Playback(), nil), nil 147 | } 148 | 149 | func (b *bridge) StagePlay(key *ari.Key, id string, uri string) (*ari.PlaybackHandle, error) { 150 | if id == "" { 151 | id = rid.New(rid.Playback) 152 | } 153 | k, err := b.c.getRequest(&proxy.Request{ 154 | Kind: "BridgeStagePlay", 155 | Key: key, 156 | BridgePlay: &proxy.BridgePlay{ 157 | MediaURI: uri, 158 | PlaybackID: id, 159 | }, 160 | }) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | return ari.NewPlaybackHandle(k.New(ari.PlaybackKey, id), b.c.Playback(), func(h *ari.PlaybackHandle) error { 166 | _, err := b.Play(k.New(ari.BridgeKey, key.ID), id, uri) 167 | return err 168 | }), nil 169 | } 170 | 171 | func (b *bridge) Record(key *ari.Key, name string, opts *ari.RecordingOptions) (*ari.LiveRecordingHandle, error) { 172 | if opts == nil { 173 | opts = &ari.RecordingOptions{} 174 | } 175 | if name == "" { 176 | name = rid.New(rid.Recording) 177 | } 178 | 179 | k, err := b.c.createRequest(&proxy.Request{ 180 | Kind: "BridgeRecord", 181 | Key: key, 182 | BridgeRecord: &proxy.BridgeRecord{ 183 | Name: name, 184 | Options: opts, 185 | }, 186 | }) 187 | if err != nil { 188 | return nil, err 189 | } 190 | return ari.NewLiveRecordingHandle(k.New(ari.LiveRecordingKey, name), b.c.LiveRecording(), nil), nil 191 | } 192 | 193 | func (b *bridge) StageRecord(key *ari.Key, name string, opts *ari.RecordingOptions) (*ari.LiveRecordingHandle, error) { 194 | if opts == nil { 195 | opts = &ari.RecordingOptions{} 196 | } 197 | if name == "" { 198 | name = rid.New(rid.Recording) 199 | } 200 | 201 | k, err := b.c.getRequest(&proxy.Request{ 202 | Kind: "BridgeStageRecord", 203 | Key: key, 204 | BridgeRecord: &proxy.BridgeRecord{ 205 | Name: name, 206 | Options: opts, 207 | }, 208 | }) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | return ari.NewLiveRecordingHandle(k.New(ari.LiveRecordingKey, name), b.c.LiveRecording(), func(h *ari.LiveRecordingHandle) error { 214 | _, err := b.Record(k.New(ari.BridgeKey, key.ID), name, opts) 215 | return err 216 | }), nil 217 | } 218 | 219 | func (b *bridge) Subscribe(key *ari.Key, n ...string) ari.Subscription { 220 | err := b.c.commandRequest(&proxy.Request{ 221 | Kind: "BridgeSubscribe", 222 | Key: key, 223 | }) 224 | if err != nil { 225 | b.c.log.Warn("failed to call bridge subscribe") 226 | if key.Dialog != "" { 227 | b.c.log.Error("dialog present; failing") 228 | return nil 229 | } 230 | } 231 | 232 | return b.c.Bus().Subscribe(key, n...) 233 | } 234 | 235 | func (b *bridge) VideoSource(key *ari.Key, channelID string) error { 236 | return b.c.commandRequest(&proxy.Request{ 237 | Kind: "BridgeVideoSource", 238 | Key: key, 239 | BridgeVideoSource: &proxy.BridgeVideoSource{ 240 | Channel: channelID, 241 | }, 242 | }) 243 | } 244 | 245 | func (b *bridge) VideoSourceDelete(key *ari.Key) error { 246 | return b.c.commandRequest(&proxy.Request{ 247 | Kind: "BridgeVideoSourceDelete", 248 | Key: key, 249 | }) 250 | } 251 | -------------------------------------------------------------------------------- /client/bridge_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestBridgeCreate(t *testing.T) { 10 | integration.TestBridgeCreate(t, &srv{}) 11 | } 12 | 13 | func TestBridgeList(t *testing.T) { 14 | integration.TestBridgeList(t, &srv{}) 15 | } 16 | 17 | func TestBridgeData(t *testing.T) { 18 | integration.TestBridgeData(t, &srv{}) 19 | } 20 | 21 | func TestBridgeAddChannel(t *testing.T) { 22 | integration.TestBridgeAddChannel(t, &srv{}) 23 | } 24 | 25 | func TestBridgeRemoveChannel(t *testing.T) { 26 | integration.TestBridgeRemoveChannel(t, &srv{}) 27 | } 28 | 29 | func TestBridgeDelete(t *testing.T) { 30 | integration.TestBridgeDelete(t, &srv{}) 31 | } 32 | 33 | func TestBridgePlay(t *testing.T) { 34 | integration.TestBridgePlay(t, &srv{}) 35 | } 36 | 37 | func TestBridgeRecord(t *testing.T) { 38 | integration.TestBridgeRecord(t, &srv{}) 39 | } 40 | -------------------------------------------------------------------------------- /client/bus/bus.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/CyCoreSystems/ari-proxy/v5/messagebus" 8 | "github.com/CyCoreSystems/ari/v5" 9 | "github.com/inconshreveable/log15" 10 | ) 11 | 12 | // EventChanBufferLength is the number of unhandled events which can be queued 13 | // to the event channel buffer before further events are lost. 14 | var EventChanBufferLength = 10 15 | 16 | // Bus provides an ari.Bus interface to MessageBus 17 | type Bus struct { 18 | prefix string 19 | 20 | log log15.Logger 21 | 22 | mbus messagebus.Client 23 | } 24 | 25 | // New returns a new Bus 26 | func New(prefix string, m messagebus.Client, log log15.Logger) *Bus { 27 | return &Bus{ 28 | prefix: prefix, 29 | log: log, 30 | mbus: m, 31 | } 32 | } 33 | 34 | func (b *Bus) subjectFromKey(key *ari.Key) string { 35 | if key == nil { 36 | return fmt.Sprintf( 37 | "%sevent.%s", 38 | b.prefix, 39 | b.mbus.GetWildcardString(messagebus.WildcardZeroOrMoreWords), 40 | ) 41 | } 42 | 43 | if key.Dialog != "" { 44 | return fmt.Sprintf("%sdialogevent.%s", b.prefix, key.Dialog) 45 | } 46 | 47 | subj := fmt.Sprintf("%sevent.", b.prefix) 48 | if key.App == "" { 49 | return subj + b.mbus.GetWildcardString(messagebus.WildcardZeroOrMoreWords) 50 | } 51 | subj += key.App + "." 52 | 53 | if key.Node == "" { 54 | return subj + b.mbus.GetWildcardString(messagebus.WildcardZeroOrMoreWords) 55 | } 56 | return subj + key.Node 57 | } 58 | 59 | // Subscription represents an ari.Subscription over MessageBus 60 | type Subscription struct { 61 | key *ari.Key 62 | 63 | log log15.Logger 64 | 65 | subscription messagebus.Subscription 66 | 67 | eventChan chan ari.Event 68 | 69 | events []string 70 | 71 | closed bool 72 | 73 | mu sync.RWMutex 74 | } 75 | 76 | // Close implements ari.Bus 77 | func (b *Bus) Close() { 78 | // No-op 79 | } 80 | 81 | // Send implements ari.Bus 82 | func (b *Bus) Send(e ari.Event) { 83 | // No-op 84 | } 85 | 86 | // Subscribe implements ari.Bus 87 | func (b *Bus) Subscribe(key *ari.Key, n ...string) ari.Subscription { 88 | var err error 89 | 90 | s := &Subscription{ 91 | key: key, 92 | log: b.log, 93 | eventChan: make(chan ari.Event, EventChanBufferLength), 94 | events: n, 95 | } 96 | 97 | var app string 98 | if key != nil { 99 | app = key.App 100 | } 101 | 102 | s.subscription, err = b.mbus.SubscribeEvent( 103 | b.subjectFromKey(key), 104 | app, 105 | s.receive, 106 | ) 107 | if err != nil { 108 | b.log.Error("failed to subscribe to MessageBus", "error", err) 109 | return nil 110 | } 111 | return s 112 | } 113 | 114 | // Events returns the channel on which events from this subscription will be sent 115 | func (s *Subscription) Events() <-chan ari.Event { 116 | return s.eventChan 117 | } 118 | 119 | // Cancel destroys the subscription 120 | func (s *Subscription) Cancel() { 121 | if s == nil { 122 | return 123 | } 124 | 125 | if s.subscription != nil { 126 | err := s.subscription.Unsubscribe() 127 | if err != nil { 128 | s.log.Error("failed unsubscribe from MessageBus", "error", err) 129 | } 130 | } 131 | 132 | s.mu.Lock() 133 | if !s.closed { 134 | s.closed = true 135 | close(s.eventChan) 136 | } 137 | s.mu.Unlock() 138 | } 139 | 140 | func (s *Subscription) receive(data []byte) { 141 | e, err := ari.DecodeEvent(data) 142 | if err != nil { 143 | s.log.Error("failed to convert received message to ari.Event", "error", err) 144 | return 145 | } 146 | 147 | if s.matchEvent(e) { 148 | s.mu.RLock() 149 | if !s.closed { 150 | s.eventChan <- e 151 | } 152 | s.mu.RUnlock() 153 | } 154 | } 155 | 156 | func (s *Subscription) matchEvent(o ari.Event) bool { 157 | // First, filter by type 158 | var match bool 159 | for _, kind := range s.events { 160 | if kind == o.GetType() || kind == ari.Events.All { 161 | match = true 162 | break 163 | } 164 | } 165 | if !match { 166 | return false 167 | } 168 | 169 | // If we don't have a resource ID, we match everything 170 | // Next, match the entity 171 | if s.key == nil || s.key.ID == "" { 172 | return true 173 | } 174 | 175 | for _, k := range o.Keys() { 176 | if s.key.Match(k) { 177 | return true 178 | } 179 | } 180 | return false 181 | } 182 | -------------------------------------------------------------------------------- /client/bus/bus_test.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/inconshreveable/log15" 8 | 9 | "github.com/CyCoreSystems/ari/v5" 10 | ) 11 | 12 | func TestMatchEvent(t *testing.T) { 13 | key := &ari.Key{ 14 | Kind: ari.ChannelKey, 15 | ID: "testA", 16 | Node: "0test0", 17 | App: "testApp", 18 | } 19 | 20 | e := &ari.StasisEnd{ 21 | EventData: ari.EventData{ 22 | Type: "StasisEnd", 23 | Application: "testApp", 24 | Node: "0test0", 25 | Timestamp: ari.DateTime(time.Now()), 26 | }, 27 | Header: make(ari.Header), 28 | Channel: ari.ChannelData{ 29 | Key: nil, 30 | ID: "testB", 31 | Name: "Local/bozo", 32 | State: "up", 33 | Accountcode: "49er", 34 | Dialplan: &ari.DialplanCEP{ 35 | Context: "default", 36 | Exten: "s", 37 | Priority: 1, 38 | }, 39 | }, 40 | } 41 | 42 | s := &Subscription{ 43 | key: key, 44 | log: log15.New(), 45 | eventChan: make(chan ari.Event, EventChanBufferLength), 46 | events: []string{"StasisEnd"}, 47 | } 48 | 49 | if s.matchEvent(e) { 50 | t.Error("matched incorrect event") 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/channel_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestChannelData(t *testing.T) { 10 | integration.TestChannelData(t, &srv{}) 11 | } 12 | 13 | func TestChannelAnswer(t *testing.T) { 14 | integration.TestChannelAnswer(t, &srv{}) 15 | } 16 | 17 | func TestChannelBusy(t *testing.T) { 18 | integration.TestChannelBusy(t, &srv{}) 19 | } 20 | 21 | func TestChannelCongestion(t *testing.T) { 22 | integration.TestChannelCongestion(t, &srv{}) 23 | } 24 | 25 | func TestChannelHangup(t *testing.T) { 26 | integration.TestChannelHangup(t, &srv{}) 27 | } 28 | 29 | func TestChannelList(t *testing.T) { 30 | integration.TestChannelList(t, &srv{}) 31 | } 32 | 33 | func TestChannelMute(t *testing.T) { 34 | integration.TestChannelMute(t, &srv{}) 35 | } 36 | 37 | func TestChannelUnmute(t *testing.T) { 38 | integration.TestChannelUnmute(t, &srv{}) 39 | } 40 | 41 | func TestChannelMOH(t *testing.T) { 42 | integration.TestChannelMOH(t, &srv{}) 43 | } 44 | 45 | func TestChannelStopMOH(t *testing.T) { 46 | integration.TestChannelStopMOH(t, &srv{}) 47 | } 48 | 49 | func TestChannelContinue(t *testing.T) { 50 | integration.TestChannelContinue(t, &srv{}) 51 | } 52 | -------------------------------------------------------------------------------- /client/clientserver_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/CyCoreSystems/ari-proxy/v5/server" 10 | "github.com/CyCoreSystems/ari/v5" 11 | "github.com/CyCoreSystems/ari/v5/rid" 12 | "github.com/nats-io/nats.go" 13 | ) 14 | 15 | type srv struct { 16 | s *server.Server 17 | } 18 | 19 | func (s *srv) Start(ctx context.Context, t *testing.T, mockClient ari.Client, nc *nats.EncodedConn, completeCh chan struct{}) (ari.Client, error) { 20 | s.s = server.New() 21 | 22 | // tests may run in parallel so we don't want two separate proxy servers to conflict. 23 | s.s.MBPrefix = rid.New("") + "." 24 | 25 | go func() { 26 | if err := s.s.ListenOn(ctx, mockClient, nc); err != nil { 27 | if err != context.Canceled { 28 | t.Errorf("Failed to start server: %s", err) 29 | } 30 | } 31 | close(completeCh) 32 | }() 33 | 34 | select { 35 | case <-s.s.Ready(): 36 | case <-time.After(500 * time.Millisecond): 37 | return nil, errors.New("Timeout waiting for server ready") 38 | } 39 | 40 | cl, err := New(ctx, WithTimeoutRetries(4), WithPrefix(s.s.MBPrefix), WithApplication("asdf")) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return cl, nil 46 | } 47 | 48 | func (s *srv) Ready() <-chan struct{} { 49 | return s.s.Ready() 50 | } 51 | 52 | func (s *srv) Close() error { 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /client/cluster/cluster.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "strings" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // AutoPurgeInterval is the maximum amount of time to wait before automatically purging the cluster of stale members 10 | var AutoPurgeInterval = 24 * time.Hour 11 | 12 | // AutoPurgeAge is the maximum age allowed for members' last update when automatically purging. 13 | var AutoPurgeAge = 12 * time.Hour 14 | 15 | // Cluster describes the set of ari proxies in a system. The list is indexed by a hash of the asterisk ID and the ARI application and indicates the time of last contact. 16 | type Cluster struct { 17 | lastPurge time.Time 18 | 19 | members map[string]time.Time 20 | 21 | mu sync.Mutex 22 | } 23 | 24 | // New returns a new Cluster 25 | func New() *Cluster { 26 | return &Cluster{ 27 | members: make(map[string]time.Time), 28 | } 29 | } 30 | 31 | // hash returns the key for a given proxy instance 32 | func hash(id, app string) string { 33 | return id + "|" + app 34 | } 35 | 36 | // dehash returns the Asterisk ID and ARI application represented by the given key 37 | func dehash(key string) (id string, app string) { 38 | pieces := strings.Split(key, "|") 39 | if len(pieces) < 2 { 40 | return 41 | } 42 | return pieces[0], pieces[1] 43 | } 44 | 45 | // Member describes the state of a Member of an application cluster 46 | type Member struct { 47 | // ID is the unique identifier for the Asterisk node 48 | ID string 49 | 50 | // App indicates the ARI application of this proxy 51 | App string 52 | 53 | // LastActive is the timestamp of the last occurrence of this node 54 | LastActive time.Time 55 | } 56 | 57 | // All returns a list of all cluster members whose LastActive time is no older thatn the given maxAge. 58 | func (c *Cluster) All(maxAge time.Duration) (list []Member) { 59 | c.mu.Lock() 60 | defer c.mu.Unlock() 61 | 62 | for k, v := range c.members { 63 | if maxAge == 0 || time.Since(v) < maxAge { 64 | id, app := dehash(k) 65 | list = append(list, Member{ 66 | ID: id, 67 | App: app, 68 | LastActive: v, 69 | }) 70 | } 71 | } 72 | return 73 | } 74 | 75 | // App returns a list of all cluster members for the given ARI Application whose LastActive time is no older than the given maxAge. 76 | func (c *Cluster) App(app string, maxAge time.Duration) (list []Member) { 77 | c.mu.Lock() 78 | defer c.mu.Unlock() 79 | 80 | for k, v := range c.members { 81 | i, a := dehash(k) 82 | if app == a && (maxAge == 0 || time.Since(v) < maxAge) { 83 | list = append(list, Member{ 84 | ID: i, 85 | App: a, 86 | LastActive: v, 87 | }) 88 | } 89 | } 90 | return 91 | } 92 | 93 | // Matching returns a list of all cluster members for whom the given proxy Metadata matches 94 | func (c *Cluster) Matching(id, app string, maxAge time.Duration) (list []Member) { 95 | c.mu.Lock() 96 | defer c.mu.Unlock() 97 | 98 | for k, v := range c.members { 99 | if time.Since(v) > maxAge { 100 | continue 101 | } 102 | 103 | i, a := dehash(k) 104 | if id != "" && id != i { 105 | continue 106 | } 107 | if app != "" && app != a { 108 | continue 109 | } 110 | list = append(list, Member{ 111 | ID: i, 112 | App: a, 113 | LastActive: v, 114 | }) 115 | } 116 | return 117 | } 118 | 119 | // Update adds (or updates) a proxy to/in the cluster 120 | func (c *Cluster) Update(id, app string) { 121 | c.mu.Lock() 122 | c.members[hash(id, app)] = time.Now() 123 | c.mu.Unlock() 124 | 125 | // See if it is time to auto-purge 126 | if time.Since(c.lastPurge) > AutoPurgeInterval { 127 | c.Purge(AutoPurgeAge) 128 | } 129 | } 130 | 131 | // Purge removes any proxies in the cluster which are older than the given maxAge. 132 | func (c *Cluster) Purge(maxAge time.Duration) { 133 | c.mu.Lock() 134 | defer c.mu.Unlock() 135 | 136 | c.lastPurge = time.Now() 137 | 138 | var removalKeys []string 139 | 140 | for k, v := range c.members { 141 | if maxAge == 0 || time.Since(v) > maxAge { 142 | removalKeys = append(removalKeys, k) 143 | } 144 | } 145 | 146 | for _, key := range removalKeys { 147 | delete(c.members, key) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /client/cluster/cluster_test.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | 8 | "github.com/CyCoreSystems/ari/v5/rid" 9 | ) 10 | 11 | func TestHash(t *testing.T) { 12 | id := rid.New("") 13 | app := rid.New("") 14 | 15 | testID, testApp := dehash(hash(id, app)) 16 | if id != testID { 17 | t.Error("Asterisk IDs do not match") 18 | } 19 | if app != testApp { 20 | t.Error("ARI Applications do not match") 21 | } 22 | } 23 | 24 | func TestAll(t *testing.T) { 25 | c := New() 26 | c.Update("A1", "TestApp") 27 | c.Update("A2", "TestApp") 28 | 29 | list := c.All(0) 30 | if len(list) != 2 { 31 | t.Errorf("Incorrect number of cluster members: %d != 2", len(list)) 32 | } 33 | 34 | c.Update("B1", "TestApp2") 35 | c.Update("B2", "TestApp2") 36 | 37 | list = c.All(0) 38 | if len(list) != 4 { 39 | t.Errorf("Incorrect number of cluster members: %d != 4", len(list)) 40 | } 41 | } 42 | 43 | func TestApp(t *testing.T) { 44 | c := New() 45 | c.Update("A1", "TestApp") 46 | c.Update("A2", "TestApp") 47 | c.Update("B1", "TestApp2") 48 | c.Update("B2", "TestApp2") 49 | 50 | list := c.App("TestApp", 0) 51 | if len(list) != 2 { 52 | t.Errorf("Incorrect number of cluster members: %d != 2", len(list)) 53 | } 54 | if list[1].App != "TestApp" { 55 | t.Errorf("Incorrect app: %s != TestApp", list[0].App) 56 | } 57 | if !strings.HasPrefix(list[0].ID, "A") { 58 | t.Errorf("Incorrect ID: %s does not begin with A", list[1].ID) 59 | } 60 | 61 | list = c.App("TestApp2", 0) 62 | if len(list) != 2 { 63 | t.Errorf("Incorrect number of cluster members: %d != 2", len(list)) 64 | } 65 | if list[0].App != "TestApp2" { 66 | t.Errorf("Incorrect app: %s != TestApp2", list[0].App) 67 | } 68 | if !strings.HasPrefix(list[1].ID, "B") { 69 | t.Errorf("Incorrect ID: %s does not begin with B", list[1].ID) 70 | } 71 | } 72 | 73 | func TestPurge(t *testing.T) { 74 | c := New() 75 | c.Update("A1", "TestApp") 76 | c.Update("A2", "TestApp") 77 | c.Update("B1", "TestApp2") 78 | c.Update("B2", "TestApp2") 79 | 80 | list := c.All(0) 81 | if len(list) != 4 { 82 | t.Errorf("Incorrect number of cluster members: %d != 4", len(list)) 83 | } 84 | 85 | c.Purge(50 * time.Millisecond) 86 | 87 | list = c.All(0) 88 | if len(list) != 4 { 89 | t.Errorf("Incorrect number of cluster members: %d != 4", len(list)) 90 | } 91 | 92 | time.Sleep(50 * time.Millisecond) 93 | 94 | c.Update("B1", "TestApp2") 95 | 96 | c.Purge(45 * time.Millisecond) 97 | 98 | list = c.All(0) 99 | if len(list) != 1 { 100 | t.Errorf("Incorrect number of cluster members: %d != 1", len(list)) 101 | } 102 | 103 | c.Update("A2", "TestApp") 104 | 105 | list = c.All(0) 106 | if len(list) != 2 { 107 | t.Errorf("Incorrect number of cluster members: %d != 2", len(list)) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type config struct { 9 | c *Client 10 | } 11 | 12 | func (c *config) Get(key *ari.Key) *ari.ConfigHandle { 13 | return ari.NewConfigHandle(key, c) 14 | } 15 | 16 | func (c *config) Data(key *ari.Key) (*ari.ConfigData, error) { 17 | data, err := c.c.dataRequest(&proxy.Request{ 18 | Kind: "AsteriskConfigData", 19 | Key: key, 20 | }) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return data.Config, nil 25 | } 26 | 27 | func (c *config) Update(key *ari.Key, tuples []ari.ConfigTuple) error { 28 | return c.c.commandRequest(&proxy.Request{ 29 | Kind: "AsteriskConfigUpdate", 30 | Key: key, 31 | AsteriskConfig: &proxy.AsteriskConfig{ 32 | Tuples: tuples, 33 | }, 34 | }) 35 | } 36 | 37 | func (c *config) Delete(key *ari.Key) error { 38 | return c.c.commandRequest(&proxy.Request{ 39 | Kind: "AsteriskConfigDelete", 40 | Key: key, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /client/config_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestConfigData(t *testing.T) { 10 | integration.TestConfigData(t, &srv{}) 11 | } 12 | 13 | func TestConfigDelete(t *testing.T) { 14 | integration.TestConfigDelete(t, &srv{}) 15 | } 16 | 17 | func TestConfigUpdate(t *testing.T) { 18 | integration.TestConfigUpdate(t, &srv{}) 19 | } 20 | -------------------------------------------------------------------------------- /client/device.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type deviceState struct { 9 | c *Client 10 | } 11 | 12 | func (ds *deviceState) Get(key *ari.Key) *ari.DeviceStateHandle { 13 | k, err := ds.c.getRequest(&proxy.Request{ 14 | Kind: "DeviceStateGet", 15 | Key: key, 16 | }) 17 | if err != nil { 18 | ds.c.log.Warn("failed to get device state for handle") 19 | return ari.NewDeviceStateHandle(key, ds) 20 | } 21 | return ari.NewDeviceStateHandle(k, ds) 22 | } 23 | 24 | func (ds *deviceState) List(filter *ari.Key) ([]*ari.Key, error) { 25 | return ds.c.listRequest(&proxy.Request{ 26 | Kind: "DeviceStateList", 27 | Key: filter, 28 | }) 29 | } 30 | 31 | func (ds *deviceState) Data(key *ari.Key) (*ari.DeviceStateData, error) { 32 | data, err := ds.c.dataRequest(&proxy.Request{ 33 | Kind: "DeviceStateData", 34 | Key: key, 35 | }) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return data.DeviceState, nil 40 | } 41 | 42 | func (ds *deviceState) Update(key *ari.Key, state string) error { 43 | return ds.c.commandRequest(&proxy.Request{ 44 | Kind: "DeviceStateUpdate", 45 | Key: key, 46 | DeviceStateUpdate: &proxy.DeviceStateUpdate{ 47 | State: state, 48 | }, 49 | }) 50 | } 51 | 52 | func (ds *deviceState) Delete(key *ari.Key) error { 53 | return ds.c.commandRequest(&proxy.Request{ 54 | Kind: "DeviceStateDelete", 55 | Key: key, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /client/device_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestDeviceData(t *testing.T) { 10 | integration.TestDeviceData(t, &srv{}) 11 | } 12 | 13 | func TestDeviceDelete(t *testing.T) { 14 | integration.TestDeviceDelete(t, &srv{}) 15 | } 16 | 17 | func TestDeviceUpdate(t *testing.T) { 18 | integration.TestDeviceUpdate(t, &srv{}) 19 | } 20 | 21 | func TestDeviceList(t *testing.T) { 22 | integration.TestDeviceList(t, &srv{}) 23 | } 24 | -------------------------------------------------------------------------------- /client/doc.go: -------------------------------------------------------------------------------- 1 | // Package client provides an ari.Client implementation for a NATS/RabbitMQ-based ARI proxy cluster 2 | package client 3 | -------------------------------------------------------------------------------- /client/endpoint.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type endpoint struct { 9 | c *Client 10 | } 11 | 12 | func (e *endpoint) Data(key *ari.Key) (*ari.EndpointData, error) { 13 | data, err := e.c.dataRequest(&proxy.Request{ 14 | Kind: "EndpointData", 15 | Key: key, 16 | }) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return data.Endpoint, nil 21 | } 22 | 23 | func (e *endpoint) Get(key *ari.Key) *ari.EndpointHandle { 24 | k, err := e.c.getRequest(&proxy.Request{ 25 | Kind: "EndpointGet", 26 | Key: key, 27 | }) 28 | if err != nil { 29 | e.c.log.Warn("failed to get endpoint for handle", "error", err) 30 | return ari.NewEndpointHandle(key, e) 31 | } 32 | return ari.NewEndpointHandle(k, e) 33 | } 34 | 35 | func (e *endpoint) List(filter *ari.Key) ([]*ari.Key, error) { 36 | return e.c.listRequest(&proxy.Request{ 37 | Kind: "EndpointList", 38 | Key: filter, 39 | }) 40 | } 41 | 42 | func (e *endpoint) ListByTech(tech string, filter *ari.Key) ([]*ari.Key, error) { 43 | return e.c.listRequest(&proxy.Request{ 44 | Kind: "EndpointListByTech", 45 | Key: filter, 46 | EndpointListByTech: &proxy.EndpointListByTech{ 47 | Tech: tech, 48 | }, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /client/endpoint_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestEndpointList(t *testing.T) { 10 | integration.TestEndpointList(t, &srv{}) 11 | } 12 | 13 | func TestEndpointListByTech(t *testing.T) { 14 | integration.TestEndpointListByTech(t, &srv{}) 15 | } 16 | 17 | func TestEndpointData(t *testing.T) { 18 | integration.TestEndpointData(t, &srv{}) 19 | } 20 | -------------------------------------------------------------------------------- /client/errors.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "errors" 4 | 5 | type wrappedError struct { 6 | Message string 7 | Err error 8 | } 9 | 10 | func (err *wrappedError) Cause() error { 11 | return err.Err 12 | } 13 | 14 | func (err *wrappedError) Error() string { 15 | return err.Message + ": " + err.Err.Error() 16 | } 17 | 18 | // remote error, used wrap the error response before sending 19 | 20 | type codedError struct { 21 | err error 22 | code int 23 | } 24 | 25 | func (err *codedError) Error() string { 26 | return err.err.Error() 27 | } 28 | 29 | func (err *codedError) Code() int { 30 | return err.code 31 | } 32 | 33 | type causer interface { 34 | Cause() error 35 | } 36 | 37 | type coded interface { 38 | Code() int 39 | } 40 | 41 | // ErrorToMap converts an error type to a key-value map 42 | func ErrorToMap(err error, parent string) map[string]interface{} { 43 | data := make(map[string]interface{}) 44 | if parent == err.Error() { 45 | // NOTE: this is done because of how errors.Wrap works, internally, 46 | // to build a stacktrace. We end up with duplicate 47 | // entries in the tree of errors. 48 | if c, ok := err.(causer); ok { 49 | return ErrorToMap(c.Cause(), parent) 50 | } 51 | } 52 | data["message"] = err.Error() 53 | if c, ok := err.(coded); ok { 54 | data["code"] = c.Code() 55 | } 56 | if c, ok := err.(causer); ok { 57 | data["cause"] = ErrorToMap(c.Cause(), data["message"].(string)) 58 | } 59 | return data 60 | } 61 | 62 | // MapToError converts a JSON parsed map to an error type 63 | func MapToError(i map[string]interface{}) error { 64 | msg, _ := i["message"].(string) 65 | code, codeOK := i["code"].(int) 66 | cause, causeOK := i["cause"].(map[string]interface{}) 67 | 68 | err := errors.New(msg) 69 | 70 | if codeOK { 71 | err = &codedError{err, code} 72 | } 73 | 74 | if causeOK { 75 | causeError := MapToError(cause) 76 | l := len(msg) - len(causeError.Error()) 77 | msg = msg[:l-2] 78 | err = &wrappedError{msg, causeError} 79 | } 80 | 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /client/listen.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/CyCoreSystems/ari-proxy/v5/messagebus" 8 | "github.com/CyCoreSystems/ari/v5" 9 | "github.com/rotisserie/eris" 10 | ) 11 | 12 | // ListenQueue is the queue group to use for distributing StasisStart events to Listeners. 13 | var ListenQueue = "ARIProxyStasisStartDistributorQueue" 14 | 15 | // Listen listens for StasisStart events, filtered by the given key. Any 16 | // matching events will be sent down the returned StasisStart channel. The 17 | // context which is passed to Listen can be used to stop the Listen execution. 18 | // 19 | // Importantly, the StasisStart events are listened in a NATS/RabbitMQ Queue, which 20 | // means that this may be used to deliver new calls to only a single handler 21 | // out of a set of 1 or more handlers in a cluster. 22 | func Listen(ctx context.Context, ac ari.Client, h func(*ari.ChannelHandle, *ari.StasisStart)) error { 23 | c, ok := ac.(*Client) 24 | if !ok { 25 | return eris.New("ARI Client must be a proxy client") 26 | } 27 | 28 | subj := fmt.Sprintf( 29 | "%sevent.%s.%s", 30 | c.core.prefix, 31 | c.ApplicationName(), 32 | c.mbus.GetWildcardString(messagebus.WildcardZeroOrMoreWords), 33 | ) 34 | 35 | c.log.Debug("listening for events", "subject", subj) 36 | sub, err := c.mbus.SubscribeEvent(subj, ListenQueue, listenProcessor(ac, h)) 37 | if err != nil { 38 | return eris.Wrap(err, "failed to subscribe to events") 39 | } 40 | defer sub.Unsubscribe() // nolint: errcheck 41 | 42 | <-ctx.Done() 43 | 44 | return nil 45 | } 46 | 47 | func listenProcessor(ac ari.Client, h func(*ari.ChannelHandle, *ari.StasisStart)) func([]byte) { 48 | return func(data []byte) { 49 | e, err := ari.DecodeEvent(data) 50 | if err != nil { 51 | Logger.Error("failed to decode event", "error", err) 52 | return 53 | } 54 | 55 | Logger.Debug("received event", e.GetType()) 56 | if e.GetType() != "StasisStart" { 57 | return 58 | } 59 | 60 | v, ok := e.(*ari.StasisStart) 61 | if !ok { 62 | Logger.Error("failed to type-assert StasisStart event") 63 | return 64 | } 65 | 66 | h(ari.NewChannelHandle(v.Key(ari.ChannelKey, v.Channel.ID), ac.Channel(), nil), v) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/liveRecording.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type liveRecording struct { 9 | c *Client 10 | } 11 | 12 | func (l *liveRecording) Get(key *ari.Key) *ari.LiveRecordingHandle { 13 | k, err := l.c.getRequest(&proxy.Request{ 14 | Kind: "RecordingLiveGet", 15 | Key: key, 16 | }) 17 | if err != nil { 18 | l.c.log.Warn("failed to get liveRecording for handle", "error", err) 19 | return ari.NewLiveRecordingHandle(key, l, nil) 20 | } 21 | return ari.NewLiveRecordingHandle(k, l, nil) 22 | } 23 | 24 | func (l *liveRecording) Data(key *ari.Key) (*ari.LiveRecordingData, error) { 25 | data, err := l.c.dataRequest(&proxy.Request{ 26 | Kind: "RecordingLiveData", 27 | Key: key, 28 | }) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return data.LiveRecording, nil 33 | } 34 | 35 | func (l *liveRecording) Stop(key *ari.Key) error { 36 | return l.c.commandRequest(&proxy.Request{ 37 | Kind: "RecordingLiveStop", 38 | Key: key, 39 | }) 40 | } 41 | 42 | func (l *liveRecording) Pause(key *ari.Key) error { 43 | return l.c.commandRequest(&proxy.Request{ 44 | Kind: "RecordingLivePause", 45 | Key: key, 46 | }) 47 | } 48 | 49 | func (l *liveRecording) Resume(key *ari.Key) error { 50 | return l.c.commandRequest(&proxy.Request{ 51 | Kind: "RecordingLiveResume", 52 | Key: key, 53 | }) 54 | } 55 | 56 | func (l *liveRecording) Mute(key *ari.Key) error { 57 | return l.c.commandRequest(&proxy.Request{ 58 | Kind: "RecordingLiveMute", 59 | Key: key, 60 | }) 61 | } 62 | 63 | func (l *liveRecording) Unmute(key *ari.Key) error { 64 | return l.c.commandRequest(&proxy.Request{ 65 | Kind: "RecordingLiveUnmute", 66 | Key: key, 67 | }) 68 | } 69 | 70 | func (l *liveRecording) Scrap(key *ari.Key) error { 71 | return l.c.commandRequest(&proxy.Request{ 72 | Kind: "RecordingLiveScrap", 73 | Key: key, 74 | }) 75 | } 76 | 77 | func (l *liveRecording) Stored(key *ari.Key) *ari.StoredRecordingHandle { 78 | return ari.NewStoredRecordingHandle(key.New(ari.StoredRecordingKey, key.ID), l.c.StoredRecording(), nil) 79 | } 80 | 81 | func (l *liveRecording) Subscribe(key *ari.Key, n ...string) ari.Subscription { 82 | err := l.c.commandRequest(&proxy.Request{ 83 | Kind: "RecordingLiveSubscribe", 84 | Key: key, 85 | }) 86 | if err != nil { 87 | l.c.log.Warn("failed to call liveRecording Subscribe") 88 | if key.Dialog != "" { 89 | l.c.log.Error("dialog present; failing") 90 | return nil 91 | } 92 | } 93 | 94 | return l.c.Bus().Subscribe(key, n...) 95 | } 96 | -------------------------------------------------------------------------------- /client/liveRecording_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestLiveRecordingData(t *testing.T) { 10 | integration.TestLiveRecordingData(t, &srv{}) 11 | } 12 | 13 | /* 14 | func TestLiveRecordingDelete(t *testing.T) { 15 | integration.TestLiveRecordingDelete(t, &srv{}) 16 | } 17 | */ 18 | 19 | func TestLiveRecordingMute(t *testing.T) { 20 | integration.TestLiveRecordingMute(t, &srv{}) 21 | } 22 | 23 | func TestLiveRecordingUnmute(t *testing.T) { 24 | integration.TestLiveRecordingUnmute(t, &srv{}) 25 | } 26 | 27 | func TestLiveRecordingPause(t *testing.T) { 28 | integration.TestLiveRecordingPause(t, &srv{}) 29 | } 30 | 31 | func TestLiveRecordingStop(t *testing.T) { 32 | integration.TestLiveRecordingStop(t, &srv{}) 33 | } 34 | 35 | func TestLiveRecordingResume(t *testing.T) { 36 | integration.TestLiveRecordingResume(t, &srv{}) 37 | } 38 | 39 | func TestLiveRecordingScrap(t *testing.T) { 40 | integration.TestLiveRecordingScrap(t, &srv{}) 41 | } 42 | -------------------------------------------------------------------------------- /client/logger.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "github.com/inconshreveable/log15" 4 | 5 | // Logger defaults to a discard handler (null output). 6 | // If you wish to enable logging, you can set your own 7 | // handler like so: 8 | // ari.Logger.SetHandler(log15.StderrHandler) 9 | // 10 | var Logger = log15.New() 11 | 12 | func init() { 13 | // Null logger, by default 14 | Logger.SetHandler(log15.DiscardHandler()) 15 | } 16 | -------------------------------------------------------------------------------- /client/logging.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type logging struct { 9 | c *Client 10 | } 11 | 12 | func (l *logging) Create(key *ari.Key, levels string) (*ari.LogHandle, error) { 13 | k, err := l.c.createRequest(&proxy.Request{ 14 | Kind: "AsteriskLoggingCreate", 15 | Key: key, 16 | AsteriskLoggingChannel: &proxy.AsteriskLoggingChannel{ 17 | Levels: levels, 18 | }, 19 | }) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return ari.NewLogHandle(k, l), nil 24 | } 25 | 26 | func (l *logging) Data(key *ari.Key) (*ari.LogData, error) { 27 | data, err := l.c.dataRequest(&proxy.Request{ 28 | Kind: "AsteriskLoggingData", 29 | Key: key, 30 | }) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return data.Log, nil 35 | } 36 | 37 | func (l *logging) Get(key *ari.Key) *ari.LogHandle { 38 | k, err := l.c.getRequest(&proxy.Request{ 39 | Kind: "AsteriskLoggingGet", 40 | Key: key, 41 | }) 42 | if err != nil { 43 | l.c.log.Warn("failed to get logging key for handle", "error", err) 44 | return ari.NewLogHandle(key, l) 45 | } 46 | return ari.NewLogHandle(k, l) 47 | } 48 | 49 | func (l *logging) List(filter *ari.Key) ([]*ari.Key, error) { 50 | return l.c.listRequest(&proxy.Request{ 51 | Kind: "AsteriskLoggingList", 52 | Key: filter, 53 | }) 54 | } 55 | 56 | func (l *logging) Rotate(key *ari.Key) error { 57 | return l.c.commandRequest(&proxy.Request{ 58 | Kind: "AsteriskLoggingRotate", 59 | Key: key, 60 | }) 61 | } 62 | 63 | func (l *logging) Delete(key *ari.Key) error { 64 | return l.c.commandRequest(&proxy.Request{ 65 | Kind: "AsteriskLoggingDelete", 66 | Key: key, 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /client/logging_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestLoggingList(t *testing.T) { 10 | integration.TestLoggingList(t, &srv{}) 11 | } 12 | 13 | func TestLoggingCreate(t *testing.T) { 14 | integration.TestLoggingCreate(t, &srv{}) 15 | } 16 | 17 | func TestLoggingRotate(t *testing.T) { 18 | integration.TestLoggingRotate(t, &srv{}) 19 | } 20 | 21 | func TestLoggingDelete(t *testing.T) { 22 | integration.TestLoggingDelete(t, &srv{}) 23 | } 24 | -------------------------------------------------------------------------------- /client/mailbox.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type mailbox struct { 9 | c *Client 10 | } 11 | 12 | func (m *mailbox) Get(key *ari.Key) *ari.MailboxHandle { 13 | k, err := m.c.getRequest(&proxy.Request{ 14 | Kind: "MailboxGet", 15 | Key: key, 16 | }) 17 | if err != nil { 18 | m.c.log.Warn("failed to get bridge for handle", "error", err) 19 | return ari.NewMailboxHandle(key, m) 20 | } 21 | return ari.NewMailboxHandle(k, m) 22 | } 23 | 24 | func (m *mailbox) List(filter *ari.Key) ([]*ari.Key, error) { 25 | return m.c.listRequest(&proxy.Request{ 26 | Kind: "MailboxList", 27 | Key: filter, 28 | }) 29 | } 30 | 31 | func (m *mailbox) Data(key *ari.Key) (*ari.MailboxData, error) { 32 | data, err := m.c.dataRequest(&proxy.Request{ 33 | Kind: "MailboxData", 34 | Key: key, 35 | }) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return data.Mailbox, nil 40 | } 41 | 42 | func (m *mailbox) Update(key *ari.Key, oldMessages int, newMessages int) error { 43 | return m.c.commandRequest(&proxy.Request{ 44 | Kind: "MailboxUpdate", 45 | Key: key, 46 | MailboxUpdate: &proxy.MailboxUpdate{ 47 | New: newMessages, 48 | Old: oldMessages, 49 | }, 50 | }) 51 | } 52 | 53 | func (m *mailbox) Delete(key *ari.Key) error { 54 | return m.c.commandRequest(&proxy.Request{ 55 | Kind: "MailboxDelete", 56 | Key: key, 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /client/mailbox_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestMailboxList(t *testing.T) { 10 | integration.TestMailboxList(t, &srv{}) 11 | } 12 | 13 | func TestMailboxUpdate(t *testing.T) { 14 | integration.TestMailboxUpdate(t, &srv{}) 15 | } 16 | 17 | func TestMailboxDelete(t *testing.T) { 18 | integration.TestMailboxDelete(t, &srv{}) 19 | } 20 | 21 | func TestMailboxData(t *testing.T) { 22 | integration.TestMailboxData(t, &srv{}) 23 | } 24 | -------------------------------------------------------------------------------- /client/modules.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type modules struct { 9 | c *Client 10 | } 11 | 12 | func (m *modules) Data(key *ari.Key) (*ari.ModuleData, error) { 13 | data, err := m.c.dataRequest(&proxy.Request{ 14 | Kind: "AsteriskModuleData", 15 | Key: key, 16 | }) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return data.Module, nil 21 | } 22 | 23 | func (m *modules) Get(key *ari.Key) *ari.ModuleHandle { 24 | k, err := m.c.getRequest(&proxy.Request{ 25 | Kind: "AsteriskModuleGet", 26 | Key: key, 27 | }) 28 | if err != nil { 29 | m.c.log.Warn("failed to get module for handle", "error", err) 30 | return ari.NewModuleHandle(key, m) 31 | } 32 | return ari.NewModuleHandle(k, m) 33 | } 34 | 35 | func (m *modules) List(filter *ari.Key) ([]*ari.Key, error) { 36 | return m.c.listRequest(&proxy.Request{ 37 | Kind: "AsteriskModuleList", 38 | Key: filter, 39 | }) 40 | } 41 | 42 | func (m *modules) Load(key *ari.Key) error { 43 | return m.c.commandRequest(&proxy.Request{ 44 | Kind: "AsteriskModuleLoad", 45 | Key: key, 46 | }) 47 | } 48 | 49 | func (m *modules) Reload(key *ari.Key) error { 50 | return m.c.commandRequest(&proxy.Request{ 51 | Kind: "AsteriskModuleReload", 52 | Key: key, 53 | }) 54 | } 55 | 56 | func (m *modules) Unload(key *ari.Key) error { 57 | return m.c.commandRequest(&proxy.Request{ 58 | Kind: "AsteriskModuleUnload", 59 | Key: key, 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /client/modules_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestModulesData(t *testing.T) { 10 | integration.TestModulesData(t, &srv{}) 11 | } 12 | 13 | func TestModulesLoad(t *testing.T) { 14 | integration.TestModulesLoad(t, &srv{}) 15 | } 16 | 17 | func TestModulesReload(t *testing.T) { 18 | integration.TestModulesReload(t, &srv{}) 19 | } 20 | 21 | func TestModulesUnload(t *testing.T) { 22 | integration.TestModulesUnload(t, &srv{}) 23 | } 24 | 25 | func TestModulesList(t *testing.T) { 26 | integration.TestModulesList(t, &srv{}) 27 | } 28 | -------------------------------------------------------------------------------- /client/playback.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type playback struct { 9 | c *Client 10 | } 11 | 12 | func (p *playback) Get(key *ari.Key) *ari.PlaybackHandle { 13 | k, err := p.c.getRequest(&proxy.Request{ 14 | Kind: "PlaybackGet", 15 | Key: key, 16 | }) 17 | if err != nil { 18 | p.c.log.Warn("failed to get playback for handle", "error", err) 19 | return ari.NewPlaybackHandle(key, p, nil) 20 | } 21 | return ari.NewPlaybackHandle(k, p, nil) 22 | } 23 | 24 | func (p *playback) Data(key *ari.Key) (*ari.PlaybackData, error) { 25 | data, err := p.c.dataRequest(&proxy.Request{ 26 | Kind: "PlaybackData", 27 | Key: key, 28 | }) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return data.Playback, nil 33 | } 34 | 35 | func (p *playback) Control(key *ari.Key, op string) error { 36 | return p.c.commandRequest(&proxy.Request{ 37 | Kind: "PlaybackControl", 38 | Key: key, 39 | PlaybackControl: &proxy.PlaybackControl{ 40 | Command: op, 41 | }, 42 | }) 43 | } 44 | 45 | func (p *playback) Stop(key *ari.Key) error { 46 | return p.c.commandRequest(&proxy.Request{ 47 | Kind: "PlaybackStop", 48 | Key: key, 49 | }) 50 | } 51 | 52 | func (p *playback) Subscribe(key *ari.Key, n ...string) ari.Subscription { 53 | err := p.c.commandRequest(&proxy.Request{ 54 | Kind: "PlaybackSubscribe", 55 | Key: key, 56 | }) 57 | if err != nil { 58 | p.c.log.Warn("failed to call bridge subscribe", "error", err) 59 | if key.Dialog != "" { 60 | p.c.log.Error("dialog present; failing", "error", err) 61 | return nil 62 | } 63 | } 64 | return p.c.Bus().Subscribe(key, n...) 65 | } 66 | -------------------------------------------------------------------------------- /client/playback_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestPlaybackData(t *testing.T) { 10 | integration.TestPlaybackData(t, &srv{}) 11 | } 12 | 13 | func TestPlaybackControl(t *testing.T) { 14 | integration.TestPlaybackControl(t, &srv{}) 15 | } 16 | 17 | func TestPlaybackStop(t *testing.T) { 18 | integration.TestPlaybackStop(t, &srv{}) 19 | } 20 | -------------------------------------------------------------------------------- /client/request.go: -------------------------------------------------------------------------------- 1 | package client 2 | -------------------------------------------------------------------------------- /client/sound.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type sound struct { 9 | c *Client 10 | } 11 | 12 | func (s *sound) List(filters map[string]string, keyFilter *ari.Key) ([]*ari.Key, error) { 13 | return s.c.listRequest(&proxy.Request{ 14 | Kind: "SoundList", 15 | Key: keyFilter, 16 | SoundList: &proxy.SoundList{ 17 | Filters: filters, 18 | }, 19 | }) 20 | } 21 | 22 | func (s *sound) Data(key *ari.Key) (*ari.SoundData, error) { 23 | data, err := s.c.dataRequest(&proxy.Request{ 24 | Kind: "SoundData", 25 | Key: key, 26 | }) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return data.Sound, nil 31 | } 32 | -------------------------------------------------------------------------------- /client/sound_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestSoundData(t *testing.T) { 10 | integration.TestSoundData(t, &srv{}) 11 | } 12 | 13 | func TestSoundList(t *testing.T) { 14 | integration.TestSoundList(t, &srv{}) 15 | } 16 | -------------------------------------------------------------------------------- /client/storedRecording.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 5 | "github.com/CyCoreSystems/ari/v5" 6 | ) 7 | 8 | type storedRecording struct { 9 | c *Client 10 | } 11 | 12 | func (s *storedRecording) List(filter *ari.Key) ([]*ari.Key, error) { 13 | return s.c.listRequest(&proxy.Request{ 14 | Kind: "RecordingStoredList", 15 | Key: filter, 16 | }) 17 | } 18 | 19 | func (s *storedRecording) Get(key *ari.Key) *ari.StoredRecordingHandle { 20 | k, err := s.c.getRequest(&proxy.Request{ 21 | Kind: "RecordingStoredGet", 22 | Key: key, 23 | }) 24 | if err != nil { 25 | s.c.log.Warn("failed to get stored recording for handle", "error", err) 26 | return ari.NewStoredRecordingHandle(key, s, nil) 27 | } 28 | return ari.NewStoredRecordingHandle(k, s, nil) 29 | } 30 | 31 | func (s *storedRecording) Data(key *ari.Key) (*ari.StoredRecordingData, error) { 32 | data, err := s.c.dataRequest(&proxy.Request{ 33 | Kind: "RecordingStoredData", 34 | Key: key, 35 | }) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return data.StoredRecording, nil 40 | } 41 | 42 | func (s *storedRecording) Copy(key *ari.Key, dest string) (*ari.StoredRecordingHandle, error) { 43 | h := ari.NewStoredRecordingHandle(key.New(ari.StoredRecordingKey, dest), s, nil) 44 | 45 | err := s.c.commandRequest(&proxy.Request{ 46 | Kind: "RecordingStoredCopy", 47 | Key: key, 48 | RecordingStoredCopy: &proxy.RecordingStoredCopy{ 49 | Destination: dest, 50 | }, 51 | }) 52 | 53 | // NOTE: Always return the handle, even when we have an error 54 | return h, err 55 | } 56 | 57 | func (s *storedRecording) Delete(key *ari.Key) error { 58 | return s.c.commandRequest(&proxy.Request{ 59 | Kind: "RecordingStoredDelete", 60 | Key: key, 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /client/subscription.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | /* 4 | type natsSubscription struct { 5 | m ari.Matcher 6 | closeChan chan struct{} 7 | closed bool 8 | events chan ari.Event 9 | } 10 | 11 | func newSubscription(m ari.Matcher) *natsSubscription { 12 | return &natsSubscription{ 13 | m: m, 14 | closeChan: make(chan struct{}), 15 | events: make(chan ari.Event, 10), 16 | } 17 | } 18 | 19 | func (ns *natsSubscription) Start(s ari.Subscriber, n ...string) { 20 | 21 | sub := s.Subscribe(n...) 22 | 23 | go func() { 24 | defer sub.Cancel() 25 | for { 26 | select { 27 | case <-ns.closeChan: 28 | return 29 | case evt, ok := <-sub.Events(): 30 | if !ok { 31 | ns.Cancel() 32 | continue 33 | } 34 | if ns.m == nil { 35 | ns.events <- evt 36 | } else if ns.m.Match(evt) { 37 | ns.events <- evt 38 | } 39 | } 40 | } 41 | }() 42 | } 43 | 44 | func (ns *natsSubscription) Events() chan ari.Event { 45 | return ns.events 46 | } 47 | 48 | func (ns *natsSubscription) Cancel() { 49 | if !ns.closed && ns.closeChan != nil { 50 | ns.closed = true 51 | close(ns.closeChan) 52 | } 53 | } 54 | */ 55 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/CyCoreSystems/ari-proxy/v5/server" 10 | "github.com/CyCoreSystems/ari/v5/client/native" 11 | 12 | "github.com/inconshreveable/log15" 13 | "github.com/nats-io/nats.go" 14 | "github.com/spf13/cobra" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | // Log is the package logger 19 | var Log log15.Logger 20 | 21 | // RootCmd is the Cobra root command descriptor 22 | var RootCmd = &cobra.Command{ 23 | Use: "ari-proxy", 24 | Short: "Proxy for the Asterisk REST interface.", 25 | Long: `ari-proxy is a proxy for working the Asterisk daemon over NATS/RabbitMQ. 26 | ARI commands are exposed over NATS/RabbitMQ for operation.`, 27 | RunE: func(cmd *cobra.Command, args []string) error { 28 | ctx, cancel := context.WithCancel(context.Background()) 29 | defer cancel() 30 | 31 | if ok, _ := cmd.PersistentFlags().GetBool("version"); ok { // nolint: gas 32 | fmt.Println(version) 33 | os.Exit(0) 34 | } 35 | 36 | handler := log15.StdoutHandler 37 | if viper.GetBool("verbose") { 38 | Log.Info("verbose logging enabled") 39 | handler = log15.LvlFilterHandler(log15.LvlDebug, handler) 40 | } else { 41 | handler = log15.LvlFilterHandler(log15.LvlInfo, handler) 42 | } 43 | Log.SetHandler(handler) 44 | 45 | native.Logger.SetHandler(handler) 46 | 47 | return runServer(ctx, Log) 48 | }, 49 | } 50 | 51 | var cfgFile string 52 | 53 | func init() { 54 | Log = log15.New() 55 | 56 | cobra.OnInitialize(readConfig) 57 | 58 | p := RootCmd.PersistentFlags() 59 | 60 | p.BoolP("version", "V", false, "Print version information and exit") 61 | 62 | p.StringVar(&cfgFile, "config", "", "config file (default is $HOME/.ari-proxy.yaml)") 63 | p.BoolP("verbose", "v", false, "Enable verbose logging") 64 | 65 | p.String("nats.url", nats.DefaultURL, "URL for connecting to the NATS cluster") //backward compatibility 66 | p.String("messagebus.url", nats.DefaultURL, "URL for connecting to the Message Bus cluster") 67 | p.String("ari.application", "", "ARI Stasis Application") 68 | p.String("ari.username", "", "Username for connecting to ARI") 69 | p.String("ari.password", "", "Password for connecting to ARI") 70 | p.String("ari.http_url", "http://localhost:8088/ari", "HTTP Base URL for connecting to ARI") 71 | p.String("ari.websocket_url", "ws://localhost:8088/ari/events", "Websocket URL for connecting to ARI") 72 | 73 | for _, n := range []string{"verbose", "nats.url", "messagebus.url", "ari.application", "ari.username", "ari.password", "ari.http_url", "ari.websocket_url"} { 74 | err := viper.BindPFlag(n, p.Lookup(n)) 75 | if err != nil { 76 | panic("failed to bind flag " + n) 77 | } 78 | } 79 | } 80 | 81 | // readConfig reads in config file and ENV variables if set. 82 | func readConfig() { 83 | viper.SetConfigName(".ari-proxy") // name of config file (without extension) 84 | viper.AddConfigPath("$HOME") // adding home directory as first search path 85 | 86 | if cfgFile != "" { // enable ability to specify config file via flag 87 | viper.SetConfigFile(cfgFile) 88 | } 89 | 90 | // Load from the environment, as well 91 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 92 | viper.AutomaticEnv() // read in environment variables that match 93 | 94 | // If a config file is found, read it in. 95 | err := viper.ReadInConfig() 96 | if err == nil { 97 | Log.Debug("read configuration from file") 98 | } 99 | } 100 | 101 | func runServer(ctx context.Context, log log15.Logger) error { 102 | messagebusURL := viper.GetString("messagebus.url") 103 | if messagebusURL == "" { 104 | messagebusURL = viper.GetString("nats.url") //backward compatibility 105 | } 106 | if os.Getenv("NATS_SERVICE_HOST") != "" { 107 | messagebusURL = "nats://" + os.Getenv("NATS_SERVICE_HOST") + ":" + os.Getenv("NATS_SERVICE_PORT_CLIENT") 108 | } 109 | 110 | srv := server.New() 111 | srv.Log = log 112 | 113 | log.Info("starting ari-proxy server", "version", version) 114 | return srv.Listen(ctx, &native.Options{ 115 | Application: viper.GetString("ari.application"), 116 | Username: viper.GetString("ari.username"), 117 | Password: viper.GetString("ari.password"), 118 | URL: viper.GetString("ari.http_url"), 119 | WebsocketURL: viper.GetString("ari.websocket_url"), 120 | }, messagebusURL) 121 | } 122 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/CyCoreSystems/ari-proxy/v5 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/CyCoreSystems/ari/v5 v5.3.1 7 | github.com/go-stack/stack v1.8.1 // indirect 8 | github.com/inconshreveable/log15 v2.16.0+incompatible 9 | github.com/nats-io/nats.go v1.28.0 10 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 11 | github.com/rabbitmq/amqp091-go v1.8.1 12 | github.com/rotisserie/eris v0.5.4 13 | github.com/spf13/afero v1.9.5 // indirect 14 | github.com/spf13/cobra v1.7.0 15 | github.com/spf13/viper v1.16.0 16 | github.com/stretchr/testify v1.8.4 17 | github.com/subosito/gotenv v1.6.0 // indirect 18 | golang.org/x/crypto v0.12.0 // indirect 19 | golang.org/x/net v0.14.0 // indirect 20 | golang.org/x/sys v0.12.0 // indirect 21 | gopkg.in/ini.v1 v1.67.0 // indirect 22 | ) 23 | 24 | require ( 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/fsnotify/fsnotify v1.6.0 // indirect 27 | github.com/gogo/protobuf v1.3.2 // indirect 28 | github.com/hashicorp/hcl v1.0.0 // indirect 29 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 30 | github.com/klauspost/compress v1.16.7 // indirect 31 | github.com/magiconair/properties v1.8.7 // indirect 32 | github.com/mattn/go-colorable v0.1.13 // indirect 33 | github.com/mattn/go-isatty v0.0.19 // indirect 34 | github.com/mitchellh/mapstructure v1.5.0 // indirect 35 | github.com/nats-io/nats-server/v2 v2.9.3 // indirect 36 | github.com/nats-io/nkeys v0.4.4 // indirect 37 | github.com/nats-io/nuid v1.0.1 // indirect 38 | github.com/oklog/ulid v1.3.1 // indirect 39 | github.com/pmezard/go-difflib v1.0.0 // indirect 40 | github.com/spf13/cast v1.5.1 // indirect 41 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 42 | github.com/spf13/pflag v1.0.5 // indirect 43 | github.com/stretchr/objx v0.5.0 // indirect 44 | golang.org/x/term v0.12.0 // indirect 45 | golang.org/x/text v0.13.0 // indirect 46 | gopkg.in/yaml.v3 v3.0.1 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /internal/integration/application.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari/v5" 7 | "github.com/rotisserie/eris" 8 | tmock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | func TestApplicationList(t *testing.T, s Server) { 12 | runTest("emptyList", t, s, func(t *testing.T, m *mock, cl ari.Client) { 13 | m.Application.On("List", (*ari.Key)(nil)).Return([]*ari.Key{}, nil) 14 | 15 | if _, err := cl.Application().List(nil); err != nil { 16 | t.Errorf("Unexpected error in remote List call: %v", err) 17 | } 18 | }) 19 | 20 | runTest("nonEmptyList", t, s, func(t *testing.T, m *mock, cl ari.Client) { 21 | h1 := ari.NewKey(ari.ApplicationKey, "1") 22 | h2 := ari.NewKey(ari.ApplicationKey, "2") 23 | m.Application.On("List", (*ari.Key)(nil)).Return([]*ari.Key{h1, h2}, nil) 24 | 25 | items, err := cl.Application().List(nil) 26 | if err != nil { 27 | t.Errorf("Unexpected error in remote List call: %v", err) 28 | } 29 | if len(items) != 2 { 30 | t.Errorf("Expected items to be length 2, got %d", len(items)) 31 | } else { 32 | if items[0].ID != "1" { 33 | t.Errorf("Expected item 0 to be '1', got %s", items[0].ID) 34 | } 35 | if items[1].ID != "2" { 36 | t.Errorf("Expected item 1 to be '2', got %s", items[1].ID) 37 | } 38 | } 39 | }) 40 | } 41 | 42 | func TestApplicationData(t *testing.T, s Server) { 43 | key := ari.NewKey(ari.ApplicationKey, "1") 44 | runTest("simple", t, s, func(t *testing.T, m *mock, cl ari.Client) { 45 | ad := &ari.ApplicationData{} 46 | ad.Name = "app1" 47 | 48 | m.Application.On("Data", tmock.Anything).Return(ad, nil) 49 | 50 | res, err := cl.Application().Data(key) 51 | if err != nil { 52 | t.Errorf("Unexpected error in remote Data call: %v", err) 53 | } 54 | if res == nil || res.Name != ad.Name { 55 | t.Errorf("Expected application data name %s, got %s", ad, res) 56 | } 57 | 58 | m.Shutdown() 59 | m.Application.AssertCalled(t, "Data", key) 60 | }) 61 | 62 | runTest("error", t, s, func(t *testing.T, m *mock, cl ari.Client) { 63 | expected := eris.New("unknown error") 64 | 65 | m.Application.On("Data", key).Return(nil, expected) 66 | 67 | res, err := cl.Application().Data(key) 68 | if err == nil || eris.Cause(err).Error() != expected.Error() { 69 | t.Errorf("Expected error '%v', got '%v'", expected, err) 70 | } 71 | if res != nil { 72 | t.Errorf("Expected application data result to be empty, got %s", res) 73 | } 74 | 75 | m.Shutdown() 76 | 77 | m.Application.AssertCalled(t, "Data", key) 78 | }) 79 | } 80 | 81 | func TestApplicationSubscribe(t *testing.T, s Server) { 82 | key := ari.NewKey(ari.ApplicationKey, "1") 83 | 84 | runTest("simple", t, s, func(t *testing.T, m *mock, cl ari.Client) { 85 | m.Application.On("Subscribe", key, "2").Return(nil) 86 | 87 | if err := cl.Application().Subscribe(key, "2"); err != nil { 88 | t.Errorf("Unexpected error in remote Subscribe call: %v", err) 89 | } 90 | 91 | m.Shutdown() 92 | 93 | m.Application.AssertCalled(t, "Subscribe", key, "2") 94 | }) 95 | 96 | runTest("error", t, s, func(t *testing.T, m *mock, cl ari.Client) { 97 | expected := eris.New("unknown error") 98 | 99 | m.Application.On("Subscribe", key, "2").Return(expected) 100 | 101 | if err := cl.Application().Subscribe(key, "2"); err == nil || eris.Cause(err).Error() != expected.Error() { 102 | t.Errorf("Expected error '%v', got '%v'", expected, err) 103 | } 104 | 105 | m.Shutdown() 106 | 107 | m.Application.AssertCalled(t, "Subscribe", key, "2") 108 | }) 109 | } 110 | 111 | func TestApplicationUnsubscribe(t *testing.T, s Server) { 112 | key := ari.NewKey(ari.ApplicationKey, "1") 113 | 114 | runTest("simple", t, s, func(t *testing.T, m *mock, cl ari.Client) { 115 | m.Application.On("Unsubscribe", key, "2").Return(nil) 116 | 117 | if err := cl.Application().Unsubscribe(key, "2"); err != nil { 118 | t.Errorf("Unexpected error in remote Unsubscribe call: %T", err) 119 | } 120 | 121 | m.Shutdown() 122 | 123 | m.Application.AssertCalled(t, "Unsubscribe", key, "2") 124 | }) 125 | 126 | runTest("error", t, s, func(t *testing.T, m *mock, cl ari.Client) { 127 | expected := eris.New("unknown error") 128 | 129 | m.Application.On("Unsubscribe", key, "2").Return(expected) 130 | 131 | if err := cl.Application().Unsubscribe(key, "2"); err == nil || eris.Cause(err).Error() != expected.Error() { 132 | t.Errorf("Expected error '%v', got '%v'", expected, err) 133 | } 134 | 135 | m.Application.AssertCalled(t, "Unsubscribe", key, "2") 136 | }) 137 | } 138 | 139 | func TestApplicationGet(t *testing.T, s Server) { 140 | key := ari.NewKey(ari.ApplicationKey, "1") 141 | 142 | runTest("simple", t, s, func(t *testing.T, m *mock, cl ari.Client) { 143 | ad := &ari.ApplicationData{} 144 | ad.Name = "app1" 145 | 146 | m.Application.On("Data", tmock.Anything).Return(ad, nil) 147 | 148 | if h := cl.Application().Get(key); h == nil { 149 | t.Errorf("Unexpected nil-handle") 150 | } 151 | 152 | m.Shutdown() 153 | 154 | m.Application.AssertCalled(t, "Data", key) 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /internal/integration/asterisk.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari/v5" 7 | "github.com/CyCoreSystems/ari/v5/client/arimocks" 8 | "github.com/rotisserie/eris" 9 | ) 10 | 11 | func TestAsteriskInfo(t *testing.T, s Server) { 12 | runTest("noFilter", t, s, func(t *testing.T, m *mock, cl ari.Client) { 13 | var ai ari.AsteriskInfo 14 | ai.SystemInfo.EntityID = "1" 15 | 16 | m.Asterisk.On("Info", ari.NodeKey("asdf", "1")).Return(&ai, nil) 17 | 18 | ret, err := cl.Asterisk().Info(ari.NodeKey("asdf", "1")) 19 | if err != nil { 20 | t.Errorf("Unexpected error in remote Info call: %v", err) 21 | } 22 | if ret == nil || ret.SystemInfo.EntityID != ai.SystemInfo.EntityID { 23 | t.Errorf("Expected asterisk info %v, got %v", ai, ret) 24 | } 25 | 26 | m.Shutdown() 27 | 28 | m.Asterisk.AssertCalled(t, "Info", ari.NodeKey("asdf", "1")) 29 | }) 30 | 31 | runTest("noFilterError", t, s, func(t *testing.T, m *mock, cl ari.Client) { 32 | expected := eris.New("unknown error") 33 | 34 | m.Asterisk.On("Info", ari.NodeKey("asdf", "1")).Return(nil, expected) 35 | 36 | ret, err := cl.Asterisk().Info(ari.NodeKey("asdf", "1")) 37 | if err == nil || eris.Cause(err).Error() != expected.Error() { 38 | t.Errorf("Expected error '%v', got '%v'", expected, err) 39 | } 40 | if ret != nil { 41 | t.Errorf("Expected nil ret, got '%v'", ret) 42 | } 43 | 44 | m.Shutdown() 45 | 46 | m.Asterisk.AssertCalled(t, "Info", ari.NodeKey("asdf", "1")) 47 | }) 48 | } 49 | 50 | func TestAsteriskVariablesGet(t *testing.T, s Server) { 51 | key := ari.NewKey(ari.VariableKey, "s") 52 | 53 | runTest("simple", t, s, func(t *testing.T, m *mock, cl ari.Client) { 54 | var ai ari.AsteriskInfo 55 | ai.SystemInfo.EntityID = "1" 56 | 57 | mv := arimocks.AsteriskVariables{} 58 | mv.On("Get", key).Return("hello", nil) 59 | m.Asterisk.On("Variables").Return(&mv) 60 | 61 | ret, err := cl.Asterisk().Variables().Get(key) 62 | if err != nil { 63 | t.Errorf("Unexpected error in remote Variables Get call: %v", err) 64 | } 65 | if ret != "hello" { 66 | t.Errorf("Expected Variables Get return %v, got %v", "hello", ret) 67 | } 68 | 69 | m.Shutdown() 70 | 71 | m.Asterisk.AssertCalled(t, "Variables") 72 | mv.AssertCalled(t, "Get", key) 73 | }) 74 | 75 | runTest("error", t, s, func(t *testing.T, m *mock, cl ari.Client) { 76 | expected := eris.New("unknown error") 77 | 78 | mv := arimocks.AsteriskVariables{} 79 | mv.On("Get", key).Return("", expected) 80 | 81 | m.Asterisk.On("Variables").Return(&mv) 82 | 83 | ret, err := cl.Asterisk().Variables().Get(key) 84 | if err == nil || eris.Cause(err).Error() != expected.Error() { 85 | t.Errorf("Expected error '%v', got '%v'", expected, err) 86 | } 87 | if ret != "" { 88 | t.Errorf("Expected Variables Get return %v, got %v", "", ret) 89 | } 90 | 91 | m.Shutdown() 92 | 93 | m.Asterisk.AssertCalled(t, "Variables") 94 | mv.AssertCalled(t, "Get", key) 95 | }) 96 | } 97 | 98 | func TestAsteriskVariablesSet(t *testing.T, s Server) { 99 | key := ari.NewKey(ari.VariableKey, "s") 100 | 101 | runTest("simple", t, s, func(t *testing.T, m *mock, cl ari.Client) { 102 | var ai ari.AsteriskInfo 103 | ai.SystemInfo.EntityID = "1" 104 | 105 | mv := arimocks.AsteriskVariables{} 106 | m.Asterisk.On("Variables").Return(&mv) 107 | mv.On("Set", key, "hello").Return(nil) 108 | 109 | err := cl.Asterisk().Variables().Set(key, "hello") 110 | if err != nil { 111 | t.Errorf("Unexpected error in remote Variables Set call: %v", err) 112 | } 113 | 114 | m.Shutdown() 115 | 116 | m.Asterisk.AssertCalled(t, "Variables") 117 | mv.AssertCalled(t, "Set", key, "hello") 118 | }) 119 | 120 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 121 | var ai ari.AsteriskInfo 122 | ai.SystemInfo.EntityID = "1" 123 | 124 | mv := arimocks.AsteriskVariables{} 125 | m.Asterisk.On("Variables").Return(&mv) 126 | mv.On("Set", key, "hello").Return(eris.New("error")) 127 | 128 | err := cl.Asterisk().Variables().Set(key, "hello") 129 | if err == nil { 130 | t.Errorf("Expected error in remote Variables Set call: %v", err) 131 | } 132 | 133 | m.Shutdown() 134 | 135 | m.Asterisk.AssertCalled(t, "Variables") 136 | mv.AssertCalled(t, "Set", key, "hello") 137 | }) 138 | } 139 | -------------------------------------------------------------------------------- /internal/integration/clientserver.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | 9 | "sync" 10 | 11 | "github.com/CyCoreSystems/ari/v5" 12 | "github.com/nats-io/nats.go" 13 | "github.com/rotisserie/eris" 14 | ) 15 | 16 | func natsConnect() (*nats.EncodedConn, error) { 17 | c, err := nats.Connect(nats.DefaultURL) 18 | if err != nil { 19 | return nil, eris.Wrap(err, "failed to connect to NATS") 20 | } 21 | nc, err := nats.NewEncodedConn(c, nats.JSON_ENCODER) 22 | if err != nil { 23 | return nil, eris.Wrap(err, "failed to encode NATS connection") 24 | } 25 | return nc, err 26 | } 27 | 28 | // Server represents a generalized ari-proxy server 29 | type Server interface { 30 | Start(ctx context.Context, t *testing.T, client ari.Client, nc *nats.EncodedConn, completeCh chan struct{}) (ari.Client, error) 31 | Ready() <-chan struct{} 32 | Close() error 33 | } 34 | 35 | // TestHandler is the interface for test execution 36 | type testHandler func(t *testing.T, m *mock, cl ari.Client) 37 | 38 | func runTest(desc string, t *testing.T, s Server, fn testHandler) { 39 | defer func() { 40 | // recover from panic if one occured. Set err to nil otherwise. 41 | if err := recover(); err != nil { 42 | t.Errorf("PANIC") 43 | } 44 | }() 45 | 46 | t.Run(desc, func(t *testing.T) { 47 | // setup mocking 48 | m := standardMock() 49 | 50 | // setup ari-proxy server 51 | ctx, cancel := context.WithCancel(context.Background()) 52 | defer cancel() 53 | 54 | nc, err := natsConnect() 55 | if err != nil { 56 | t.Skipf("Error connecting to nats: %s", err) 57 | } 58 | 59 | completeCh := make(chan struct{}) 60 | 61 | cl, err := s.Start(ctx, t, m.Client, nc, completeCh) 62 | if err != nil { 63 | t.Errorf("Failed to start client/server: %s", err) 64 | return 65 | } 66 | defer cl.Close() 67 | 68 | var once sync.Once 69 | 70 | m.Shutdown = func() { 71 | once.Do(func() { 72 | s.Close() 73 | 74 | cancel() 75 | <-completeCh 76 | }) 77 | } 78 | 79 | fn(t, m, cl) 80 | 81 | m.Shutdown() 82 | 83 | timeout, ok := TimeoutCount(cl) 84 | if !ok { 85 | t.Errorf("Failed to get timeout count from ari-proxy client") 86 | } else { 87 | if timeout > 0 { 88 | fmt.Fprintf(os.Stderr, "Timeouts: %d\n", timeout) 89 | } 90 | } 91 | }) 92 | } 93 | 94 | type timeoutCounter interface { 95 | TimeoutCount() int64 96 | } 97 | 98 | // TimeoutCount gets the timeout count from the ari client, if available. 99 | func TimeoutCount(c ari.Client) (int64, bool) { 100 | cl, ok := c.(timeoutCounter) 101 | if !ok { 102 | return 0, false 103 | } 104 | return cl.TimeoutCount(), true 105 | } 106 | -------------------------------------------------------------------------------- /internal/integration/config.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/CyCoreSystems/ari/v5" 8 | "github.com/CyCoreSystems/ari/v5/client/arimocks" 9 | tmock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | var _ = tmock.Anything 13 | 14 | func TestConfigData(t *testing.T, s Server) { 15 | key := ari.NewKey("config", ari.ConfigID("c1", "o1", "id1")) 16 | 17 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 18 | var expected ari.ConfigData 19 | expected.Class = "c1" 20 | expected.Key = key 21 | expected.Type = "o1" 22 | expected.Fields = []ari.ConfigTuple{ 23 | ari.ConfigTuple{ 24 | Value: "v1", 25 | Attribute: "a1", 26 | }, 27 | } 28 | 29 | cfg := arimocks.Config{} 30 | m.Asterisk.On("Config").Return(&cfg) 31 | cfg.On("Data", key).Return(&expected, nil) 32 | 33 | data, err := cl.Asterisk().Config().Data(key) 34 | if err != nil { 35 | t.Errorf("Unexpected error in remove config data call: %s", err) 36 | } 37 | if data == nil { 38 | t.Errorf("Expected non-nil data") 39 | } else { 40 | failed := false 41 | failed = failed || data.Class != expected.Class 42 | failed = failed || data.ID() != expected.ID() 43 | failed = failed || data.Type != expected.Type 44 | failed = failed || len(data.Fields) != len(expected.Fields) 45 | for idx := range data.Fields { 46 | failed = failed || data.Fields[idx].Attribute != expected.Fields[idx].Attribute 47 | failed = failed || data.Fields[idx].Value != expected.Fields[idx].Value 48 | } 49 | if failed { 50 | t.Errorf("Expected config data '%v', got '%v'", expected, data) 51 | } 52 | } 53 | 54 | m.Asterisk.AssertCalled(t, "Config") 55 | cfg.AssertCalled(t, "Data", key) 56 | }) 57 | 58 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 59 | cfg := arimocks.Config{} 60 | m.Asterisk.On("Config").Return(&cfg) 61 | cfg.On("Data", key).Return(nil, errors.New("error")) 62 | 63 | data, err := cl.Asterisk().Config().Data(key) 64 | if err == nil { 65 | t.Errorf("Expected error in remove config data call") 66 | } 67 | if data != nil { 68 | t.Errorf("Expected nil data") 69 | } 70 | 71 | m.Asterisk.AssertCalled(t, "Config") 72 | cfg.AssertCalled(t, "Data", key) 73 | }) 74 | } 75 | 76 | func TestConfigDelete(t *testing.T, s Server) { 77 | key := ari.NewKey("config", ari.ConfigID("c1", "o1", "id1")) 78 | 79 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 80 | cfg := arimocks.Config{} 81 | m.Asterisk.On("Config").Return(&cfg) 82 | cfg.On("Delete", key).Return(nil) 83 | 84 | err := cl.Asterisk().Config().Delete(key) 85 | if err != nil { 86 | t.Errorf("Unexpected error in remove config delete call: %s", err) 87 | } 88 | 89 | m.Asterisk.AssertCalled(t, "Config") 90 | cfg.AssertCalled(t, "Delete", key) 91 | }) 92 | 93 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 94 | cfg := arimocks.Config{} 95 | m.Asterisk.On("Config").Return(&cfg) 96 | cfg.On("Delete", key).Return(errors.New("error")) 97 | 98 | err := cl.Asterisk().Config().Delete(key) 99 | if err == nil { 100 | t.Errorf("Expected error in remove config delete call") 101 | } 102 | 103 | m.Asterisk.AssertCalled(t, "Config") 104 | cfg.AssertCalled(t, "Delete", key) 105 | }) 106 | } 107 | 108 | func TestConfigUpdate(t *testing.T, s Server) { 109 | key := ari.NewKey("config", ari.ConfigID("c1", "o1", "id1")) 110 | 111 | tuples := []ari.ConfigTuple{ 112 | ari.ConfigTuple{ 113 | Value: "v1", 114 | Attribute: "a1", 115 | }, 116 | ari.ConfigTuple{ 117 | Value: "v2", 118 | Attribute: "a2", 119 | }, 120 | } 121 | 122 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 123 | cfg := arimocks.Config{} 124 | m.Asterisk.On("Config").Return(&cfg) 125 | cfg.On("Update", key, tuples).Return(nil) 126 | 127 | err := cl.Asterisk().Config().Update(key, tuples) 128 | if err != nil { 129 | t.Errorf("Unexpected error in remove config Update call: %s", err) 130 | } 131 | 132 | m.Asterisk.AssertCalled(t, "Config") 133 | cfg.AssertCalled(t, "Update", key, tuples) 134 | }) 135 | 136 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 137 | cfg := arimocks.Config{} 138 | m.Asterisk.On("Config").Return(&cfg) 139 | cfg.On("Update", key, tuples).Return(errors.New("error")) 140 | 141 | err := cl.Asterisk().Config().Update(key, tuples) 142 | if err == nil { 143 | t.Errorf("Expected error in remove config Update call") 144 | } 145 | 146 | m.Asterisk.AssertCalled(t, "Config") 147 | cfg.AssertCalled(t, "Update", key, tuples) 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /internal/integration/device.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/CyCoreSystems/ari/v5" 8 | ) 9 | 10 | func TestDeviceData(t *testing.T, s Server) { 11 | key := ari.NewKey(ari.DeviceStateKey, "d1") 12 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 13 | var expected ari.DeviceStateData 14 | expected.State = "deviceData1" 15 | expected.Key = ari.NewKey(ari.DeviceStateKey, "d1") 16 | 17 | m.DeviceState.On("Data", key).Return(&expected, nil) 18 | 19 | data, err := cl.DeviceState().Data(key) 20 | if err != nil { 21 | t.Errorf("Error in remote device state data call: %s", err) 22 | } 23 | if data == nil || data.State != expected.State { 24 | t.Errorf("Expected data '%s', got '%v'", expected, data) 25 | } 26 | 27 | m.DeviceState.AssertCalled(t, "Data", key) 28 | }) 29 | 30 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 31 | m.DeviceState.On("Data", key).Return(nil, errors.New("err")) 32 | 33 | data, err := cl.DeviceState().Data(key) 34 | if err == nil { 35 | t.Errorf("Expected error in remote device state data call: %s", err) 36 | } 37 | if data != nil { 38 | t.Errorf("Expected data to be nil, got '%v'", *data) 39 | } 40 | 41 | m.DeviceState.AssertCalled(t, "Data", key) 42 | }) 43 | } 44 | 45 | func TestDeviceDelete(t *testing.T, s Server) { 46 | key := ari.NewKey(ari.DeviceStateKey, "d1") 47 | 48 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 49 | m.DeviceState.On("Delete", key).Return(nil) 50 | 51 | err := cl.DeviceState().Delete(key) 52 | if err != nil { 53 | t.Errorf("Error in remote device state Delete call: %s", err) 54 | } 55 | 56 | m.DeviceState.AssertCalled(t, "Delete", key) 57 | }) 58 | 59 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 60 | m.DeviceState.On("Delete", key).Return(errors.New("err")) 61 | 62 | err := cl.DeviceState().Delete(key) 63 | if err == nil { 64 | t.Errorf("Expected error in remote device state Delete call: %s", err) 65 | } 66 | 67 | m.DeviceState.AssertCalled(t, "Delete", key) 68 | }) 69 | } 70 | 71 | func TestDeviceUpdate(t *testing.T, s Server) { 72 | key := ari.NewKey(ari.DeviceStateKey, "d1") 73 | 74 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 75 | m.DeviceState.On("Update", key, "st1").Return(nil) 76 | 77 | err := cl.DeviceState().Update(key, "st1") 78 | if err != nil { 79 | t.Errorf("Error in remote device state Update call: %s", err) 80 | } 81 | 82 | m.DeviceState.AssertCalled(t, "Update", key, "st1") 83 | }) 84 | 85 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 86 | m.DeviceState.On("Update", key, "st1").Return(errors.New("err")) 87 | 88 | err := cl.DeviceState().Update(key, "st1") 89 | if err == nil { 90 | t.Errorf("Expected error in remote device state Update call: %s", err) 91 | } 92 | 93 | m.DeviceState.AssertCalled(t, "Update", key, "st1") 94 | }) 95 | } 96 | 97 | func TestDeviceList(t *testing.T, s Server) { 98 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 99 | h1 := ari.NewKey(ari.DeviceStateKey, "h1") 100 | h2 := ari.NewKey(ari.DeviceStateKey, "h2") 101 | 102 | m.DeviceState.On("List", (*ari.Key)(nil)).Return([]*ari.Key{h1, h2}, nil) 103 | 104 | list, err := cl.DeviceState().List(nil) 105 | if err != nil { 106 | t.Errorf("Error in remote device state List call: %s", err) 107 | } 108 | if len(list) != 2 { 109 | t.Errorf("Expected list of length 2, got %d", len(list)) 110 | } 111 | 112 | m.DeviceState.AssertCalled(t, "List", (*ari.Key)(nil)) 113 | }) 114 | 115 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 116 | m.DeviceState.On("List", (*ari.Key)(nil)).Return([]*ari.Key{}, errors.New("error")) 117 | 118 | list, err := cl.DeviceState().List(nil) 119 | if err == nil { 120 | t.Errorf("Expected error in remote device state List call") 121 | } 122 | if len(list) != 0 { 123 | t.Errorf("Expected list of length 0, got %d", len(list)) 124 | } 125 | 126 | m.DeviceState.AssertCalled(t, "List", (*ari.Key)(nil)) 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /internal/integration/doc.go: -------------------------------------------------------------------------------- 1 | // Package integration contains integration tests for server to client communications 2 | package integration 3 | -------------------------------------------------------------------------------- /internal/integration/endpoint.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/CyCoreSystems/ari/v5" 8 | ) 9 | 10 | func TestEndpointList(t *testing.T, s Server) { 11 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 12 | h1 := ari.NewEndpointKey("h1", "1") 13 | h2 := ari.NewEndpointKey("h1", "2") 14 | 15 | m.Endpoint.On("List", (*ari.Key)(nil)).Return([]*ari.Key{h1, h2}, nil) 16 | 17 | list, err := cl.Endpoint().List(nil) 18 | if err != nil { 19 | t.Errorf("Error in remote Endpoint List call: %s", err) 20 | } 21 | if len(list) != 2 { 22 | t.Errorf("Expected list of length 2, got %d", len(list)) 23 | } 24 | 25 | m.Endpoint.AssertCalled(t, "List", (*ari.Key)(nil)) 26 | }) 27 | 28 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 29 | m.Endpoint.On("List", (*ari.Key)(nil)).Return([]*ari.Key{}, errors.New("error")) 30 | 31 | list, err := cl.Endpoint().List(nil) 32 | if err == nil { 33 | t.Errorf("Expected error in remote Endpoint List call") 34 | } 35 | if len(list) != 0 { 36 | t.Errorf("Expected list of length 0, got %d", len(list)) 37 | } 38 | 39 | m.Endpoint.AssertCalled(t, "List", (*ari.Key)(nil)) 40 | }) 41 | } 42 | 43 | func TestEndpointListByTech(t *testing.T, s Server) { 44 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 45 | h1 := ari.NewEndpointKey("h1", "1") 46 | h2 := ari.NewEndpointKey("h1", "2") 47 | 48 | m.Endpoint.On("ListByTech", "tech", &ari.Key{Kind: "", ID: "", Node: "", Dialog: "", App: ""}).Return([]*ari.Key{h1, h2}, nil) 49 | 50 | list, err := cl.Endpoint().ListByTech("tech", nil) 51 | if err != nil { 52 | t.Errorf("Error in remote Endpoint List call: %s", err) 53 | } 54 | if len(list) != 2 { 55 | t.Errorf("Expected list of length 2, got %d", len(list)) 56 | } 57 | 58 | m.Endpoint.AssertCalled(t, "ListByTech", "tech", &ari.Key{Kind: "", ID: "", Node: "", Dialog: "", App: ""}) 59 | }) 60 | 61 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 62 | m.Endpoint.On("ListByTech", "tech", &ari.Key{Kind: "", ID: "", Node: "", Dialog: "", App: ""}).Return([]*ari.Key{}, errors.New("error")) 63 | 64 | list, err := cl.Endpoint().ListByTech("tech", nil) 65 | if err == nil { 66 | t.Errorf("Expected error in remote Endpoint List call") 67 | } 68 | if len(list) != 0 { 69 | t.Errorf("Expected list of length 0, got %d", len(list)) 70 | } 71 | 72 | m.Endpoint.AssertCalled(t, "ListByTech", "tech", &ari.Key{Kind: "", ID: "", Node: "", Dialog: "", App: ""}) 73 | }) 74 | } 75 | 76 | func TestEndpointData(t *testing.T, s Server) { 77 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 78 | var expected ari.EndpointData 79 | expected.State = "st1" 80 | expected.Technology = "tech1" 81 | expected.Resource = "resource" 82 | 83 | h1 := ari.NewEndpointKey(expected.Technology, expected.Resource) 84 | 85 | m.Endpoint.On("Data", h1).Return(&expected, nil) 86 | 87 | data, err := cl.Endpoint().Data(h1) 88 | if err != nil { 89 | t.Errorf("Error in remote Endpoint Data call: %s", err) 90 | } 91 | if data == nil { 92 | t.Errorf("Expected data to be non-nil") 93 | } else { 94 | failed := false 95 | failed = failed || expected.State != data.State 96 | failed = failed || expected.Resource != data.Resource 97 | failed = failed || expected.Technology != data.Technology 98 | if failed { 99 | t.Errorf("Expected '%v', got '%v'", expected, data) 100 | } 101 | } 102 | 103 | m.Endpoint.AssertCalled(t, "Data", h1) 104 | }) 105 | 106 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 107 | var expected ari.EndpointData 108 | expected.State = "st1" 109 | expected.Technology = "tech1" 110 | expected.Resource = "resource" 111 | 112 | h1 := ari.NewEndpointKey(expected.Technology, expected.Resource) 113 | 114 | m.Endpoint.On("Data", h1).Return(nil, errors.New("error")) 115 | 116 | data, err := cl.Endpoint().Data(h1) 117 | if err == nil { 118 | t.Errorf("Expected error in remote Endpoint Data call") 119 | } 120 | if data != nil { 121 | t.Errorf("Expected data to be nil") 122 | } 123 | 124 | m.Endpoint.AssertCalled(t, "Data", h1) 125 | }) 126 | } 127 | -------------------------------------------------------------------------------- /internal/integration/liverecording.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/CyCoreSystems/ari/v5" 9 | ) 10 | 11 | func TestLiveRecordingData(t *testing.T, s Server) { 12 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 13 | expected := ari.LiveRecordingData{ 14 | Name: "n1", 15 | Format: "format", 16 | Cause: "c1", 17 | Silence: ari.DurationSec(3 * time.Second), 18 | State: "st1", 19 | Talking: ari.DurationSec(3 * time.Second), 20 | TargetURI: "uri1", 21 | Duration: ari.DurationSec(6 * time.Second), 22 | } 23 | 24 | key := ari.NewKey(ari.LiveRecordingKey, "lr1") 25 | 26 | m.LiveRecording.On("Data", key).Return(&expected, nil) 27 | 28 | ret, err := cl.LiveRecording().Data(key) 29 | if err != nil { 30 | t.Errorf("Unexpected error in liverecording Data: %s", err) 31 | } 32 | if ret == nil { 33 | t.Errorf("Expected live recording data to be non-nil") 34 | } else { 35 | failed := false 36 | failed = failed || ret.Name != expected.Name 37 | failed = failed || ret.Format != expected.Format 38 | failed = failed || ret.Cause != expected.Cause 39 | failed = failed || ret.Silence != expected.Silence 40 | failed = failed || ret.State != expected.State 41 | failed = failed || ret.Talking != expected.Talking 42 | failed = failed || ret.TargetURI != expected.TargetURI 43 | failed = failed || ret.Duration != expected.Duration 44 | if failed { 45 | t.Errorf("Expected '%v', got '%v'", expected, ret) 46 | } 47 | } 48 | 49 | m.LiveRecording.AssertCalled(t, "Data", key) 50 | }) 51 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 52 | key := ari.NewKey(ari.LiveRecordingKey, "lr1") 53 | 54 | m.LiveRecording.On("Data", key).Return(nil, errors.New("err")) 55 | 56 | ret, err := cl.LiveRecording().Data(key) 57 | if err == nil { 58 | t.Errorf("Expected error in liverecording Data") 59 | } 60 | if ret != nil { 61 | t.Errorf("Expected live recording data to be nil") 62 | } 63 | 64 | m.LiveRecording.AssertCalled(t, "Data", key) 65 | }) 66 | } 67 | 68 | func testLiveRecordingCommand(t *testing.T, m *mock, name string, id *ari.Key, expected error, fn func(*ari.Key) error) { 69 | m.LiveRecording.On(name, id).Return(expected) 70 | err := fn(id) 71 | failed := false 72 | failed = failed || err == nil && expected != nil 73 | failed = failed || err != nil && expected == nil 74 | failed = failed || err != nil && expected != nil && err.Error() != expected.Error() 75 | if failed { 76 | t.Errorf("Expected live recording %s(%s) to return '%v', got '%v'", 77 | name, id, expected, err, 78 | ) 79 | } 80 | m.LiveRecording.AssertCalled(t, name, id) 81 | } 82 | 83 | /* 84 | func TestLiveRecordingDelete(t *testing.T, s Server) { 85 | key := ari.NewKey(ari.LiveRecordingKey, "lr1") 86 | 87 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 88 | testLiveRecordingCommand(t, m, "Delete", key, nil, cl.LiveRecording().Delete) 89 | }) 90 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 91 | testLiveRecordingCommand(t, m, "Delete", key, errors.New("err"), cl.LiveRecording().Delete) 92 | }) 93 | } 94 | */ 95 | 96 | func TestLiveRecordingMute(t *testing.T, s Server) { 97 | key := ari.NewKey(ari.LiveRecordingKey, "lr1") 98 | 99 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 100 | testLiveRecordingCommand(t, m, "Mute", key, nil, cl.LiveRecording().Mute) 101 | }) 102 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 103 | testLiveRecordingCommand(t, m, "Mute", key, errors.New("err"), cl.LiveRecording().Mute) 104 | }) 105 | } 106 | 107 | func TestLiveRecordingPause(t *testing.T, s Server) { 108 | key := ari.NewKey(ari.LiveRecordingKey, "lr1") 109 | 110 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 111 | testLiveRecordingCommand(t, m, "Pause", key, nil, cl.LiveRecording().Pause) 112 | }) 113 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 114 | testLiveRecordingCommand(t, m, "Pause", key, errors.New("err"), cl.LiveRecording().Pause) 115 | }) 116 | } 117 | 118 | func TestLiveRecordingStop(t *testing.T, s Server) { 119 | key := ari.NewKey(ari.LiveRecordingKey, "lr1") 120 | 121 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 122 | testLiveRecordingCommand(t, m, "Stop", key, nil, cl.LiveRecording().Stop) 123 | }) 124 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 125 | testLiveRecordingCommand(t, m, "Stop", key, errors.New("err"), cl.LiveRecording().Stop) 126 | }) 127 | } 128 | 129 | func TestLiveRecordingUnmute(t *testing.T, s Server) { 130 | key := ari.NewKey(ari.LiveRecordingKey, "lr1") 131 | 132 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 133 | testLiveRecordingCommand(t, m, "Unmute", key, nil, cl.LiveRecording().Unmute) 134 | }) 135 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 136 | testLiveRecordingCommand(t, m, "Unmute", key, errors.New("err"), cl.LiveRecording().Unmute) 137 | }) 138 | } 139 | 140 | func TestLiveRecordingResume(t *testing.T, s Server) { 141 | key := ari.NewKey(ari.LiveRecordingKey, "lr1") 142 | 143 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 144 | testLiveRecordingCommand(t, m, "Resume", key, nil, cl.LiveRecording().Resume) 145 | }) 146 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 147 | testLiveRecordingCommand(t, m, "Resume", key, errors.New("err"), cl.LiveRecording().Resume) 148 | }) 149 | } 150 | 151 | func TestLiveRecordingScrap(t *testing.T, s Server) { 152 | key := ari.NewKey(ari.LiveRecordingKey, "lr1") 153 | 154 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 155 | testLiveRecordingCommand(t, m, "Scrap", key, nil, cl.LiveRecording().Scrap) 156 | }) 157 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 158 | testLiveRecordingCommand(t, m, "Scrap", key, errors.New("err"), cl.LiveRecording().Scrap) 159 | }) 160 | } 161 | -------------------------------------------------------------------------------- /internal/integration/logging.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/CyCoreSystems/ari/v5" 8 | ) 9 | 10 | func TestLoggingList(t *testing.T, s Server) { 11 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 12 | expected := []*ari.Key{ 13 | ari.NewKey(ari.LoggingKey, "n1"), 14 | } 15 | 16 | m.Logging.On("List", (*ari.Key)(nil)).Return(expected, nil) 17 | 18 | ld, err := cl.Asterisk().Logging().List(nil) 19 | if err != nil { 20 | t.Errorf("Unexpected error in logging list: %s", err) 21 | } 22 | if len(ld) != len(expected) { 23 | t.Errorf("Expected return of length %d, got %d", len(expected), len(ld)) 24 | } else { 25 | for idx := range ld { 26 | failed := false 27 | failed = failed || ld[idx].ID != expected[idx].ID 28 | 29 | if failed { 30 | t.Errorf("Expected item '%d' to be '%v', got '%v", 31 | idx, expected[idx], ld[idx]) 32 | } 33 | } 34 | } 35 | 36 | m.Logging.AssertCalled(t, "List", (*ari.Key)(nil)) 37 | }) 38 | 39 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 40 | var expected []*ari.Key 41 | 42 | m.Logging.On("List", (*ari.Key)(nil)).Return(expected, errors.New("error")) 43 | 44 | ld, err := cl.Asterisk().Logging().List(nil) 45 | if err == nil { 46 | t.Errorf("Expected error in logging list") 47 | } 48 | if len(ld) != len(expected) { 49 | t.Errorf("Expected return of length %d, got %d", len(expected), len(ld)) 50 | } else { 51 | for idx := range ld { 52 | failed := false 53 | failed = failed || ld[idx].ID != expected[idx].ID // nolint 54 | 55 | if failed { 56 | t.Errorf("Expected item '%d' to be '%v', got '%v", 57 | idx, expected[idx], ld[idx]) // nolint 58 | } 59 | } 60 | } 61 | 62 | m.Logging.AssertCalled(t, "List", (*ari.Key)(nil)) 63 | }) 64 | } 65 | 66 | func TestLoggingCreate(t *testing.T, s Server) { 67 | key := ari.NewKey(ari.LoggingKey, "n1") 68 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 69 | m.Logging.On("Create", key, "l1").Return(ari.NewLogHandle(key, m.Logging), nil) 70 | 71 | _, err := cl.Asterisk().Logging().Create(key, "l1") 72 | if err != nil { 73 | t.Errorf("Unexpected error in logging create: %s", err) 74 | } 75 | 76 | m.Logging.AssertCalled(t, "Create", key, "l1") 77 | }) 78 | 79 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 80 | m.Logging.On("Create", key, "l1").Return(nil, errors.New("error")) 81 | 82 | _, err := cl.Asterisk().Logging().Create(key, "l1") 83 | if err == nil { 84 | t.Errorf("Expected error in logging create") 85 | } 86 | 87 | m.Logging.AssertCalled(t, "Create", key, "l1") 88 | }) 89 | } 90 | 91 | func TestLoggingDelete(t *testing.T, s Server) { 92 | key := ari.NewKey(ari.LoggingKey, "n1") 93 | 94 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 95 | m.Logging.On("Delete", key).Return(nil) 96 | 97 | err := cl.Asterisk().Logging().Delete(key) 98 | if err != nil { 99 | t.Errorf("Unexpected error in logging Delete: %s", err) 100 | } 101 | 102 | m.Logging.AssertCalled(t, "Delete", key) 103 | }) 104 | 105 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 106 | m.Logging.On("Delete", key).Return(errors.New("error")) 107 | 108 | err := cl.Asterisk().Logging().Delete(key) 109 | if err == nil { 110 | t.Errorf("Expected error in logging Delete") 111 | } 112 | 113 | m.Logging.AssertCalled(t, "Delete", key) 114 | }) 115 | } 116 | 117 | func TestLoggingRotate(t *testing.T, s Server) { 118 | key := ari.NewKey(ari.LoggingKey, "n1") 119 | 120 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 121 | m.Logging.On("Rotate", key).Return(nil) 122 | 123 | err := cl.Asterisk().Logging().Rotate(key) 124 | if err != nil { 125 | t.Errorf("Unexpected error in logging Rotate: %s", err) 126 | } 127 | 128 | m.Logging.AssertCalled(t, "Rotate", key) 129 | }) 130 | 131 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 132 | m.Logging.On("Rotate", key).Return(errors.New("error")) 133 | 134 | err := cl.Asterisk().Logging().Rotate(key) 135 | if err == nil { 136 | t.Errorf("Expected error in logging Rotate") 137 | } 138 | 139 | m.Logging.AssertCalled(t, "Rotate", key) 140 | }) 141 | } 142 | -------------------------------------------------------------------------------- /internal/integration/mailbox.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/CyCoreSystems/ari/v5" 8 | ) 9 | 10 | func TestMailboxList(t *testing.T, s Server) { 11 | runTest("empty", t, s, func(t *testing.T, m *mock, cl ari.Client) { 12 | m.Mailbox.On("List", (*ari.Key)(nil)).Return([]*ari.Key{}, nil) 13 | 14 | ret, err := cl.Mailbox().List(nil) 15 | if err != nil { 16 | t.Errorf("Unexpected error in remote List call") 17 | } 18 | if len(ret) != 0 { 19 | t.Errorf("Expected return length to be 0, got %d", len(ret)) 20 | } 21 | 22 | m.Shutdown() 23 | 24 | m.Mailbox.AssertCalled(t, "List", (*ari.Key)(nil)) 25 | }) 26 | 27 | runTest("nonEmpty", t, s, func(t *testing.T, m *mock, cl ari.Client) { 28 | h1 := ari.NewKey(ari.MailboxKey, "h1") 29 | h2 := ari.NewKey(ari.MailboxKey, "h2") 30 | 31 | m.Mailbox.On("List", (*ari.Key)(nil)).Return([]*ari.Key{h1, h2}, nil) 32 | 33 | ret, err := cl.Mailbox().List(nil) 34 | if err != nil { 35 | t.Errorf("Unexpected error in remote List call") 36 | } 37 | if len(ret) != 2 { 38 | t.Errorf("Expected return length to be 2, got %d", len(ret)) 39 | } 40 | 41 | m.Shutdown() 42 | 43 | m.Mailbox.AssertCalled(t, "List", (*ari.Key)(nil)) 44 | }) 45 | 46 | runTest("error", t, s, func(t *testing.T, m *mock, cl ari.Client) { 47 | m.Mailbox.On("List", (*ari.Key)(nil)).Return(nil, errors.New("unknown error")) 48 | 49 | ret, err := cl.Mailbox().List(nil) 50 | if err == nil { 51 | t.Errorf("Expected error in remote List call") 52 | } 53 | if len(ret) != 0 { 54 | t.Errorf("Expected return length to be 0, got %d", len(ret)) 55 | } 56 | 57 | m.Shutdown() 58 | 59 | m.Mailbox.AssertCalled(t, "List", (*ari.Key)(nil)) 60 | }) 61 | } 62 | 63 | func testMailboxCommand(t *testing.T, m *mock, name string, id *ari.Key, expected error, fn func(*ari.Key) error) { 64 | m.Mailbox.On(name, id).Return(expected) 65 | err := fn(id) 66 | failed := false 67 | failed = failed || err == nil && expected != nil 68 | failed = failed || err != nil && expected == nil 69 | failed = failed || err != nil && expected != nil && err.Error() != expected.Error() 70 | if failed { 71 | t.Errorf("Expected mailbox %s(%s) to return '%v', got '%v'", 72 | name, id, expected, err, 73 | ) 74 | } 75 | m.Mailbox.AssertCalled(t, name, id) 76 | } 77 | 78 | func TestMailboxDelete(t *testing.T, s Server) { 79 | key := ari.NewKey(ari.MailboxKey, "mbox1") 80 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 81 | testMailboxCommand(t, m, "Delete", key, nil, cl.Mailbox().Delete) 82 | }) 83 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 84 | testMailboxCommand(t, m, "Delete", key, errors.New("err"), cl.Mailbox().Delete) 85 | }) 86 | } 87 | 88 | func TestMailboxUpdate(t *testing.T, s Server) { 89 | key := ari.NewKey(ari.MailboxKey, "mbox1") 90 | 91 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 92 | var expected error 93 | 94 | m.Mailbox.On("Update", key, 1, 1).Return(expected) 95 | 96 | err := cl.Mailbox().Update(key, 1, 1) 97 | 98 | failed := false 99 | failed = failed || err == nil && expected != nil 100 | failed = failed || err != nil && expected == nil 101 | failed = failed || err != nil && expected != nil && err.Error() != expected.Error() // nolint 102 | if failed { 103 | t.Errorf("Expected mailbox %s(%s) to return '%v', got '%v'", 104 | "Update", "mbox1", expected, err, 105 | ) 106 | } 107 | 108 | m.Mailbox.AssertCalled(t, "Update", key, 1, 1) 109 | }) 110 | 111 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 112 | expected := errors.New("error") 113 | 114 | m.Mailbox.On("Update", key, 1, 1).Return(expected) 115 | 116 | err := cl.Mailbox().Update(key, 1, 1) 117 | 118 | failed := false 119 | failed = failed || err == nil && expected != nil 120 | failed = failed || err != nil && expected == nil 121 | failed = failed || err != nil && expected != nil && err.Error() != expected.Error() 122 | if failed { 123 | t.Errorf("Expected mailbox %s(%s) to return '%v', got '%v'", 124 | "Update", "mbox1", expected, err, 125 | ) 126 | } 127 | 128 | m.Mailbox.AssertCalled(t, "Update", key, 1, 1) 129 | }) 130 | } 131 | 132 | func TestMailboxData(t *testing.T, s Server) { 133 | key := ari.NewKey(ari.MailboxKey, "mbox1") 134 | 135 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 136 | var expected ari.MailboxData 137 | expected.Name = "mbox1" 138 | expected.NewMessages = 2 139 | expected.OldMessages = 3 140 | 141 | m.Mailbox.On("Data", key).Return(&expected, nil) 142 | 143 | data, err := cl.Mailbox().Data(key) 144 | if err != nil { 145 | t.Errorf("Unexpected error in remote mailbox Data: %s", err) 146 | } 147 | if data == nil { 148 | t.Errorf("Expected non-nil mailbox data") 149 | } else { 150 | failed := data.Name != expected.Name 151 | failed = failed || data.NewMessages != expected.NewMessages 152 | failed = failed || data.OldMessages != expected.OldMessages 153 | if failed { 154 | t.Errorf("Expected data '%v', got '%v'", expected, data) 155 | } 156 | } 157 | 158 | m.Mailbox.AssertCalled(t, "Data", key) 159 | }) 160 | 161 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 162 | expected := errors.New("error") 163 | 164 | m.Mailbox.On("Data", key).Return(nil, expected) 165 | 166 | _, err := cl.Mailbox().Data(key) 167 | 168 | failed := false 169 | failed = failed || err == nil && expected != nil 170 | failed = failed || err != nil && expected == nil 171 | failed = failed || err != nil && expected != nil && err.Error() != expected.Error() 172 | if failed { 173 | t.Errorf("Expected mailbox %s(%s) to return '%v', got '%v'", 174 | "Data", "mbox1", expected, err, 175 | ) 176 | } 177 | 178 | m.Mailbox.AssertCalled(t, "Data", key) 179 | }) 180 | } 181 | -------------------------------------------------------------------------------- /internal/integration/mock.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "github.com/CyCoreSystems/ari/v5" 5 | "github.com/CyCoreSystems/ari/v5/client/arimocks" 6 | tmock "github.com/stretchr/testify/mock" 7 | ) 8 | 9 | type mock struct { 10 | Bus *arimocks.Bus 11 | Client *arimocks.Client 12 | 13 | Application *arimocks.Application 14 | Asterisk *arimocks.Asterisk 15 | Bridge *arimocks.Bridge 16 | Channel *arimocks.Channel 17 | DeviceState *arimocks.DeviceState 18 | Endpoint *arimocks.Endpoint 19 | LiveRecording *arimocks.LiveRecording 20 | Logging *arimocks.Logging 21 | Mailbox *arimocks.Mailbox 22 | Modules *arimocks.Modules 23 | Playback *arimocks.Playback 24 | Sound *arimocks.Sound 25 | 26 | AllSub *arimocks.Subscription 27 | AllEventChannel <-chan ari.Event 28 | 29 | Shutdown func() 30 | } 31 | 32 | func standardMock() *mock { 33 | m := &mock{} 34 | 35 | m.Bus = &arimocks.Bus{} 36 | m.Client = &arimocks.Client{} 37 | 38 | m.Asterisk = &arimocks.Asterisk{} 39 | m.Application = &arimocks.Application{} 40 | m.Bridge = &arimocks.Bridge{} 41 | m.Channel = &arimocks.Channel{} 42 | m.DeviceState = &arimocks.DeviceState{} 43 | m.Endpoint = &arimocks.Endpoint{} 44 | m.LiveRecording = &arimocks.LiveRecording{} 45 | m.Logging = &arimocks.Logging{} 46 | m.Mailbox = &arimocks.Mailbox{} 47 | m.Modules = &arimocks.Modules{} 48 | m.Playback = &arimocks.Playback{} 49 | m.Sound = &arimocks.Sound{} 50 | 51 | m.AllSub = &arimocks.Subscription{} 52 | 53 | eventCh := make(<-chan ari.Event) 54 | 55 | m.AllSub.On("Cancel").Return(nil) 56 | m.AllSub.On("Events").Return(eventCh) 57 | m.Bus.On("Subscribe", tmock.Anything, "all").Return(m.AllSub).Times(1) 58 | 59 | m.Client.On("Bus").Return(m.Bus) 60 | 61 | m.Client.On("ApplicationName").Return("asdf") 62 | m.Client.On("Asterisk").Return(m.Asterisk) 63 | m.Client.On("Application").Return(m.Application) 64 | m.Client.On("Bridge").Return(m.Bridge) 65 | m.Client.On("Channel").Return(m.Channel) 66 | m.Client.On("DeviceState").Return(m.DeviceState) 67 | m.Client.On("Endpoint").Return(m.Endpoint) 68 | m.Client.On("LiveRecording").Return(m.LiveRecording) 69 | m.Asterisk.On("Logging").Return(m.Logging) 70 | m.Client.On("Mailbox").Return(m.Mailbox) 71 | m.Asterisk.On("Modules").Return(m.Modules) 72 | m.Client.On("Playback").Return(m.Playback) 73 | m.Client.On("Sound").Return(m.Sound) 74 | 75 | m.Asterisk.On("Info", (*ari.Key)(nil)).Return(&ari.AsteriskInfo{ 76 | SystemInfo: ari.SystemInfo{ 77 | EntityID: "1", 78 | }, 79 | }, nil).Times(1) // ensure that downstream tests of Info do not struggle to test additional cases 80 | 81 | return m 82 | } 83 | -------------------------------------------------------------------------------- /internal/integration/modules.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/CyCoreSystems/ari/v5" 8 | ) 9 | 10 | func TestModulesLoad(t *testing.T, s Server) { 11 | key := ari.NewKey(ari.ModuleKey, "m1") 12 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 13 | m.Modules.On("Load", key).Return(nil) 14 | 15 | if err := cl.Asterisk().Modules().Load(key); err != nil { 16 | t.Errorf("Unexpected error in module load: %s", err) 17 | } 18 | 19 | m.Shutdown() 20 | 21 | m.Asterisk.AssertCalled(t, "Modules") 22 | m.Modules.AssertCalled(t, "Load", key) 23 | }) 24 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 25 | m.Modules.On("Load", key).Return(errors.New("error")) 26 | 27 | if err := cl.Asterisk().Modules().Load(key); err == nil { 28 | t.Errorf("Expected error in module load") 29 | } 30 | 31 | m.Shutdown() 32 | 33 | m.Asterisk.AssertCalled(t, "Modules") 34 | m.Modules.AssertCalled(t, "Load", key) 35 | }) 36 | } 37 | 38 | func TestModulesUnload(t *testing.T, s Server) { 39 | key := ari.NewKey(ari.ModuleKey, "m1") 40 | 41 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 42 | m.Modules.On("Unload", key).Return(nil) 43 | 44 | if err := cl.Asterisk().Modules().Unload(key); err != nil { 45 | t.Errorf("Unexpected error in module Unload: %s", err) 46 | } 47 | 48 | m.Shutdown() 49 | 50 | m.Asterisk.AssertCalled(t, "Modules") 51 | m.Modules.AssertCalled(t, "Unload", key) 52 | }) 53 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 54 | m.Modules.On("Unload", key).Return(errors.New("error")) 55 | 56 | if err := cl.Asterisk().Modules().Unload(key); err == nil { 57 | t.Errorf("Expected error in module Unload") 58 | } 59 | 60 | m.Shutdown() 61 | 62 | m.Asterisk.AssertCalled(t, "Modules") 63 | m.Modules.AssertCalled(t, "Unload", key) 64 | }) 65 | } 66 | 67 | func TestModulesReload(t *testing.T, s Server) { 68 | key := ari.NewKey(ari.ModuleKey, "m1") 69 | 70 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 71 | m.Modules.On("Reload", key).Return(nil) 72 | 73 | if err := cl.Asterisk().Modules().Reload(key); err != nil { 74 | t.Errorf("Unexpected error in module Reload: %s", err) 75 | } 76 | 77 | m.Shutdown() 78 | 79 | m.Asterisk.AssertCalled(t, "Modules") 80 | m.Modules.AssertCalled(t, "Reload", key) 81 | }) 82 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 83 | m.Modules.On("Reload", key).Return(errors.New("error")) 84 | 85 | if err := cl.Asterisk().Modules().Reload(key); err == nil { 86 | t.Errorf("Expected error in module Reload") 87 | } 88 | 89 | m.Shutdown() 90 | 91 | m.Asterisk.AssertCalled(t, "Modules") 92 | m.Modules.AssertCalled(t, "Reload", key) 93 | }) 94 | } 95 | 96 | func TestModulesData(t *testing.T, s Server) { 97 | key := ari.NewKey(ari.ModuleKey, "m1") 98 | 99 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 100 | var d ari.ModuleData 101 | d.Description = "Desc" 102 | d.Name = "name" 103 | 104 | m.Modules.On("Data", key).Return(&d, nil) 105 | 106 | ret, err := cl.Asterisk().Modules().Data(key) 107 | if err != nil { 108 | t.Errorf("Unexpected error in module Data: %s", err) 109 | } 110 | if ret == nil { 111 | t.Errorf("Expected module data to be non-nil") 112 | } else { 113 | if ret.Description != d.Description { 114 | t.Errorf("description mismatch: %v %v", ret.Description, d.Description) 115 | } 116 | if ret.Name != d.Name { 117 | t.Errorf("name mismatch: expected '%v', got '%v'", d, ret) 118 | } 119 | } 120 | 121 | m.Shutdown() 122 | 123 | m.Asterisk.AssertCalled(t, "Modules") 124 | m.Modules.AssertCalled(t, "Data", key) 125 | }) 126 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 127 | m.Modules.On("Data", key).Return(nil, errors.New("error")) 128 | 129 | _, err := cl.Asterisk().Modules().Data(key) 130 | if err == nil { 131 | t.Errorf("Expected error in module Data: %s", err) 132 | } 133 | 134 | m.Shutdown() 135 | 136 | m.Asterisk.AssertCalled(t, "Modules") 137 | m.Modules.AssertCalled(t, "Data", key) 138 | }) 139 | } 140 | 141 | func TestModulesList(t *testing.T, s Server) { 142 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 143 | m1 := ari.NewKey(ari.ModuleKey, "m1") 144 | m2 := ari.NewKey(ari.ModuleKey, "m2") 145 | 146 | m.Modules.On("List", (*ari.Key)(nil)).Return([]*ari.Key{m1, m2}, nil) 147 | 148 | ret, err := cl.Asterisk().Modules().List(nil) 149 | if err != nil { 150 | t.Errorf("Unepected error in module List: %s", err) 151 | } 152 | if len(ret) != 2 { 153 | t.Errorf("Expected handle list length of size '2', got '%d'", len(ret)) 154 | } 155 | 156 | m.Shutdown() 157 | 158 | m.Asterisk.AssertCalled(t, "Modules") 159 | m.Modules.AssertCalled(t, "List", (*ari.Key)(nil)) 160 | }) 161 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 162 | m.Modules.On("List", (*ari.Key)(nil)).Return([]*ari.Key{}, errors.New("error")) 163 | 164 | _, err := cl.Asterisk().Modules().List(nil) 165 | if err == nil { 166 | t.Errorf("Expected error in module List") 167 | } 168 | 169 | m.Shutdown() 170 | 171 | m.Asterisk.AssertCalled(t, "Modules") 172 | m.Modules.AssertCalled(t, "List", (*ari.Key)(nil)) 173 | }) 174 | } 175 | -------------------------------------------------------------------------------- /internal/integration/playback.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/CyCoreSystems/ari/v5" 8 | ) 9 | 10 | func TestPlaybackData(t *testing.T, s Server) { 11 | key := ari.NewKey(ari.PlaybackKey, "pb1") 12 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 13 | var pb ari.PlaybackData 14 | pb.ID = "pb1" 15 | pb.State = "st1" 16 | 17 | m.Playback.On("Data", key).Return(&pb, nil) 18 | 19 | ret, err := cl.Playback().Data(key) 20 | if err != nil { 21 | t.Errorf("Unexpected error in Playback Data: %s", err) 22 | } 23 | if ret == nil { 24 | t.Errorf("Expected Playback data to be non-nil") 25 | } else { 26 | if ret.ID != "pb1" && ret.State != "st1" { 27 | t.Errorf("got '%v', expected '%v'", pb, ret) 28 | } 29 | } 30 | 31 | m.Shutdown() 32 | 33 | m.Playback.AssertCalled(t, "Data", key) 34 | }) 35 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 36 | m.Playback.On("Data", key).Return(nil, errors.New("error")) 37 | 38 | _, err := cl.Playback().Data(key) 39 | if err == nil { 40 | t.Errorf("Expected error in Playback Data: %s", err) 41 | } 42 | 43 | m.Shutdown() 44 | 45 | m.Playback.AssertCalled(t, "Data", key) 46 | }) 47 | } 48 | 49 | func TestPlaybackControl(t *testing.T, s Server) { 50 | key := ari.NewKey(ari.PlaybackKey, "pb1") 51 | 52 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 53 | m.Playback.On("Control", key, "op").Return(nil) 54 | 55 | err := cl.Playback().Control(key, "op") 56 | if err != nil { 57 | t.Errorf("Unexpected error in Playback Control: %s", err) 58 | } 59 | 60 | m.Shutdown() 61 | 62 | m.Playback.AssertCalled(t, "Control", key, "op") 63 | }) 64 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 65 | m.Playback.On("Control", key, "op").Return(errors.New("error")) 66 | 67 | err := cl.Playback().Control(key, "op") 68 | if err == nil { 69 | t.Errorf("Expected error in Playback Control: %s", err) 70 | } 71 | 72 | m.Shutdown() 73 | 74 | m.Playback.AssertCalled(t, "Control", key, "op") 75 | }) 76 | } 77 | 78 | func TestPlaybackStop(t *testing.T, s Server) { 79 | key := ari.NewKey(ari.PlaybackKey, "pb1") 80 | 81 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 82 | m.Playback.On("Stop", key).Return(nil) 83 | 84 | err := cl.Playback().Stop(key) 85 | if err != nil { 86 | t.Errorf("Unexpected error in Playback Stop: %s", err) 87 | } 88 | 89 | m.Shutdown() 90 | 91 | m.Playback.AssertCalled(t, "Stop", key) 92 | }) 93 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 94 | m.Playback.On("Stop", key).Return(errors.New("error")) 95 | 96 | err := cl.Playback().Stop(key) 97 | if err == nil { 98 | t.Errorf("Expected error in Playback Stop: %s", err) 99 | } 100 | 101 | m.Shutdown() 102 | 103 | m.Playback.AssertCalled(t, "Stop", key) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /internal/integration/sound.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/CyCoreSystems/ari/v5" 8 | ) 9 | 10 | func TestSoundData(t *testing.T, s Server) { 11 | key := ari.NewKey(ari.SoundKey, "s1") 12 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 13 | var sd ari.SoundData 14 | sd.ID = "s1" 15 | sd.Text = "text" 16 | 17 | m.Sound.On("Data", key).Return(&sd, nil) 18 | 19 | ret, err := cl.Sound().Data(key) 20 | if err != nil { 21 | t.Errorf("Unexpected error in Sound Data: %s", err) 22 | } 23 | if ret == nil { 24 | t.Errorf("Expected data to be non-nil") 25 | } else { 26 | if ret.ID != sd.ID || ret.Text != sd.Text { 27 | t.Errorf("Expected '%v', got '%v'", sd, ret) 28 | } 29 | } 30 | 31 | m.Shutdown() 32 | 33 | m.Sound.AssertCalled(t, "Data", key) 34 | }) 35 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 36 | m.Sound.On("Data", key).Return(nil, errors.New("error")) 37 | 38 | _, err := cl.Sound().Data(key) 39 | if err == nil { 40 | t.Errorf("Expected error in Sound Data: %s", err) 41 | } 42 | 43 | m.Shutdown() 44 | 45 | m.Sound.AssertCalled(t, "Data", key) 46 | }) 47 | } 48 | 49 | func TestSoundList(t *testing.T, s Server) { 50 | runTest("ok", t, s, func(t *testing.T, m *mock, cl ari.Client) { 51 | sh1 := ari.NewKey(ari.SoundKey, "sh1") 52 | sh2 := ari.NewKey(ari.SoundKey, "sh2") 53 | 54 | var filter map[string]string 55 | 56 | m.Sound.On("List", filter, &ari.Key{Kind: "", ID: "", Node: "", Dialog: "", App: ""}).Return([]*ari.Key{sh1, sh2}, nil) 57 | 58 | ret, err := cl.Sound().List(nil, nil) 59 | if err != nil { 60 | t.Errorf("Unexpected error in Sound List: %s", err) 61 | } 62 | if len(ret) != 2 { 63 | t.Errorf("Expected handle list length to be 2, got '%d'", len(ret)) 64 | } 65 | 66 | m.Shutdown() 67 | 68 | m.Sound.AssertCalled(t, "List", filter, &ari.Key{Kind: "", ID: "", Node: "", Dialog: "", App: ""}) 69 | }) 70 | runTest("err", t, s, func(t *testing.T, m *mock, cl ari.Client) { 71 | var filter map[string]string 72 | 73 | m.Sound.On("List", filter, &ari.Key{Kind: "", ID: "", Node: "", Dialog: "", App: ""}).Return(nil, errors.New("error")) 74 | 75 | ret, err := cl.Sound().List(nil, nil) 76 | if err == nil { 77 | t.Errorf("Expected error in Sound Data: %s", err) 78 | } 79 | if len(ret) != 0 { 80 | t.Errorf("Expected handle list length to be 0, got '%d'", len(ret)) 81 | } 82 | m.Shutdown() 83 | 84 | m.Sound.AssertCalled(t, "List", filter, &ari.Key{Kind: "", ID: "", Node: "", Dialog: "", App: ""}) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var version = "master" 4 | 5 | func main() { 6 | if err := RootCmd.Execute(); err != nil { 7 | Log.Error("server died", "error", err) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /messagebus/messagebus.go: -------------------------------------------------------------------------------- 1 | package messagebus 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 8 | "github.com/CyCoreSystems/ari/v5" 9 | ) 10 | 11 | // DefaultReconnectionAttemts is the default number of reconnection attempts 12 | // It implements a hard coded fault tolerance for a starting NATS cluster 13 | const DefaultReconnectionAttemts = 5 14 | 15 | // DefaultReconnectionWait is the default wating time between each reconnection 16 | // attempt 17 | const DefaultReconnectionWait = 5 * time.Second 18 | 19 | // Type is the type of MessageBus (RabbitMQ / NATS) 20 | type Type int 21 | 22 | // WildcardType used to identify wildcards used on routing keys on message bus 23 | type WildcardType int 24 | 25 | // wildcard types 26 | const ( 27 | WildcardUndefined WildcardType = iota // undefined type 28 | WildcardOneWord // one word like pre.*.post 29 | WildcardZeroOrMoreWords // zero or more words like pre.> 30 | ) 31 | 32 | // types 33 | const ( 34 | TypeUnknown Type = iota // unknown type 35 | TypeNats // NATS type 36 | TypeRabbitmq // RabbitMQ type 37 | ) 38 | 39 | // Server defines the functions used on ari-proxy server 40 | type Server interface { 41 | Connect() error 42 | Close() 43 | 44 | SubscribePing(topic string, callback PingHandler) (Subscription, error) 45 | SubscribeRequest(topic string, callback RequestHandler) (Subscription, error) 46 | SubscribeRequests(topics []string, callback RequestHandler) (Subscription, error) 47 | SubscribeCreateRequest(topic string, queue string, callback RequestHandler) (Subscription, error) 48 | PublishResponse(topic string, msg *proxy.Response) error 49 | PublishAnnounce(topic string, msg *proxy.Announcement) error 50 | PublishEvent(topic string, msg ari.Event) error 51 | } 52 | 53 | // Client defines the functions used on ari-proxy client 54 | type Client interface { 55 | Connect() error 56 | Close() 57 | 58 | SubscribeAnnounce(topic string, callback AnnounceHandler) (Subscription, error) 59 | SubscribeEvent(topic string, queue string, callback EventHandler) (Subscription, error) 60 | 61 | PublishPing(topic string) error 62 | Request(topic string, req *proxy.Request) (*proxy.Response, error) 63 | MultipleRequest(topic string, req *proxy.Request, expectedResp int) ([]*proxy.Response, error) 64 | MultipleRequestReturnFirstGoodResponse(topic string, req *proxy.Request, expectedResp int) (*proxy.Response, error) 65 | 66 | TimeoutCount() int64 67 | GetWildcardString(w WildcardType) string 68 | } 69 | 70 | // Config has general configuration for MessageBus 71 | type Config struct { 72 | URL string 73 | TimeoutRetries int 74 | RequestTimeout time.Duration 75 | } 76 | 77 | // Subscription defines subscription interface 78 | type Subscription interface { 79 | Unsubscribe() error 80 | } 81 | 82 | // RequestHandler handles requests messages 83 | type RequestHandler func(subject string, reply string, req *proxy.Request) 84 | 85 | // ResponseHandler handles response messages 86 | type ResponseHandler func(req *proxy.Response) 87 | 88 | // PingHandler handles ping messages 89 | type PingHandler func() 90 | 91 | // AnnounceHandler handles announce messages 92 | type AnnounceHandler func(o *proxy.Announcement) 93 | 94 | // EventHandler handles event messages 95 | type EventHandler func(b []byte) 96 | 97 | // GetType identifies message bus type from an url 98 | func GetType(url string) Type { 99 | if strings.HasPrefix(url, "amqp://") { 100 | return TypeRabbitmq 101 | } 102 | if strings.HasPrefix(url, "nats://") { 103 | return TypeNats 104 | } 105 | return TypeUnknown 106 | } 107 | -------------------------------------------------------------------------------- /messagebus/nats.go: -------------------------------------------------------------------------------- 1 | package messagebus 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | "github.com/CyCoreSystems/ari/v5" 8 | "github.com/CyCoreSystems/ari/v5/rid" 9 | "github.com/inconshreveable/log15" 10 | "github.com/nats-io/nats.go" 11 | "github.com/rotisserie/eris" 12 | ) 13 | 14 | // NatsBus is MessageBus implementation for RabbitMQ 15 | type NatsBus struct { 16 | Config Config 17 | Log log15.Logger 18 | 19 | conn *nats.EncodedConn 20 | countTimeouts int64 21 | } 22 | 23 | // OptionNatsFunc options for RabbitMQ 24 | type OptionNatsFunc func(n *NatsBus) 25 | 26 | // NatsMSubscription handle multiple subscriptions with same handler 27 | type NatsMSubscription struct { 28 | Subscriptions []*nats.Subscription 29 | } 30 | 31 | // Unsubscribe removes the multiple subscriptions 32 | func (n *NatsMSubscription) Unsubscribe() error { 33 | for _, sub := range n.Subscriptions { 34 | err := sub.Unsubscribe() 35 | if err != nil { 36 | return eris.Wrap(err, "failed to unsubscribe "+sub.Subject) 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | // NewNatsBus creates a NatsBus 43 | func NewNatsBus(config Config, options ...OptionNatsFunc) *NatsBus { 44 | 45 | mbus := NatsBus{ 46 | Config: config, 47 | } 48 | 49 | for _, optfn := range options { 50 | optfn(&mbus) 51 | } 52 | 53 | return &mbus 54 | } 55 | 56 | // WithNatsConn binds an existing NATS connection 57 | func WithNatsConn(nconn *nats.EncodedConn) OptionNatsFunc { 58 | return func(n *NatsBus) { 59 | n.conn = nconn 60 | } 61 | } 62 | 63 | // Connect creates a NATS connection 64 | func (n *NatsBus) Connect() error { 65 | nc, err := nats.Connect(n.Config.URL) 66 | reconnectionAttempts := DefaultReconnectionAttemts 67 | for err == nats.ErrNoServers && reconnectionAttempts > 0 { 68 | n.Log.Info("retrying to connect to NATS server", "attempts", reconnectionAttempts) 69 | time.Sleep(DefaultReconnectionWait) 70 | nc, err = nats.Connect(n.Config.URL) 71 | reconnectionAttempts-- 72 | } 73 | if err != nil { 74 | return eris.Wrap(err, "failed to connect to NATS") 75 | } 76 | n.conn, err = nats.NewEncodedConn(nc, nats.JSON_ENCODER) 77 | if err != nil { 78 | nc.Close() 79 | return eris.Wrap(err, "failed to encode NATS connection") 80 | } 81 | return nil 82 | } 83 | 84 | // SubscribePing subscribe ping messages 85 | func (n *NatsBus) SubscribePing(topic string, callback PingHandler) (Subscription, error) { 86 | return n.conn.Subscribe(topic, func(m *nats.Msg) { 87 | callback() 88 | }) 89 | } 90 | 91 | // SubscribeRequest subscribe request messages 92 | func (n *NatsBus) SubscribeRequest(topic string, callback RequestHandler) (Subscription, error) { 93 | return n.conn.Subscribe(topic, callback) 94 | } 95 | 96 | // SubscribeRequests subscribe request messages using multiple topics 97 | func (n *NatsBus) SubscribeRequests(topics []string, callback RequestHandler) (Subscription, error) { 98 | 99 | subs := NatsMSubscription{} 100 | for _, topic := range topics { 101 | sub, err := n.conn.Subscribe(topic, callback) 102 | if err != nil { 103 | subs.Unsubscribe() // nolint: errcheck 104 | return nil, eris.Wrapf(err, "failed to create %s subscription", topic) 105 | } 106 | subs.Subscriptions = append(subs.Subscriptions, sub) 107 | 108 | } 109 | return &subs, nil 110 | } 111 | 112 | // SubscribeAnnounce subscribe announce messages 113 | func (n *NatsBus) SubscribeAnnounce(topic string, callback AnnounceHandler) (Subscription, error) { 114 | return n.conn.Subscribe(topic, callback) 115 | } 116 | 117 | // SubscribeEvent subscribe event messages 118 | func (n *NatsBus) SubscribeEvent(topic string, queue string, callback EventHandler) (Subscription, error) { 119 | return n.conn.Subscribe(topic, func(m *nats.Msg) { 120 | callback(m.Data) 121 | }) 122 | } 123 | 124 | // SubscribeCreateRequest subscribe create request messages 125 | func (n *NatsBus) SubscribeCreateRequest(topic string, queue string, callback RequestHandler) (Subscription, error) { 126 | return n.conn.QueueSubscribe(topic, queue, callback) 127 | } 128 | 129 | // PublishResponse sends response message 130 | func (n *NatsBus) PublishResponse(topic string, msg *proxy.Response) error { 131 | return n.conn.Publish(topic, msg) 132 | } 133 | 134 | // PublishPing sends ping message 135 | func (n *NatsBus) PublishPing(topic string) error { 136 | return n.conn.Publish(topic, &proxy.Request{}) 137 | } 138 | 139 | // PublishAnnounce sends announce message 140 | func (n *NatsBus) PublishAnnounce(topic string, msg *proxy.Announcement) error { 141 | return n.conn.Publish(topic, msg) 142 | } 143 | 144 | // PublishEvent sends event message 145 | func (n *NatsBus) PublishEvent(topic string, msg ari.Event) error { 146 | return n.conn.Publish(topic, msg) 147 | } 148 | 149 | // Close closes the connection 150 | func (n *NatsBus) Close() { 151 | if n.conn != nil { 152 | n.conn.Close() 153 | } 154 | } 155 | 156 | // GetWildcardString returns wildcard based on type 157 | func (n *NatsBus) GetWildcardString(w WildcardType) string { 158 | switch w { 159 | case WildcardOneWord: 160 | return "*" 161 | case WildcardZeroOrMoreWords: 162 | return ">" 163 | } 164 | return "" 165 | } 166 | 167 | // Request sends a request message 168 | func (n *NatsBus) Request(topic string, req *proxy.Request) (*proxy.Response, error) { 169 | var err error 170 | var resp proxy.Response 171 | for i := 0; i <= n.Config.TimeoutRetries; i++ { 172 | err = n.conn.Request(topic, req, &resp, n.Config.RequestTimeout) 173 | if err == nats.ErrTimeout { 174 | n.countTimeouts++ 175 | continue 176 | } 177 | if err != nil { 178 | return nil, err 179 | } 180 | return &resp, nil 181 | } 182 | return nil, err 183 | } 184 | 185 | // MultipleRequest sends a request message to multiple consumers 186 | func (n *NatsBus) MultipleRequest(topic string, req *proxy.Request, expectedResp int) ([]*proxy.Response, error) { 187 | var responses []*proxy.Response 188 | 189 | reply := rid.New("rp") 190 | 191 | rf := &responseForwarder{ 192 | expected: expectedResp, 193 | fwdChan: make(chan *proxy.Response), 194 | } 195 | 196 | replySub, err := n.conn.Subscribe(reply, rf.Forward) 197 | if err != nil { 198 | return nil, eris.Wrap(err, "failed to subscribe to data responses") 199 | } 200 | defer replySub.Unsubscribe() // nolint: errcheck 201 | 202 | // Make an all-call for the entity data 203 | err = n.conn.PublishRequest(topic, reply, req) 204 | if err != nil { 205 | return nil, eris.Wrap(err, "failed to make request for data") 206 | } 207 | 208 | // Wait for replies 209 | timer := time.NewTimer(n.Config.RequestTimeout) 210 | defer timer.Stop() 211 | for { 212 | select { 213 | case <-timer.C: 214 | return responses, nil 215 | case resp, more := <-rf.fwdChan: 216 | if !more { 217 | return responses, nil 218 | } 219 | responses = append(responses, resp) 220 | } 221 | } 222 | } 223 | 224 | // MultipleRequestReturnFirstGoodResponse sends a request message to multiple consumers and returns the first good response 225 | func (n *NatsBus) MultipleRequestReturnFirstGoodResponse(topic string, req *proxy.Request, expectedResp int) (*proxy.Response, error) { 226 | 227 | reply := rid.New("rp") 228 | 229 | rf := &responseForwarder{ 230 | expected: expectedResp, 231 | fwdChan: make(chan *proxy.Response), 232 | } 233 | 234 | replySub, err := n.conn.Subscribe(reply, rf.Forward) 235 | if err != nil { 236 | return nil, eris.Wrap(err, "failed to subscribe to data responses") 237 | } 238 | defer replySub.Unsubscribe() // nolint: errcheck 239 | 240 | // Make an all-call for the entity data 241 | err = n.conn.PublishRequest(topic, reply, req) 242 | if err != nil { 243 | return nil, eris.Wrap(err, "failed to make request for data") 244 | } 245 | 246 | // Wait for replies 247 | timer := time.NewTimer(n.Config.RequestTimeout) 248 | defer timer.Stop() 249 | for { 250 | select { 251 | case <-timer.C: 252 | // Return the last error if we got one; otherwise, return a timeout error 253 | if err == nil { 254 | err = eris.New("timeout") 255 | } 256 | 257 | return nil, err 258 | case resp, more := <-rf.fwdChan: 259 | if !more { 260 | if err == nil { 261 | err = eris.New("no data") 262 | } 263 | 264 | return nil, err 265 | } 266 | if resp != nil { 267 | if err = resp.Err(); err == nil { // store the error for later return 268 | return resp, nil // No error means to return the current value 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | // TimeoutCount is the amount of times the communication times out 276 | func (n *NatsBus) TimeoutCount() int64 { 277 | return n.countTimeouts 278 | } 279 | -------------------------------------------------------------------------------- /messagebus/rabbitmqsub.go: -------------------------------------------------------------------------------- 1 | package messagebus 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/CyCoreSystems/ari/v5/rid" 8 | "github.com/rabbitmq/amqp091-go" 9 | ) 10 | 11 | // RmqSubscription handle RabbitMQ subscription 12 | type RmqSubscription struct { 13 | consumerID string 14 | channel *amqp091.Channel 15 | mu sync.RWMutex 16 | 17 | Topics []string 18 | Queue string 19 | Exchange string 20 | ExchangeKind string 21 | QueueArgs amqp091.Table 22 | } 23 | 24 | // Unsubscribe remove the subscription 25 | func (rs *RmqSubscription) Unsubscribe() error { 26 | rs.mu.RLock() 27 | defer rs.mu.RUnlock() 28 | if rs.consumerID == "" { 29 | return nil 30 | } 31 | err := rs.channel.Cancel(rs.consumerID, false) 32 | if err != nil { 33 | return err 34 | } 35 | return rs.channel.Close() 36 | } 37 | 38 | // reconnect reconnects the subscription 39 | func (rs *RmqSubscription) reconnect(r *RabbitmqBus) <-chan amqp091.Delivery { 40 | rs.mu.Lock() 41 | 42 | if rs.channel != nil { 43 | rs.channel.Close() // nolint: errcheck 44 | } 45 | rs.mu.Unlock() 46 | 47 | for { 48 | msgs, err := rs.execute(r) 49 | if err != nil { 50 | r.Log.Error("failed to execute subscription", "error", err) 51 | time.Sleep(DefaultReconnectionWait) 52 | continue 53 | } 54 | return msgs 55 | } 56 | } 57 | 58 | // execute declares the subscription on RabbitMQ 59 | func (rs *RmqSubscription) execute(r *RabbitmqBus) (<-chan amqp091.Delivery, error) { 60 | rs.mu.Lock() 61 | defer rs.mu.Unlock() 62 | 63 | ch, err := r.newChannel() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | rs.channel = ch 69 | queue, err := ch.QueueDeclare( 70 | rs.Queue, // name of queue 71 | true, // durable 72 | false, // delete when unused 73 | false, // exclusive 74 | false, // nowait 75 | rs.QueueArgs, // arguments 76 | ) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | err = ch.ExchangeDeclare( 82 | rs.Exchange, // name of exchange 83 | rs.ExchangeKind, // kind 84 | true, // durable 85 | false, // delete when unused 86 | false, // internal 87 | false, // nowait 88 | nil, // arguments 89 | ) 90 | if err != nil { 91 | return nil, err 92 | } 93 | if queue.Name != rs.Queue { 94 | rs.Queue = queue.Name 95 | } 96 | 97 | for _, topic := range rs.Topics { 98 | err = ch.QueueBind(queue.Name, topic, rs.Exchange, false, nil) 99 | if err != nil { 100 | return nil, err 101 | } 102 | } 103 | 104 | rs.consumerID = rid.New(ridConsumer) 105 | return ch.Consume(rs.Queue, rs.consumerID, false, false, true, true, nil) 106 | } 107 | -------------------------------------------------------------------------------- /messagebus/response_forwarder.go: -------------------------------------------------------------------------------- 1 | package messagebus 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | type responseForwarder struct { 10 | closed bool 11 | count int 12 | expected int 13 | fwdChan chan *proxy.Response 14 | 15 | mu sync.Mutex 16 | } 17 | 18 | func (f *responseForwarder) Forward(o *proxy.Response) { 19 | 20 | f.mu.Lock() 21 | defer f.mu.Unlock() 22 | 23 | f.count++ 24 | 25 | if f.closed { 26 | return 27 | } 28 | 29 | // always send up reply, so we can track errors. 30 | select { 31 | case f.fwdChan <- o: 32 | default: 33 | } 34 | 35 | if f.count >= f.expected { 36 | f.closed = true 37 | close(f.fwdChan) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /proxy/subjects.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import "fmt" 4 | 5 | // Subject returns the communication subject for the given parameters 6 | func Subject(prefix, class, appName, asterisk string) (ret string) { 7 | ret = fmt.Sprintf("%s%s", prefix, class) 8 | if appName != "" { 9 | ret += "." + appName 10 | if asterisk != "" { 11 | ret += "." + asterisk 12 | } 13 | } 14 | return 15 | } 16 | -------------------------------------------------------------------------------- /server/application.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 9 | ) 10 | 11 | func (s *Server) applicationData(ctx context.Context, reply string, req *proxy.Request) { 12 | data, err := s.ari.Application().Data(req.Key) 13 | if err != nil { 14 | s.sendError(reply, err) 15 | return 16 | } 17 | 18 | s.publish(reply, &proxy.Response{ 19 | Data: &proxy.EntityData{ 20 | Application: data, 21 | }, 22 | }) 23 | } 24 | 25 | func (s *Server) applicationList(ctx context.Context, reply string, req *proxy.Request) { 26 | list, err := s.ari.Application().List(nil) 27 | if err != nil { 28 | s.sendError(reply, err) 29 | return 30 | } 31 | 32 | s.publish(reply, &proxy.Response{ 33 | Keys: list, 34 | }) 35 | } 36 | 37 | func (s *Server) applicationGet(ctx context.Context, reply string, req *proxy.Request) { 38 | data, err := s.ari.Application().Data(req.Key) 39 | if err != nil { 40 | s.sendError(reply, err) 41 | return 42 | } 43 | 44 | s.publish(reply, &proxy.Response{ 45 | Key: data.Key, 46 | }) 47 | } 48 | 49 | func parseEventSource(src string) (string, string, error) { 50 | var err error 51 | 52 | pieces := strings.Split(src, ":") 53 | if len(pieces) != 2 { 54 | return "", "", errors.New("Invalid EventSource") 55 | } 56 | 57 | switch pieces[0] { 58 | case "channel": 59 | case "bridge": 60 | case "endpoint": 61 | case "deviceState": 62 | default: 63 | err = errors.New("Unhandled EventSource type") 64 | } 65 | return pieces[0], pieces[1], err 66 | } 67 | 68 | func (s *Server) applicationSubscribe(ctx context.Context, reply string, req *proxy.Request) { 69 | err := s.ari.Application().Subscribe(req.Key, req.ApplicationSubscribe.EventSource) 70 | if err != nil { 71 | s.sendError(reply, err) 72 | return 73 | } 74 | 75 | if req.Key.Dialog != "" { 76 | eType, eID, err := parseEventSource(req.ApplicationSubscribe.EventSource) 77 | if err != nil { 78 | s.Log.Warn("failed to parse event source", "error", err, "eventsource", req.ApplicationSubscribe.EventSource) 79 | } else { 80 | s.Dialog.Bind(req.Key.Dialog, eType, eID) 81 | } 82 | } 83 | 84 | s.sendError(reply, nil) 85 | } 86 | 87 | func (s *Server) applicationUnsubscribe(ctx context.Context, reply string, req *proxy.Request) { 88 | s.sendError(reply, s.ari.Application().Unsubscribe(req.Key, req.ApplicationSubscribe.EventSource)) 89 | } 90 | -------------------------------------------------------------------------------- /server/application_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestApplicationList(t *testing.T) { 10 | integration.TestApplicationList(t, &srv{}) 11 | } 12 | 13 | func TestApplicationData(t *testing.T) { 14 | integration.TestApplicationData(t, &srv{}) 15 | } 16 | 17 | func TestApplicationSubscribe(t *testing.T) { 18 | integration.TestApplicationSubscribe(t, &srv{}) 19 | } 20 | 21 | func TestApplicationUnsubscribe(t *testing.T) { 22 | integration.TestApplicationUnsubscribe(t, &srv{}) 23 | } 24 | 25 | func TestApplicationGet(t *testing.T) { 26 | integration.TestApplicationGet(t, &srv{}) 27 | } 28 | -------------------------------------------------------------------------------- /server/asterisk.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) asteriskInfo(ctx context.Context, reply string, req *proxy.Request) { 10 | data, err := s.ari.Asterisk().Info(req.Key) 11 | if err != nil { 12 | s.sendError(reply, err) 13 | return 14 | } 15 | 16 | s.publish(reply, &proxy.Response{ 17 | Data: &proxy.EntityData{ 18 | Asterisk: data, 19 | }, 20 | }) 21 | } 22 | 23 | func (s *Server) asteriskVariableGet(ctx context.Context, reply string, req *proxy.Request) { 24 | val, err := s.ari.Asterisk().Variables().Get(req.Key) 25 | if err != nil { 26 | s.sendError(reply, err) 27 | return 28 | } 29 | 30 | s.publish(reply, &proxy.Response{ 31 | Data: &proxy.EntityData{ 32 | Variable: val, 33 | }, 34 | }) 35 | } 36 | 37 | func (s *Server) asteriskVariableSet(ctx context.Context, reply string, req *proxy.Request) { 38 | err := s.ari.Asterisk().Variables().Set(req.Key, req.AsteriskVariableSet.Value) 39 | if err != nil { 40 | s.sendError(reply, err) 41 | return 42 | } 43 | 44 | s.sendError(reply, nil) 45 | } 46 | -------------------------------------------------------------------------------- /server/asterisk_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestAsteriskInfo(t *testing.T) { 10 | integration.TestAsteriskInfo(t, &srv{}) 11 | } 12 | 13 | func TestAsteriskVariablesGet(t *testing.T) { 14 | integration.TestAsteriskVariablesGet(t, &srv{}) 15 | } 16 | 17 | func TestAsteriskVariablesSet(t *testing.T) { 18 | integration.TestAsteriskVariablesSet(t, &srv{}) 19 | } 20 | -------------------------------------------------------------------------------- /server/bridge.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | "github.com/CyCoreSystems/ari/v5" 8 | "github.com/CyCoreSystems/ari/v5/rid" 9 | ) 10 | 11 | func (s *Server) bridgeAddChannel(ctx context.Context, reply string, req *proxy.Request) { 12 | channel := req.BridgeAddChannel.Channel 13 | 14 | // bind dialog 15 | if req.Key.Dialog != "" { 16 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 17 | s.Dialog.Bind(req.Key.Dialog, "channel", channel) 18 | } 19 | 20 | err := s.ari.Bridge().AddChannel(req.Key, channel) 21 | if err != nil { 22 | s.sendError(reply, err) 23 | return 24 | } 25 | 26 | s.sendError(reply, nil) 27 | } 28 | 29 | func (s *Server) bridgeCreate(ctx context.Context, reply string, req *proxy.Request) { 30 | // bind dialog 31 | if req.Key.Dialog != "" { 32 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 33 | } 34 | 35 | h, err := s.ari.Bridge().Create(req.Key, req.BridgeCreate.Type, req.BridgeCreate.Name) 36 | if err != nil { 37 | s.sendError(reply, err) 38 | return 39 | } 40 | 41 | s.publish(reply, &proxy.Response{ 42 | Key: h.Key(), 43 | }) 44 | } 45 | 46 | func (s *Server) bridgeStageCreate(ctx context.Context, reply string, req *proxy.Request) { 47 | bh := s.ari.Bridge().Get(req.Key) 48 | 49 | if req.Key.Dialog != "" { 50 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 51 | } 52 | 53 | s.publish(reply, &proxy.Response{ 54 | Key: bh.Key(), 55 | }) 56 | } 57 | 58 | func (s *Server) bridgeData(ctx context.Context, reply string, req *proxy.Request) { 59 | bd, err := s.ari.Bridge().Data(req.Key) 60 | if err != nil { 61 | s.sendError(reply, err) 62 | return 63 | } 64 | 65 | s.publish(reply, &proxy.Response{ 66 | Data: &proxy.EntityData{ 67 | Bridge: bd, 68 | }, 69 | }) 70 | } 71 | 72 | func (s *Server) bridgeDelete(ctx context.Context, reply string, req *proxy.Request) { 73 | // bind dialog 74 | if req.Key.Dialog != "" { 75 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 76 | } 77 | 78 | err := s.ari.Bridge().Delete(req.Key) 79 | if err != nil { 80 | s.sendError(reply, err) 81 | return 82 | } 83 | 84 | s.sendError(reply, nil) 85 | } 86 | 87 | func (s *Server) bridgeGet(ctx context.Context, reply string, req *proxy.Request) { 88 | data, err := s.ari.Bridge().Data(req.Key) 89 | if err != nil { 90 | s.sendError(reply, err) 91 | return 92 | } 93 | 94 | if req.Key.Dialog != "" { 95 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 96 | } 97 | 98 | s.publish(reply, &proxy.Response{ 99 | Key: data.Key, 100 | }) 101 | } 102 | 103 | func (s *Server) bridgeList(ctx context.Context, reply string, req *proxy.Request) { 104 | list, err := s.ari.Bridge().List(nil) 105 | if err != nil { 106 | s.sendError(reply, err) 107 | return 108 | } 109 | 110 | s.publish(reply, &proxy.Response{ 111 | Keys: list, 112 | }) 113 | } 114 | 115 | func (s *Server) bridgeMOH(ctx context.Context, reply string, req *proxy.Request) { 116 | // bind dialog 117 | if req.Key.Dialog != "" { 118 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 119 | } 120 | 121 | s.sendError( 122 | reply, 123 | s.ari.Bridge().MOH(req.Key, req.BridgeMOH.Class), 124 | ) 125 | } 126 | 127 | func (s *Server) bridgeStopMOH(ctx context.Context, reply string, req *proxy.Request) { 128 | // bind dialog 129 | if req.Key.Dialog != "" { 130 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 131 | } 132 | 133 | s.sendError( 134 | reply, 135 | s.ari.Bridge().StopMOH(req.Key), 136 | ) 137 | } 138 | 139 | func (s *Server) bridgePlay(ctx context.Context, reply string, req *proxy.Request) { 140 | // bind dialog 141 | if req.Key.Dialog != "" { 142 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 143 | s.Dialog.Bind(req.Key.Dialog, "playback", req.BridgePlay.PlaybackID) 144 | } 145 | 146 | ph, err := s.ari.Bridge().Play(req.Key, req.BridgePlay.PlaybackID, req.BridgePlay.MediaURI) 147 | if err != nil { 148 | s.sendError(reply, err) 149 | return 150 | } 151 | 152 | s.publish(reply, &proxy.Response{ 153 | Key: ph.Key(), 154 | }) 155 | } 156 | 157 | func (s *Server) bridgeStagePlay(ctx context.Context, reply string, req *proxy.Request) { 158 | data, err := s.ari.Bridge().Data(req.Key) 159 | if err != nil || data == nil { 160 | s.sendError(reply, err) 161 | return 162 | } 163 | 164 | if req.BridgePlay.PlaybackID == "" { 165 | req.BridgePlay.PlaybackID = rid.New(rid.Playback) 166 | } 167 | 168 | // bind dialog 169 | if req.Key.Dialog != "" { 170 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 171 | s.Dialog.Bind(req.Key.Dialog, "playback", req.BridgePlay.PlaybackID) 172 | } 173 | 174 | s.publish(reply, &proxy.Response{ 175 | Key: s.ari.Playback().Get(ari.NewKey(ari.PlaybackKey, req.BridgePlay.PlaybackID)).Key(), 176 | }) 177 | } 178 | 179 | func (s *Server) bridgeRecord(ctx context.Context, reply string, req *proxy.Request) { 180 | data, err := s.ari.Bridge().Data(req.Key) 181 | if err != nil || data == nil { 182 | s.sendError(reply, err) 183 | return 184 | } 185 | 186 | if req.BridgeRecord.Name == "" { 187 | req.BridgeRecord.Name = rid.New(rid.Recording) 188 | } 189 | 190 | // bind dialog 191 | if req.Key.Dialog != "" { 192 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 193 | s.Dialog.Bind(req.Key.Dialog, "recording", req.BridgeRecord.Name) 194 | } 195 | 196 | h, err := s.ari.Bridge().Record(req.Key, req.BridgeRecord.Name, req.BridgeRecord.Options) 197 | if err != nil { 198 | s.sendError(reply, err) 199 | return 200 | } 201 | 202 | s.publish(reply, &proxy.Response{ 203 | Key: h.Key(), 204 | }) 205 | } 206 | 207 | func (s *Server) bridgeStageRecord(ctx context.Context, reply string, req *proxy.Request) { 208 | data, err := s.ari.Bridge().Data(req.Key) 209 | if err != nil || data == nil { 210 | s.sendError(reply, err) 211 | return 212 | } 213 | 214 | if req.BridgeRecord.Name == "" { 215 | req.BridgeRecord.Name = rid.New(rid.Recording) 216 | } 217 | 218 | if req.Key.Dialog != "" { 219 | s.Dialog.Bind(req.Key.Dialog, "bridge", data.ID) 220 | s.Dialog.Bind(req.Key.Dialog, "recording", req.BridgeRecord.Name) 221 | } 222 | 223 | s.publish(reply, &proxy.Response{ 224 | Key: data.Key.New(ari.LiveRecordingKey, req.BridgeRecord.Name), 225 | }) 226 | } 227 | 228 | func (s *Server) bridgeRemoveChannel(ctx context.Context, reply string, req *proxy.Request) { 229 | // bind dialog 230 | if req.Key.Dialog != "" { 231 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 232 | s.Dialog.Bind(req.Key.Dialog, "channel", req.BridgeRemoveChannel.Channel) 233 | } 234 | 235 | err := s.ari.Bridge().RemoveChannel(req.Key, req.BridgeRemoveChannel.Channel) 236 | if err != nil { 237 | s.sendError(reply, err) 238 | return 239 | } 240 | 241 | s.sendError(reply, nil) 242 | } 243 | 244 | func (s *Server) bridgeSubscribe(ctx context.Context, reply string, req *proxy.Request) { 245 | // bind dialog 246 | if req.Key.Dialog != "" { 247 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 248 | } 249 | 250 | s.sendError(reply, nil) 251 | } 252 | 253 | func (s *Server) bridgeUnsubscribe(ctx context.Context, reply string, req *proxy.Request) { 254 | // no-op for now; may want to eventually optimize away the dialog subscription 255 | s.sendError(reply, nil) 256 | } 257 | 258 | func (s *Server) bridgeVideoSource(ctx context.Context, reply string, req *proxy.Request) { 259 | // bind dialog 260 | if req.Key.Dialog != "" { 261 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 262 | s.Dialog.Bind(req.Key.Dialog, "channel", req.BridgeVideoSource.Channel) 263 | } 264 | 265 | err := s.ari.Bridge().VideoSource(req.Key, req.BridgeVideoSource.Channel) 266 | if err != nil { 267 | s.sendError(reply, err) 268 | return 269 | } 270 | 271 | s.sendError(reply, nil) 272 | } 273 | 274 | func (s *Server) bridgeVideoSourceDelete(ctx context.Context, reply string, req *proxy.Request) { 275 | // bind dialog 276 | if req.Key.Dialog != "" { 277 | s.Dialog.Bind(req.Key.Dialog, "bridge", req.Key.ID) 278 | } 279 | 280 | err := s.ari.Bridge().VideoSourceDelete(req.Key) 281 | if err != nil { 282 | s.sendError(reply, err) 283 | return 284 | } 285 | 286 | s.sendError(reply, nil) 287 | } 288 | -------------------------------------------------------------------------------- /server/bridge_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestBridgeCreate(t *testing.T) { 10 | integration.TestBridgeCreate(t, &srv{}) 11 | } 12 | 13 | func TestBridgeList(t *testing.T) { 14 | integration.TestBridgeList(t, &srv{}) 15 | } 16 | 17 | func TestBridgeData(t *testing.T) { 18 | integration.TestBridgeData(t, &srv{}) 19 | } 20 | 21 | func TestBridgeAddChannel(t *testing.T) { 22 | integration.TestBridgeAddChannel(t, &srv{}) 23 | } 24 | 25 | func TestBridgeRemoveChannel(t *testing.T) { 26 | integration.TestBridgeRemoveChannel(t, &srv{}) 27 | } 28 | 29 | func TestBridgeDelete(t *testing.T) { 30 | integration.TestBridgeDelete(t, &srv{}) 31 | } 32 | 33 | func TestBridgePlay(t *testing.T) { 34 | integration.TestBridgePlay(t, &srv{}) 35 | } 36 | 37 | func TestBridgeRecord(t *testing.T) { 38 | integration.TestBridgeRecord(t, &srv{}) 39 | } 40 | -------------------------------------------------------------------------------- /server/channel_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestChannelData(t *testing.T) { 10 | integration.TestChannelData(t, &srv{}) 11 | } 12 | 13 | func TestChannelAnswer(t *testing.T) { 14 | integration.TestChannelAnswer(t, &srv{}) 15 | } 16 | 17 | func TestChannelBusy(t *testing.T) { 18 | integration.TestChannelBusy(t, &srv{}) 19 | } 20 | 21 | func TestChannelCongestion(t *testing.T) { 22 | integration.TestChannelCongestion(t, &srv{}) 23 | } 24 | 25 | func TestChannelHangup(t *testing.T) { 26 | integration.TestChannelHangup(t, &srv{}) 27 | } 28 | 29 | func TestChannelList(t *testing.T) { 30 | integration.TestChannelList(t, &srv{}) 31 | } 32 | 33 | func TestChannelMute(t *testing.T) { 34 | integration.TestChannelMute(t, &srv{}) 35 | } 36 | 37 | func TestChannelUnmute(t *testing.T) { 38 | integration.TestChannelUnmute(t, &srv{}) 39 | } 40 | 41 | func TestChannelMOH(t *testing.T) { 42 | integration.TestChannelMOH(t, &srv{}) 43 | } 44 | 45 | func TestChannelStopMOH(t *testing.T) { 46 | integration.TestChannelStopMOH(t, &srv{}) 47 | } 48 | 49 | func TestChannelCreate(t *testing.T) { 50 | integration.TestChannelCreate(t, &srv{}) 51 | } 52 | 53 | func TestChannelContinue(t *testing.T) { 54 | integration.TestChannelContinue(t, &srv{}) 55 | } 56 | 57 | func TestChannelDial(t *testing.T) { 58 | integration.TestChannelDial(t, &srv{}) 59 | } 60 | 61 | func TestChannelHold(t *testing.T) { 62 | integration.TestChannelHold(t, &srv{}) 63 | } 64 | 65 | func TestChannelStopHold(t *testing.T) { 66 | integration.TestChannelStopHold(t, &srv{}) 67 | } 68 | 69 | func TestChannelRing(t *testing.T) { 70 | integration.TestChannelRing(t, &srv{}) 71 | } 72 | 73 | func TestChannelStopRing(t *testing.T) { 74 | integration.TestChannelStopRing(t, &srv{}) 75 | } 76 | 77 | func TestChannelSilence(t *testing.T) { 78 | integration.TestChannelSilence(t, &srv{}) 79 | } 80 | 81 | func TestChannelStopSilence(t *testing.T) { 82 | integration.TestChannelStopSilence(t, &srv{}) 83 | } 84 | 85 | func TestChannelOriginate(t *testing.T) { 86 | integration.TestChannelOriginate(t, &srv{}) 87 | } 88 | 89 | func TestChannelPlay(t *testing.T) { 90 | integration.TestChannelPlay(t, &srv{}) 91 | } 92 | 93 | func TestChannelRecord(t *testing.T) { 94 | integration.TestChannelRecord(t, &srv{}) 95 | } 96 | 97 | func TestChannelSnoop(t *testing.T) { 98 | integration.TestChannelSnoop(t, &srv{}) 99 | } 100 | 101 | func TestChannelExternalMedia(t *testing.T) { 102 | integration.TestChannelExternalMedia(t, &srv{}) 103 | } 104 | 105 | func TestChannelSendDTMF(t *testing.T) { 106 | integration.TestChannelSendDTMF(t, &srv{}) 107 | } 108 | 109 | func TestChannelVariableGet(t *testing.T) { 110 | integration.TestChannelVariableGet(t, &srv{}) 111 | } 112 | 113 | func TestChannelVariableSet(t *testing.T) { 114 | integration.TestChannelVariableSet(t, &srv{}) 115 | } 116 | -------------------------------------------------------------------------------- /server/clientserver_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | "time" 9 | 10 | "github.com/CyCoreSystems/ari-proxy/v5/client" 11 | "github.com/CyCoreSystems/ari/v5" 12 | "github.com/CyCoreSystems/ari/v5/rid" 13 | "github.com/nats-io/nats.go" 14 | ) 15 | 16 | type srv struct { 17 | s *Server 18 | } 19 | 20 | func (s *srv) Start(ctx context.Context, t *testing.T, mockClient ari.Client, nc *nats.EncodedConn, completeCh chan struct{}) (ari.Client, error) { 21 | s.s = New() 22 | // tests may run in parallel so we don't want two separate proxy servers to conflict. 23 | s.s.MBPrefix = rid.New("") + "." 24 | s.s.Application = "asdf" 25 | 26 | go func() { 27 | if err := s.s.ListenOn(ctx, mockClient, nc); err != nil { 28 | if err != context.Canceled { 29 | t.Errorf("Failed to start server: %s", err) 30 | } 31 | } 32 | close(completeCh) 33 | }() 34 | 35 | select { 36 | case <-s.s.Ready(): 37 | case <-time.After(600 * time.Millisecond): 38 | return nil, errors.New("Timeout waiting for server ready") 39 | } 40 | 41 | cl, err := client.New(ctx, client.WithTimeoutRetries(4), client.WithPrefix(s.s.MBPrefix), client.WithApplication("asdf")) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return cl, nil 47 | } 48 | 49 | func (s *srv) Ready() <-chan struct{} { 50 | return s.s.Ready() 51 | } 52 | 53 | func (s *srv) Close() error { 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /server/closegroup.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | type closeGroup struct { 4 | count int 5 | closeCh chan struct{} 6 | } 7 | 8 | func (cg *closeGroup) Add(fn func() error) func() { 9 | if cg.count == 0 { 10 | cg.closeCh = make(chan struct{}) 11 | } 12 | cg.count++ 13 | return func() { 14 | err := fn() 15 | if err != nil { 16 | panic(err.Error()) 17 | } 18 | cg.count-- 19 | if cg.count == 0 { 20 | close(cg.closeCh) 21 | cg.count = -1 22 | } 23 | } 24 | } 25 | 26 | func (cg *closeGroup) Done() chan struct{} { 27 | return cg.closeCh 28 | } 29 | -------------------------------------------------------------------------------- /server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) asteriskConfigData(ctx context.Context, reply string, req *proxy.Request) { 10 | data, err := s.ari.Asterisk().Config().Data(req.Key) 11 | if err != nil { 12 | s.sendError(reply, err) 13 | return 14 | } 15 | 16 | s.publish(reply, &proxy.Response{ 17 | Data: &proxy.EntityData{ 18 | Config: data, 19 | }, 20 | }) 21 | } 22 | 23 | func (s *Server) asteriskConfigDelete(ctx context.Context, reply string, req *proxy.Request) { 24 | s.sendError(reply, s.ari.Asterisk().Config().Delete(req.Key)) 25 | } 26 | 27 | func (s *Server) asteriskConfigUpdate(ctx context.Context, reply string, req *proxy.Request) { 28 | s.sendError(reply, s.ari.Asterisk().Config().Update(req.Key, req.AsteriskConfig.Tuples)) 29 | } 30 | -------------------------------------------------------------------------------- /server/config_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestConfigData(t *testing.T) { 10 | integration.TestConfigData(t, &srv{}) 11 | } 12 | 13 | func TestConfigDelete(t *testing.T) { 14 | integration.TestConfigDelete(t, &srv{}) 15 | } 16 | 17 | func TestConfigUpdate(t *testing.T) { 18 | integration.TestConfigUpdate(t, &srv{}) 19 | } 20 | -------------------------------------------------------------------------------- /server/device.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) deviceStateData(ctx context.Context, reply string, req *proxy.Request) { 10 | data, err := s.ari.DeviceState().Data(req.Key) 11 | if err != nil { 12 | s.sendError(reply, err) 13 | return 14 | } 15 | 16 | s.publish(reply, &proxy.Response{ 17 | Data: &proxy.EntityData{ 18 | DeviceState: data, 19 | }, 20 | }) 21 | } 22 | 23 | func (s *Server) deviceStateGet(ctx context.Context, reply string, req *proxy.Request) { 24 | data, err := s.ari.DeviceState().Data(req.Key) 25 | if err != nil { 26 | s.sendError(reply, err) 27 | return 28 | } 29 | 30 | s.publish(reply, &proxy.Response{ 31 | Key: data.Key, 32 | }) 33 | } 34 | 35 | func (s *Server) deviceStateDelete(ctx context.Context, reply string, req *proxy.Request) { 36 | s.sendError(reply, s.ari.DeviceState().Delete(req.Key)) 37 | } 38 | 39 | func (s *Server) deviceStateList(ctx context.Context, reply string, req *proxy.Request) { 40 | list, err := s.ari.DeviceState().List(nil) 41 | if err != nil { 42 | s.sendError(reply, err) 43 | return 44 | } 45 | 46 | s.publish(reply, &proxy.Response{ 47 | Keys: list, 48 | }) 49 | } 50 | 51 | func (s *Server) deviceStateUpdate(ctx context.Context, reply string, req *proxy.Request) { 52 | s.sendError(reply, s.ari.DeviceState().Update(req.Key, req.DeviceStateUpdate.State)) 53 | } 54 | -------------------------------------------------------------------------------- /server/device_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestDeviceData(t *testing.T) { 10 | integration.TestDeviceData(t, &srv{}) 11 | } 12 | 13 | func TestDeviceDelete(t *testing.T) { 14 | integration.TestDeviceDelete(t, &srv{}) 15 | } 16 | 17 | func TestDeviceUpdate(t *testing.T) { 18 | integration.TestDeviceUpdate(t, &srv{}) 19 | } 20 | 21 | func TestDeviceList(t *testing.T) { 22 | integration.TestDeviceList(t, &srv{}) 23 | } 24 | -------------------------------------------------------------------------------- /server/dialog/manager.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import "sync" 4 | 5 | // Manager is a dialog manager, which tracks associations between dialogs and entities 6 | type Manager interface { 7 | // List returns a list of dialogs for the given entity type-ID pair 8 | List(eType, id string) []string 9 | 10 | // Bind binds the given dialog to an entity type-ID pair 11 | Bind(dialog, eType, id string) 12 | 13 | // Unbind removes bindings for the given entity type-ID pair 14 | Unbind(eType, id string) 15 | 16 | // UnbindDialog removes all bindings for the given dialog 17 | UnbindDialog(dialog string) 18 | } 19 | 20 | func bindingHash(eType, id string) string { 21 | return eType + ":" + id 22 | } 23 | 24 | type memManager struct { 25 | bindings map[string][]string 26 | 27 | mu sync.RWMutex 28 | } 29 | 30 | // NewMemManager returns a new in-memory dialog manager 31 | func NewMemManager() Manager { 32 | return &memManager{ 33 | bindings: make(map[string][]string), 34 | } 35 | } 36 | 37 | func (m *memManager) List(eType, id string) []string { 38 | m.mu.RLock() 39 | defer m.mu.RUnlock() 40 | 41 | list, ok := m.bindings[bindingHash(eType, id)] 42 | if !ok { 43 | return nil 44 | } 45 | return list 46 | } 47 | 48 | func (m *memManager) Bind(dialog, eType, id string) { 49 | if dialog == "" || eType == "" || id == "" { 50 | return 51 | } 52 | 53 | m.mu.Lock() 54 | defer m.mu.Unlock() 55 | 56 | list, ok := m.bindings[bindingHash(eType, id)] 57 | if !ok { 58 | list = []string{dialog} 59 | } else { 60 | // Don't add the binding if it is already there 61 | for _, b := range list { 62 | if b == dialog { 63 | return 64 | } 65 | } 66 | 67 | // Add the dialog 68 | list = append(list, dialog) 69 | } 70 | m.bindings[bindingHash(eType, id)] = list 71 | } 72 | 73 | func (m *memManager) Unbind(eType, id string) { 74 | m.mu.Lock() 75 | delete(m.bindings, bindingHash(eType, id)) 76 | m.mu.Unlock() 77 | } 78 | 79 | func (m *memManager) UnbindDialog(dialog string) { 80 | 81 | var dialogIndex int 82 | 83 | m.mu.Lock() 84 | for k, v := range m.bindings { 85 | dialogIndex = -1 86 | for i, d := range v { 87 | if d == dialog { 88 | dialogIndex = i 89 | } 90 | } 91 | if dialogIndex >= 0 { 92 | v[dialogIndex] = v[len(v)-1] 93 | v = v[:len(v)-1] 94 | m.bindings[k] = v 95 | } 96 | } 97 | m.mu.Unlock() 98 | } 99 | -------------------------------------------------------------------------------- /server/dialog/manager_test.go: -------------------------------------------------------------------------------- 1 | package dialog 2 | 3 | import "testing" 4 | 5 | func TestMemBind(t *testing.T) { 6 | m := NewMemManager().(*memManager) 7 | 8 | m.Bind("testDialog", "testType", "testID") 9 | 10 | if len(m.bindings) != 1 { 11 | t.Errorf("Binding failed; count %d != 1", len(m.bindings)) 12 | } 13 | } 14 | 15 | func TestMemUnbindDialog(t *testing.T) { 16 | m := NewMemManager().(*memManager) 17 | 18 | m.Bind("testDialog", "testType", "testID") 19 | 20 | if len(m.bindings) != 1 { 21 | t.Errorf("Binding failed; count %d != 1", len(m.bindings)) 22 | } 23 | 24 | m.UnbindDialog("testDialog") 25 | 26 | // The unbinding of a dialog doesn't clear the testType/testID 27 | if len(m.bindings) != 1 { 28 | t.Errorf("Unbinding Dialog failed; count %d != 0", len(m.bindings)) 29 | } 30 | 31 | // But it should make the testType/testID empty 32 | if i := len(m.List("testType", "testID")); i != 0 { 33 | t.Errorf("List('testType','testID'); count %d != 0", i) 34 | } 35 | } 36 | 37 | func TestMemBindUnbind(t *testing.T) { 38 | m := NewMemManager().(*memManager) 39 | 40 | m.Bind("testDialog", "testType", "testID") 41 | 42 | if len(m.bindings) != 1 { 43 | t.Errorf("Binding failed; count %d != 1", len(m.bindings)) 44 | } 45 | 46 | m.Unbind("testType", "testID") 47 | 48 | if len(m.bindings) != 0 { 49 | t.Errorf("Unbinding failed; count %d != 0", len(m.bindings)) 50 | } 51 | 52 | } 53 | 54 | func TestMemBindMultiple(t *testing.T) { 55 | m := NewMemManager().(*memManager) 56 | 57 | m.Bind("testDialog", "testType", "testID") 58 | m.Bind("testDialog2", "testType", "testID") 59 | 60 | m.Bind("testDialog", "testType", "testID2") 61 | 62 | m.Bind("testDialog", "testType2", "testID") 63 | m.Bind("testDialog3", "testType2", "testID") 64 | 65 | if len(m.bindings) != 3 { 66 | t.Errorf("Binding failed; count %d != 3", len(m.bindings)) 67 | } 68 | } 69 | 70 | func TestMemBindUnbindMultiple(t *testing.T) { 71 | m := NewMemManager().(*memManager) 72 | 73 | m.Bind("testDialog", "testType", "testID") 74 | m.Bind("testDialog2", "testType", "testID") 75 | 76 | m.Bind("testDialog", "testType", "testID2") 77 | 78 | m.Bind("testDialog", "testType2", "testID") 79 | m.Bind("testDialog3", "testType2", "testID") 80 | 81 | if len(m.bindings) != 3 { 82 | t.Errorf("Binding failed; count %d != 3", len(m.bindings)) 83 | } 84 | 85 | m.Unbind("testType", "testID") 86 | 87 | if len(m.bindings) != 2 { 88 | t.Errorf("Unbinding failed; count %d != 2", len(m.bindings)) 89 | } 90 | 91 | if len(m.List("testType", "testID")) != 0 { 92 | t.Errorf("Unbinding failed; List('testType','testID') => len %d != 2", len(m.bindings)) 93 | } 94 | 95 | m.Unbind("testType2", "testID") 96 | 97 | if len(m.bindings) != 1 { 98 | t.Errorf("Binding failed; count %d != 1", len(m.bindings)) 99 | } 100 | 101 | } 102 | 103 | func TestMemBindDuplicate(t *testing.T) { 104 | m := NewMemManager().(*memManager) 105 | 106 | m.Bind("testDialog", "testType", "testID") 107 | m.Bind("testDialog", "testType", "testID") 108 | 109 | if len(m.bindings) != 1 { 110 | t.Errorf("Binding failed; count %d != 1", len(m.bindings)) 111 | } 112 | } 113 | 114 | func TestMemList(t *testing.T) { 115 | m := NewMemManager().(*memManager) 116 | m.Bind("testDialog", "testType", "testID") 117 | m.Bind("testDialog", "testType", "testID2") 118 | m.Bind("testDialog", "testType2", "testID") 119 | m.Bind("testDialog2", "testType", "testID") 120 | m.Bind("testDialog3", "testType2", "testID") 121 | 122 | list := m.List("testType", "testID") 123 | 124 | if len(list) != 2 { 125 | t.Errorf("Incorrect count %d != 1", len(list)) 126 | } 127 | 128 | var testFound int 129 | var test2Found int 130 | for _, d := range list { 131 | if d == "testDialog" { 132 | testFound++ 133 | } 134 | if d == "testDialog2" { 135 | test2Found++ 136 | } 137 | } 138 | if testFound != 1 { 139 | t.Errorf("Incorrect number of testDialog dialogs: %d != 1", testFound) 140 | } 141 | if test2Found != 1 { 142 | t.Errorf("Incorrect number of testDialog2 dialogs: %d != 1", test2Found) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /server/doc.go: -------------------------------------------------------------------------------- 1 | // Package server provides a proxy for ARI calls. It is 2 | // usable via the client/nc Client and can use any given ari.Client 3 | package server 4 | -------------------------------------------------------------------------------- /server/endpoint.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) endpointData(ctx context.Context, reply string, req *proxy.Request) { 10 | data, err := s.ari.Endpoint().Data(req.Key) 11 | if err != nil { 12 | s.sendError(reply, err) 13 | return 14 | } 15 | 16 | s.publish(reply, &proxy.Response{ 17 | Data: &proxy.EntityData{ 18 | Endpoint: data, 19 | }, 20 | }) 21 | } 22 | 23 | func (s *Server) endpointGet(ctx context.Context, reply string, req *proxy.Request) { 24 | data, err := s.ari.Endpoint().Data(req.Key) 25 | if err != nil { 26 | s.sendError(reply, err) 27 | return 28 | } 29 | 30 | s.publish(reply, &proxy.Response{ 31 | Key: data.Key, 32 | }) 33 | } 34 | 35 | func (s *Server) endpointList(ctx context.Context, reply string, req *proxy.Request) { 36 | list, err := s.ari.Endpoint().List(nil) 37 | if err != nil { 38 | s.sendError(reply, err) 39 | return 40 | } 41 | 42 | s.publish(reply, &proxy.Response{ 43 | Keys: list, 44 | }) 45 | } 46 | 47 | func (s *Server) endpointListByTech(ctx context.Context, reply string, req *proxy.Request) { 48 | list, err := s.ari.Endpoint().ListByTech(req.EndpointListByTech.Tech, req.Key) 49 | if err != nil { 50 | s.sendError(reply, err) 51 | return 52 | } 53 | 54 | s.publish(reply, &proxy.Response{ 55 | Keys: list, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /server/endpoint_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestEndpointList(t *testing.T) { 10 | integration.TestEndpointList(t, &srv{}) 11 | } 12 | 13 | func TestEndpointListByTech(t *testing.T) { 14 | integration.TestEndpointListByTech(t, &srv{}) 15 | } 16 | 17 | func TestEndpointData(t *testing.T) { 18 | integration.TestEndpointData(t, &srv{}) 19 | } 20 | -------------------------------------------------------------------------------- /server/events.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/CyCoreSystems/ari/v5" 4 | 5 | func (s *Server) dialogsForEvent(e ari.Event) (ret []string) { 6 | for _, k := range e.Keys() { 7 | if k == nil { 8 | s.Log.Warn("received nil key for event", "event", e) 9 | continue 10 | } 11 | ret = append(ret, s.Dialog.List(k.Kind, k.ID)...) 12 | } 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /server/handler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "github.com/CyCoreSystems/ari-proxy/v5/session" 4 | 5 | // Reply is a function which, when called, replies to the request via the 6 | // response object or error. 7 | type Reply func(interface{}, error) 8 | 9 | // A Handler2 is a function which provides a session-aware request-response for messagebus 10 | type Handler2 func(msg *session.Message, reply Reply) 11 | 12 | // Handler is left for compat 13 | type Handler func(subj string, request []byte, reply Reply) 14 | -------------------------------------------------------------------------------- /server/liveRecording.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) recordingLiveData(ctx context.Context, reply string, req *proxy.Request) { 10 | data, err := s.ari.LiveRecording().Data(req.Key) 11 | if err != nil { 12 | s.sendError(reply, err) 13 | return 14 | } 15 | 16 | s.publish(reply, &proxy.Response{ 17 | Data: &proxy.EntityData{ 18 | LiveRecording: data, 19 | }, 20 | }) 21 | } 22 | 23 | func (s *Server) recordingLiveGet(ctx context.Context, reply string, req *proxy.Request) { 24 | data, err := s.ari.LiveRecording().Data(req.Key) 25 | if err != nil { 26 | s.sendError(reply, err) 27 | return 28 | } 29 | 30 | s.publish(reply, &proxy.Response{ 31 | Key: data.Key, 32 | }) 33 | } 34 | 35 | func (s *Server) recordingLiveMute(ctx context.Context, reply string, req *proxy.Request) { 36 | s.sendError(reply, s.ari.LiveRecording().Mute(req.Key)) 37 | } 38 | 39 | func (s *Server) recordingLivePause(ctx context.Context, reply string, req *proxy.Request) { 40 | s.sendError(reply, s.ari.LiveRecording().Pause(req.Key)) 41 | } 42 | 43 | func (s *Server) recordingLiveResume(ctx context.Context, reply string, req *proxy.Request) { 44 | s.sendError(reply, s.ari.LiveRecording().Resume(req.Key)) 45 | } 46 | 47 | func (s *Server) recordingLiveScrap(ctx context.Context, reply string, req *proxy.Request) { 48 | s.sendError(reply, s.ari.LiveRecording().Scrap(req.Key)) 49 | } 50 | 51 | func (s *Server) recordingLiveSubscribe(ctx context.Context, reply string, req *proxy.Request) { 52 | if req.Key.Dialog != "" { 53 | s.Dialog.Bind(req.Key.Dialog, "recording", req.Key.ID) 54 | } 55 | 56 | s.sendError(reply, nil) 57 | } 58 | 59 | func (s *Server) recordingLiveStop(ctx context.Context, reply string, req *proxy.Request) { 60 | s.sendError(reply, s.ari.LiveRecording().Stop(req.Key)) 61 | } 62 | 63 | func (s *Server) recordingLiveUnmute(ctx context.Context, reply string, req *proxy.Request) { 64 | s.sendError(reply, s.ari.LiveRecording().Unmute(req.Key)) 65 | } 66 | -------------------------------------------------------------------------------- /server/liveRecording_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestLiveRecordingData(t *testing.T) { 10 | integration.TestLiveRecordingData(t, &srv{}) 11 | } 12 | 13 | /* 14 | func TestLiveRecordingDelete(t *testing.T) { 15 | integration.TestLiveRecordingDelete(t, &srv{}) 16 | } 17 | */ 18 | 19 | func TestLiveRecordingMute(t *testing.T) { 20 | integration.TestLiveRecordingMute(t, &srv{}) 21 | } 22 | 23 | func TestLiveRecordingUnmute(t *testing.T) { 24 | integration.TestLiveRecordingUnmute(t, &srv{}) 25 | } 26 | 27 | func TestLiveRecordingPause(t *testing.T) { 28 | integration.TestLiveRecordingPause(t, &srv{}) 29 | } 30 | 31 | func TestLiveRecordingStop(t *testing.T) { 32 | integration.TestLiveRecordingStop(t, &srv{}) 33 | } 34 | 35 | func TestLiveRecordingResume(t *testing.T) { 36 | integration.TestLiveRecordingResume(t, &srv{}) 37 | } 38 | 39 | func TestLiveRecordingScrap(t *testing.T) { 40 | integration.TestLiveRecordingScrap(t, &srv{}) 41 | } 42 | -------------------------------------------------------------------------------- /server/logging.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) asteriskLoggingList(ctx context.Context, reply string, req *proxy.Request) { 10 | list, err := s.ari.Asterisk().Logging().List(nil) 11 | if err != nil { 12 | s.sendError(reply, err) 13 | return 14 | } 15 | 16 | s.publish(reply, &proxy.Response{ 17 | Keys: list, 18 | }) 19 | } 20 | 21 | func (s *Server) asteriskLoggingGet(ctx context.Context, reply string, req *proxy.Request) { 22 | data, err := s.ari.Asterisk().Logging().Data(req.Key) 23 | if err != nil { 24 | s.sendError(reply, err) 25 | return 26 | } 27 | 28 | s.publish(reply, &proxy.Response{ 29 | Key: data.Key, 30 | }) 31 | } 32 | 33 | func (s *Server) asteriskLoggingCreate(ctx context.Context, reply string, req *proxy.Request) { 34 | h, err := s.ari.Asterisk().Logging().Create(req.Key, req.AsteriskLoggingChannel.Levels) 35 | if err != nil { 36 | s.sendError(reply, err) 37 | return 38 | } 39 | 40 | s.publish(reply, &proxy.Response{ 41 | Key: h.Key(), 42 | }) 43 | } 44 | 45 | func (s *Server) asteriskLoggingData(ctx context.Context, reply string, req *proxy.Request) { 46 | data, err := s.ari.Asterisk().Logging().Data(req.Key) 47 | if err != nil { 48 | s.sendError(reply, err) 49 | return 50 | } 51 | 52 | s.publish(reply, &proxy.Response{ 53 | Data: &proxy.EntityData{ 54 | Log: data, 55 | }, 56 | }) 57 | } 58 | 59 | func (s *Server) asteriskLoggingRotate(ctx context.Context, reply string, req *proxy.Request) { 60 | s.sendError(reply, s.ari.Asterisk().Logging().Rotate(req.Key)) 61 | } 62 | 63 | func (s *Server) asteriskLoggingDelete(ctx context.Context, reply string, req *proxy.Request) { 64 | s.sendError(reply, s.ari.Asterisk().Logging().Delete(req.Key)) 65 | } 66 | -------------------------------------------------------------------------------- /server/logging_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestLoggingList(t *testing.T) { 10 | integration.TestLoggingList(t, &srv{}) 11 | } 12 | 13 | func TestLoggingCreate(t *testing.T) { 14 | integration.TestLoggingCreate(t, &srv{}) 15 | } 16 | 17 | func TestLoggingRotate(t *testing.T) { 18 | integration.TestLoggingRotate(t, &srv{}) 19 | } 20 | 21 | func TestLoggingDelete(t *testing.T) { 22 | integration.TestLoggingDelete(t, &srv{}) 23 | } 24 | -------------------------------------------------------------------------------- /server/mailbox.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) mailboxData(ctx context.Context, reply string, req *proxy.Request) { 10 | data, err := s.ari.Mailbox().Data(req.Key) 11 | if err != nil { 12 | s.sendError(reply, err) 13 | return 14 | } 15 | 16 | s.publish(reply, &proxy.Response{ 17 | Data: &proxy.EntityData{ 18 | Mailbox: data, 19 | }, 20 | }) 21 | } 22 | 23 | func (s *Server) mailboxGet(ctx context.Context, reply string, req *proxy.Request) { 24 | data, err := s.ari.Mailbox().Data(req.Key) 25 | if err != nil { 26 | s.sendError(reply, err) 27 | return 28 | } 29 | 30 | s.publish(reply, &proxy.Response{ 31 | Key: data.Key, 32 | }) 33 | } 34 | 35 | func (s *Server) mailboxDelete(ctx context.Context, reply string, req *proxy.Request) { 36 | s.sendError(reply, s.ari.Mailbox().Delete(req.Key)) 37 | } 38 | 39 | func (s *Server) mailboxList(ctx context.Context, reply string, req *proxy.Request) { 40 | list, err := s.ari.Mailbox().List(nil) 41 | if err != nil { 42 | s.sendError(reply, err) 43 | return 44 | } 45 | 46 | s.publish(reply, &proxy.Response{ 47 | Keys: list, 48 | }) 49 | } 50 | 51 | func (s *Server) mailboxUpdate(ctx context.Context, reply string, req *proxy.Request) { 52 | s.sendError(reply, s.ari.Mailbox().Update(req.Key, req.MailboxUpdate.Old, req.MailboxUpdate.New)) 53 | } 54 | -------------------------------------------------------------------------------- /server/mailbox_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestMailboxList(t *testing.T) { 10 | integration.TestMailboxList(t, &srv{}) 11 | } 12 | 13 | func TestMailboxUpdate(t *testing.T) { 14 | integration.TestMailboxUpdate(t, &srv{}) 15 | } 16 | 17 | func TestMailboxDelete(t *testing.T) { 18 | integration.TestMailboxDelete(t, &srv{}) 19 | } 20 | 21 | func TestMailboxData(t *testing.T) { 22 | integration.TestMailboxData(t, &srv{}) 23 | } 24 | -------------------------------------------------------------------------------- /server/modules.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) asteriskModuleLoad(ctx context.Context, reply string, req *proxy.Request) { 10 | s.sendError(reply, s.ari.Asterisk().Modules().Load(req.Key)) 11 | } 12 | 13 | func (s *Server) asteriskModuleUnload(ctx context.Context, reply string, req *proxy.Request) { 14 | s.sendError(reply, s.ari.Asterisk().Modules().Unload(req.Key)) 15 | } 16 | 17 | func (s *Server) asteriskModuleReload(ctx context.Context, reply string, req *proxy.Request) { 18 | s.sendError(reply, s.ari.Asterisk().Modules().Reload(req.Key)) 19 | } 20 | 21 | func (s *Server) asteriskModuleData(ctx context.Context, reply string, req *proxy.Request) { 22 | data, err := s.ari.Asterisk().Modules().Data(req.Key) 23 | if err != nil { 24 | s.sendError(reply, err) 25 | return 26 | } 27 | 28 | s.publish(reply, &proxy.Response{ 29 | Data: &proxy.EntityData{ 30 | Module: data, 31 | }, 32 | }) 33 | } 34 | 35 | func (s *Server) asteriskModuleGet(ctx context.Context, reply string, req *proxy.Request) { 36 | data, err := s.ari.Asterisk().Modules().Data(req.Key) 37 | if err != nil { 38 | s.sendError(reply, err) 39 | return 40 | } 41 | 42 | s.publish(reply, &proxy.Response{ 43 | Key: data.Key, 44 | }) 45 | } 46 | 47 | func (s *Server) asteriskModuleList(ctx context.Context, reply string, req *proxy.Request) { 48 | list, err := s.ari.Asterisk().Modules().List(nil) 49 | if err != nil { 50 | s.sendError(reply, err) 51 | return 52 | } 53 | 54 | s.publish(reply, &proxy.Response{ 55 | Keys: list, 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /server/modules_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestModulesData(t *testing.T) { 10 | integration.TestModulesData(t, &srv{}) 11 | } 12 | 13 | func TestModulesLoad(t *testing.T) { 14 | integration.TestModulesLoad(t, &srv{}) 15 | } 16 | 17 | func TestModulesReload(t *testing.T) { 18 | integration.TestModulesReload(t, &srv{}) 19 | } 20 | 21 | func TestModulesUnload(t *testing.T) { 22 | integration.TestModulesUnload(t, &srv{}) 23 | } 24 | 25 | func TestModulesList(t *testing.T) { 26 | integration.TestModulesList(t, &srv{}) 27 | } 28 | -------------------------------------------------------------------------------- /server/options.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/inconshreveable/log15" 7 | ) 8 | 9 | // Options are the group of options for the ari-proxy server 10 | type Options struct { 11 | //TODO: include nats/rabbitmq options 12 | 13 | URL string 14 | 15 | Logger log15.Logger 16 | Parent context.Context 17 | } 18 | -------------------------------------------------------------------------------- /server/playback.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) playbackControl(ctx context.Context, reply string, req *proxy.Request) { 10 | s.sendError(reply, s.ari.Playback().Control(req.Key, req.PlaybackControl.Command)) 11 | } 12 | 13 | func (s *Server) playbackData(ctx context.Context, reply string, req *proxy.Request) { 14 | data, err := s.ari.Playback().Data(req.Key) 15 | if err != nil { 16 | s.sendError(reply, err) 17 | return 18 | } 19 | 20 | s.publish(reply, &proxy.Response{ 21 | Data: &proxy.EntityData{ 22 | Playback: data, 23 | }, 24 | }) 25 | } 26 | 27 | func (s *Server) playbackGet(ctx context.Context, reply string, req *proxy.Request) { 28 | s.Log.Debug("Fetching playback data", "playback", req.Key) 29 | data, err := s.ari.Playback().Data(req.Key) 30 | if err != nil { 31 | s.sendError(reply, err) 32 | return 33 | } 34 | 35 | s.publish(reply, &proxy.Response{ 36 | Key: data.Key, 37 | }) 38 | } 39 | 40 | func (s *Server) playbackStop(ctx context.Context, reply string, req *proxy.Request) { 41 | s.sendError(reply, s.ari.Playback().Stop(req.Key)) 42 | } 43 | 44 | func (s *Server) playbackSubscribe(ctx context.Context, reply string, req *proxy.Request) { 45 | // bind dialog 46 | if req.Key.Dialog != "" { 47 | s.Dialog.Bind(req.Key.Dialog, "playback", req.Key.ID) 48 | } 49 | 50 | s.sendError(reply, nil) 51 | } 52 | -------------------------------------------------------------------------------- /server/playback_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestPlaybackData(t *testing.T) { 10 | integration.TestPlaybackData(t, &srv{}) 11 | } 12 | 13 | func TestPlaybackControl(t *testing.T) { 14 | integration.TestPlaybackControl(t, &srv{}) 15 | } 16 | 17 | func TestPlaybackStop(t *testing.T) { 18 | integration.TestPlaybackStop(t, &srv{}) 19 | } 20 | -------------------------------------------------------------------------------- /server/sound.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) soundData(ctx context.Context, reply string, req *proxy.Request) { 10 | data, err := s.ari.Sound().Data(req.Key) 11 | if err != nil { 12 | s.sendError(reply, err) 13 | return 14 | } 15 | 16 | s.publish(reply, &proxy.Response{ 17 | Data: &proxy.EntityData{ 18 | Sound: data, 19 | }, 20 | }) 21 | } 22 | 23 | func (s *Server) soundList(ctx context.Context, reply string, req *proxy.Request) { 24 | filters := req.SoundList.Filters 25 | 26 | if len(filters) == 0 { 27 | filters = nil // just send nil to upstream if empty. makes tests easier 28 | } 29 | 30 | list, err := s.ari.Sound().List(filters, req.Key) 31 | if err != nil { 32 | s.sendError(reply, err) 33 | return 34 | } 35 | 36 | s.publish(reply, &proxy.Response{ 37 | Keys: list, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /server/sound_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/internal/integration" 7 | ) 8 | 9 | func TestSoundData(t *testing.T) { 10 | integration.TestSoundData(t, &srv{}) 11 | } 12 | 13 | func TestSoundList(t *testing.T) { 14 | integration.TestSoundList(t, &srv{}) 15 | } 16 | -------------------------------------------------------------------------------- /server/storedRecording.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/CyCoreSystems/ari-proxy/v5/proxy" 7 | ) 8 | 9 | func (s *Server) recordingStoredCopy(ctx context.Context, reply string, req *proxy.Request) { 10 | h, err := s.ari.StoredRecording().Copy(req.Key, req.RecordingStoredCopy.Destination) 11 | if err != nil { 12 | s.sendError(reply, err) 13 | return 14 | } 15 | 16 | s.publish(reply, &proxy.Response{ 17 | Key: h.Key(), 18 | }) 19 | } 20 | 21 | func (s *Server) recordingStoredData(ctx context.Context, reply string, req *proxy.Request) { 22 | data, err := s.ari.StoredRecording().Data(req.Key) 23 | if err != nil { 24 | s.sendError(reply, err) 25 | return 26 | } 27 | 28 | s.publish(reply, &proxy.Response{ 29 | Data: &proxy.EntityData{ 30 | StoredRecording: data, 31 | }, 32 | }) 33 | } 34 | 35 | func (s *Server) recordingStoredGet(ctx context.Context, reply string, req *proxy.Request) { 36 | data, err := s.ari.StoredRecording().Data(req.Key) 37 | if err != nil { 38 | s.sendError(reply, err) 39 | return 40 | } 41 | 42 | s.publish(reply, &proxy.Response{ 43 | Key: data.Key, 44 | }) 45 | } 46 | 47 | func (s *Server) recordingStoredDelete(ctx context.Context, reply string, req *proxy.Request) { 48 | s.sendError(reply, s.ari.StoredRecording().Delete(req.Key)) 49 | } 50 | 51 | func (s *Server) recordingStoredList(ctx context.Context, reply string, req *proxy.Request) { 52 | list, err := s.ari.StoredRecording().List(nil) 53 | if err != nil { 54 | s.sendError(reply, err) 55 | return 56 | } 57 | 58 | s.publish(reply, &proxy.Response{ 59 | Keys: list, 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /session/dialog.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | // A Dialog is a session between the ARI proxy client and the ARI proxy server 4 | type Dialog struct { 5 | ID string 6 | Transport Transport 7 | Objects Objects 8 | ChannelID string // The channel ID from the StasisStart event 9 | } 10 | 11 | // NewDialog creates a new dialog with the given transport 12 | func NewDialog(id string, transport Transport) *Dialog { 13 | return &Dialog{ 14 | ID: id, 15 | Transport: transport, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /session/events.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | // AppStart is the event sent on the start of an application and the creation of a server side dialog 4 | type AppStart struct { 5 | ServerID string `json:"server"` 6 | DialogID string `json:"dialog"` 7 | Application string `json:"application"` 8 | AppArgs []string `json:"appargs"` 9 | ChannelID string `json:"channel"` // The channel from the stasis start event 10 | } 11 | -------------------------------------------------------------------------------- /session/message.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | // Message is the wrapper for a command sent over a dialog 4 | type Message struct { 5 | Command string `json:"command"` 6 | Object string `json:"object"` 7 | Payload []byte `json:"payload"` 8 | } 9 | -------------------------------------------------------------------------------- /session/objects.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "sort" 5 | "sync" 6 | ) 7 | 8 | // Objects tracks a list of object IDs that are associated with the dialog 9 | type Objects struct { 10 | ids []string 11 | mutex sync.RWMutex 12 | } 13 | 14 | // Contains finds the id, if it exists 15 | func (o *Objects) Contains(id string) (int, bool) { 16 | o.mutex.RLock() 17 | defer o.mutex.RUnlock() 18 | 19 | idx := sort.SearchStrings(o.ids, id) 20 | 21 | if idx == len(o.ids) || o.ids[idx] != id { 22 | return -1, false 23 | } 24 | 25 | return idx, true 26 | } 27 | 28 | // Add adds the given object 29 | func (o *Objects) Add(id string) bool { 30 | 31 | if _, ok := o.Contains(id); ok { 32 | return false 33 | } 34 | 35 | o.mutex.Lock() 36 | defer o.mutex.Unlock() 37 | 38 | o.ids = append(o.ids, id) 39 | sort.Strings(o.ids) 40 | 41 | return true 42 | } 43 | 44 | // Remove removes the given object, if it exists 45 | func (o *Objects) Remove(id string) bool { 46 | idx, ok := o.Contains(id) 47 | if !ok { 48 | return false 49 | } 50 | 51 | o.mutex.Lock() 52 | defer o.mutex.Unlock() 53 | o.ids = append(o.ids[:idx], o.ids[idx+1:]...) 54 | 55 | return true 56 | } 57 | 58 | // Clear removes all the objects 59 | func (o *Objects) Clear() { 60 | o.mutex.Lock() 61 | defer o.mutex.Unlock() 62 | 63 | o.ids = make([]string, 0) 64 | } 65 | 66 | // Items returns the list of items 67 | func (o *Objects) Items() []string { 68 | return o.ids 69 | } 70 | -------------------------------------------------------------------------------- /session/objects_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "testing" 4 | 5 | var addTests = []struct { 6 | Items []string 7 | Expected []string 8 | Return []bool 9 | Description string 10 | }{ 11 | // Single adding 12 | {[]string{"1"}, []string{"1"}, []bool{true}, "Add('%v') => '%v'; '%v'. Expected '%v'; '%v'."}, 13 | 14 | // Duplicate 15 | {[]string{"1", "1"}, []string{"1"}, []bool{true, false}, "Add('%v') => '%v'; '%v'. Expected '%v'; '%v'."}, 16 | 17 | // Sorting 18 | {[]string{"2", "1"}, []string{"1", "2"}, []bool{true, true}, "Add('%v') => '%v'; '%v'. Expected '%v'; '%v'."}, 19 | } 20 | 21 | func TestObjectsAdd(t *testing.T) { 22 | 23 | for _, test := range addTests { 24 | var failed bool 25 | var objects Objects 26 | var returns []bool 27 | for idx, item := range test.Items { 28 | returns = append(returns, objects.Add(item)) 29 | if returns[idx] != test.Return[idx] { 30 | failed = true 31 | } 32 | } 33 | 34 | failed = failed || !stringSliceEq(objects.ids, test.Expected) 35 | 36 | if failed { 37 | t.Errorf(test.Description, 38 | test.Items, // input 39 | returns, objects.ids, //actual output 40 | test.Return, test.Expected, // expected output 41 | ) 42 | } 43 | } 44 | } 45 | 46 | var removeTests = []struct { 47 | Initial []string 48 | Items []string 49 | Return []bool 50 | Expected []string 51 | Description string 52 | }{ 53 | // single remove 54 | {[]string{"1", "2", "3"}, []string{"2"}, []bool{true}, []string{"1", "3"}, "Remove('%v') => '%v'; '%v'. Expected '%v'; '%v'."}, 55 | 56 | // duplicate remove 57 | {[]string{"1", "2", "3"}, []string{"2", "2"}, []bool{true, false}, []string{"1", "3"}, "Remove('%v') => '%v'; '%v'. Expected '%v'; '%v'."}, 58 | 59 | // missing remove 60 | {[]string{}, []string{"2"}, []bool{false}, []string{}, "Remove('%v') => '%v'; '%v'. Expected '%v'; '%v'."}, 61 | 62 | // remove at end 63 | {[]string{"1", "2", "3"}, []string{"3"}, []bool{true}, []string{"1", "2"}, "Remove('%v') => '%v'; '%v'. Expected '%v'; '%v'."}, 64 | } 65 | 66 | func TestObjectsRemove(t *testing.T) { 67 | 68 | for _, test := range removeTests { 69 | 70 | var objects Objects 71 | objects.ids = test.Initial 72 | 73 | var failed bool 74 | var returns []bool 75 | for idx, item := range test.Items { 76 | returns = append(returns, objects.Remove(item)) 77 | if returns[idx] != test.Return[idx] { 78 | failed = true 79 | } 80 | } 81 | 82 | failed = failed || !stringSliceEq(objects.ids, test.Expected) 83 | 84 | if failed { 85 | t.Errorf(test.Description, 86 | test.Items, // input 87 | returns, objects.ids, //actual output 88 | test.Return, test.Expected, // expected output 89 | ) 90 | } 91 | } 92 | } 93 | 94 | var clearTests = []struct { 95 | Initial []string 96 | Description string 97 | }{ 98 | {[]string{"1", "2", "3"}, "'%v'.Clear() => '%v'. Expected empty list."}, 99 | {[]string{}, "'%v'.Clear() => '%v'. Expected empty list."}, 100 | } 101 | 102 | func TestObjectsClear(t *testing.T) { 103 | for _, test := range clearTests { 104 | 105 | var failed bool 106 | 107 | var objects Objects 108 | objects.ids = test.Initial 109 | 110 | objects.Clear() 111 | 112 | failed = len(objects.ids) != 0 113 | 114 | if failed { 115 | t.Errorf(test.Description, 116 | test.Initial, // input 117 | objects.ids, //actual output 118 | ) 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /session/transport.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "github.com/CyCoreSystems/ari/v5" 4 | 5 | // Transport defines how the commands and events are sent. 6 | type Transport interface { 7 | 8 | // Command sends a command and waits for a response 9 | Command(name string, body interface{}, resp interface{}) error 10 | 11 | // Event dispatches an event 12 | Event(evt ari.Event) error 13 | } 14 | -------------------------------------------------------------------------------- /session/utils_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "testing" 4 | 5 | func stringSliceEq(left []string, right []string) bool { 6 | if len(left) != len(right) { 7 | return false 8 | } 9 | 10 | for idx := range left { 11 | if left[idx] != right[idx] { 12 | return false 13 | } 14 | } 15 | 16 | return true 17 | } 18 | 19 | var stringSliceEqTests = []struct { 20 | Left []string 21 | Right []string 22 | Expected bool 23 | Description string 24 | }{ 25 | {[]string{"1"}, []string{"1"}, true, "stringSliceEq('%v','%v') => '%v'. Expected '%v'"}, 26 | {[]string{}, []string{}, true, "stringSliceEq('%v','%v') => '%v'. Expected '%v'"}, 27 | {[]string{"1"}, []string{"2"}, false, "stringSliceEq('%v','%v') => '%v'. Expected '%v'"}, 28 | {[]string{"2", "2"}, []string{"2"}, false, "stringSliceEq('%v','%v') => '%v'. Expected '%v'"}, 29 | } 30 | 31 | func TestStringSliceEq(t *testing.T) { 32 | for _, test := range stringSliceEqTests { 33 | var failed bool 34 | 35 | ok := stringSliceEq(test.Left, test.Right) 36 | failed = ok != test.Expected 37 | if failed { 38 | t.Errorf(test.Description, 39 | test.Left, test.Right, // input 40 | ok, // actual output 41 | test.Expected, // expected output 42 | ) 43 | } 44 | } 45 | } 46 | --------------------------------------------------------------------------------