├── .ably
└── capabilities.yaml
├── .github
└── workflows
│ ├── check.yml
│ ├── docs.yml
│ ├── features.yml
│ └── integration-test.yml
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── CONTRIBUTING.md
├── COPYRIGHT
├── LICENSE
├── MAINTAINERS.md
├── README.md
├── UPDATING.md
├── ably
├── ably.go
├── ably_test.go
├── auth.go
├── auth_integration_test.go
├── auth_internal_test.go
├── auth_test.go
├── crypto.go
├── crypto_spec_test.go
├── doc.go
├── doc_test.go
├── error.go
├── error_internal_test.go
├── error_names.go
├── error_test.go
├── errors.go
├── event_emitter.go
├── event_emitter_integration_test.go
├── event_emitter_spec_integration_test.go
├── export_test.go
├── generate.go
├── http_paginated_response_integration_test.go
├── internal
│ └── ablyutil
│ │ ├── idempotent.go
│ │ ├── merge.go
│ │ ├── msgpack.go
│ │ ├── msgpack_test.go
│ │ ├── regex.go
│ │ ├── regex_test.go
│ │ ├── strings.go
│ │ ├── strings_test.go
│ │ └── time.go
├── logger.go
├── logger_test.go
├── options.go
├── options_test.go
├── os_version_other.go
├── os_version_unix.go
├── os_version_windows.go
├── paginated_result.go
├── proto_action.go
├── proto_channel_options.go
├── proto_channel_options_test.go
├── proto_channel_propeties.go
├── proto_crypto.go
├── proto_error.go
├── proto_http.go
├── proto_message.go
├── proto_message_decoding_test.go
├── proto_message_doc.txt
├── proto_message_integration_test.go
├── proto_message_test.go
├── proto_presence_message.go
├── proto_presence_message_test.go
├── proto_protocol_message.go
├── proto_protocol_message_test.go
├── proto_stat.go
├── proto_types.go
├── proto_types_test.go
├── realtime_channel.go
├── realtime_channel_integration_test.go
├── realtime_channel_internal_test.go
├── realtime_channel_spec_integration_test.go
├── realtime_channel_test.go
├── realtime_client.go
├── realtime_client_integration_test.go
├── realtime_client_internal_test.go
├── realtime_conn.go
├── realtime_conn_integration_test.go
├── realtime_conn_spec_integration_test.go
├── realtime_presence.go
├── realtime_presence_integration_test.go
├── realtime_presence_internal_test.go
├── recovery_context.go
├── recovery_context_test.go
├── rest_channel.go
├── rest_channel_integration_test.go
├── rest_channel_spec_integration_test.go
├── rest_client.go
├── rest_client_integration_test.go
├── rest_client_test.go
├── rest_presence.go
├── rest_presence_spec_integration_test.go
├── state.go
├── token.go
├── transitioner_integration_test.go
├── websocket.go
└── websocket_internal_test.go
├── ablytest
├── ablytest.go
├── cryptodata.go
├── fmt.go
├── fmt_test.go
├── logger.go
├── msgpack.go
├── pagination.go
├── proxies.go
├── recorders.go
├── resultgroup.go
├── sandbox.go
├── test_utils.go
└── timeout.go
├── examples
├── README.md
├── constants.go
├── realtime
│ ├── presence
│ │ └── main.go
│ └── pub-sub
│ │ └── main.go
├── rest
│ ├── history
│ │ └── main.go
│ ├── presence
│ │ └── main.go
│ ├── publish
│ │ └── main.go
│ ├── stats
│ │ └── main.go
│ └── status
│ │ └── main.go
└── utils.go
├── go.mod
├── go.sum
└── scripts
├── ci-generate.sh
└── errors
└── main.go
/.ably/capabilities.yaml:
--------------------------------------------------------------------------------
1 | %YAML 1.2
2 | ---
3 | common-version: 1.2.0
4 | compliance:
5 | Agent Identifier:
6 | Agents:
7 | Operating System:
8 | Authentication:
9 | API Key:
10 | Token:
11 | Callback:
12 | Literal:
13 | URL:
14 | Query Time:
15 | Debugging:
16 | Error Information:
17 | Logs:
18 | Protocol:
19 | JSON:
20 | MessagePack:
21 | Realtime:
22 | Authentication:
23 | Get Confirmed Client Identifier:
24 | Channel:
25 | Attach:
26 | Encryption:
27 | History:
28 | Mode:
29 | Presence:
30 | Enter:
31 | Client:
32 | Get:
33 | History:
34 | Subscribe:
35 | Update:
36 | Client:
37 | Publish:
38 | .caveats: Publishing also causes the client to attach to the channel.
39 | Retry Timeout:
40 | State Events:
41 | Subscribe:
42 | Deltas:
43 | Rewind:
44 | Connection:
45 | Disconnected Retry Timeout:
46 | Get Identifier:
47 | Incremental Backoff:
48 | Lifecycle Control:
49 | Recovery:
50 | State Events:
51 | Suspended Retry Timeout:
52 | Message Echoes:
53 | Message Queuing:
54 | Transport Parameters:
55 | REST:
56 | Authentication:
57 | Authorize:
58 | Create Token Request:
59 | Get Client Identifier:
60 | Request Token:
61 | Channel:
62 | Encryption:
63 | Existence Check:
64 | Get:
65 | History:
66 | Iterate:
67 | Name:
68 | Presence:
69 | History:
70 | Member List:
71 | Publish:
72 | Idempotence:
73 | Parameters for Query String:
74 | Release:
75 | Status:
76 | Channel Details:
77 | Opaque Request:
78 | Request Timeout:
79 | Service:
80 | Get Time:
81 | Statistics:
82 | Query:
83 | Support Hyperlink on Request Failure:
84 | Service:
85 | Environment:
86 | Fallbacks:
87 | Hosts:
88 | Retry Count:
89 | Retry Timeout:
90 | Host:
91 | Testing:
92 | Disable TLS:
93 | TCP Insecure Port:
94 | TCP Secure Port:
95 | Transport:
96 | Connection Open Timeout:
97 | HTTP/2:
98 | Proxy:
99 | .caveats: Can be achieved through specifying a custom HTTP client.
100 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | # Based upon:
2 | # https://github.com/actions/starter-workflows/blob/main/ci/go.yml
3 |
4 | on:
5 | workflow_dispatch:
6 | pull_request:
7 | push:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | check:
13 |
14 | runs-on: ubuntu-latest
15 | strategy:
16 | fail-fast: false
17 | matrix:
18 | go-version: ['1.19', '1.20', '1.21', '1.22']
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | with:
23 | submodules: 'recursive'
24 |
25 | - name: Set up Go ${{ matrix.go-version }}
26 | uses: actions/setup-go@v2
27 | with:
28 | go-version: ${{ matrix.go-version }}
29 |
30 | - name: Download Packages
31 | run: go get -t -v ./ably/...
32 |
33 | - name: Format
34 | run: if [ "$(gofmt -l . | wc -l)" -gt 0 ]; then exit 1; fi
35 |
36 | - name: Vet
37 | run: go vet ./ably/... ./scripts/...
38 |
39 | - name: Ensure generated code is up-to-date
40 | run: scripts/ci-generate.sh
41 |
42 | - name: Install go-junit-report
43 | run: go install github.com/jstemmer/go-junit-report@latest
44 |
45 | - name: Unit Tests
46 | run: |
47 | set -o pipefail
48 | go test -v -tags=unit ./... |& tee >(~/go/bin/go-junit-report > unit.junit)
49 |
50 | - name: Upload test results
51 | if: always()
52 | uses: ably/test-observability-action@v1
53 | with:
54 | server-url: 'https://test-observability.herokuapp.com'
55 | server-auth: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }}
56 | path: '.'
57 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: API Reference
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - main
7 | tags:
8 | - 'v*'
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | permissions:
14 | deployments: write
15 | id-token: write
16 | steps:
17 | - uses: actions/checkout@v2
18 | with:
19 | submodules: 'recursive'
20 |
21 | - name: Set up Go 1.19
22 | uses: actions/setup-go@v2
23 | with:
24 | go-version: 1.19
25 |
26 | - name: Download Packages
27 | run: |
28 | go install golang.org/x/tools/cmd/godoc@v0.3.0
29 | go install github.com/johnstarich/go/gopages@v0.1.16
30 |
31 | - uses: ably/sdk-upload-action@v2
32 | id: sdk-upload-prempt
33 | with:
34 | mode: preempt
35 | sourcePath: dist
36 | githubToken: ${{ secrets.GITHUB_TOKEN }}
37 | artifactName: godoc
38 |
39 | - name: Build Documentation
40 | run: >
41 | gopages
42 | -source-link "https://github.com/ably/ably-go/blob/${{ github.sha }}/ably/{{slice .Path 5}}{{if .Line}}#L{{.Line}}{{end}}"
43 | -brand-description "Go client library for Ably realtime messaging service."
44 | -brand-title "Ably Go SDK"
45 | -base "${{steps.sdk-upload-prempt.outputs.url-base}}../godoc"
46 |
47 | - name: Configure AWS Credentials
48 | uses: aws-actions/configure-aws-credentials@v1
49 | with:
50 | aws-region: eu-west-2
51 | role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/ably-sdk-builds-ably-go
52 | role-session-name: "${{ github.run_id }}-${{ github.run_number }}"
53 |
54 | - name: Upload Documentation
55 | uses: ably/sdk-upload-action@v2
56 | with:
57 | sourcePath: dist
58 | githubToken: ${{ secrets.GITHUB_TOKEN }}
59 | artifactName: godoc
60 |
--------------------------------------------------------------------------------
/.github/workflows/features.yml:
--------------------------------------------------------------------------------
1 | name: Features
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 |
9 | jobs:
10 | build:
11 | uses: ably/features/.github/workflows/sdk-features.yml@main
12 | with:
13 | repository-name: ably-go
14 | secrets: inherit
15 |
--------------------------------------------------------------------------------
/.github/workflows/integration-test.yml:
--------------------------------------------------------------------------------
1 | on:
2 | workflow_dispatch:
3 | pull_request:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | integration-test:
10 |
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | go-version: ['1.19', '1.20', '1.21', '1.22']
16 | protocol: ['json', 'msgpack']
17 |
18 | steps:
19 | - uses: actions/checkout@v2
20 | with:
21 | submodules: 'recursive'
22 |
23 | - name: Set up Go ${{ matrix.go-version }}
24 | uses: actions/setup-go@v2
25 | with:
26 | go-version: ${{ matrix.go-version }}
27 |
28 | - name: Download Packages
29 | run: go get -t -v ./ably/...
30 |
31 | - name: Install go-junit-report
32 | run: go install github.com/jstemmer/go-junit-report@latest
33 |
34 | - name: Integration Tests with ${{ matrix.protocol }} Protocol
35 | run: |
36 | set -o pipefail
37 | export ABLY_PROTOCOL=${{ matrix.protocol == 'json' && 'application/json' || 'application/x-msgpack' }}
38 | go test -tags=integration -p 1 -race -v -timeout 120m ./... |& tee >(~/go/bin/go-junit-report > ${{ matrix.protocol }}-integration.junit)
39 |
40 | - name: Upload test results
41 | if: always()
42 | uses: ably/test-observability-action@v1
43 | with:
44 | server-url: 'https://test-observability.herokuapp.com'
45 | server-auth: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }}
46 | path: '.'
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Output of the go coverage tool, specifically when used with LiteIDE
9 | *.out
10 |
11 | # Dependency directories (remove the comment below to include it)
12 | vendor/
13 | .idea/
14 | *.env
15 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "common"]
2 | path = common
3 | url = https://github.com/ably/ably-common.git
4 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing to Ably Go SDK
2 |
3 | Because this package uses `internal` packages, all fork development has to happen under `$GOPATH/src/github.com/ably/ably-go` to prevent `use of internal package not allowed` errors.
4 |
5 | 1. Fork `github.com/ably/ably-go`
6 | 2. go to the `ably-go` directory: `cd $GOPATH/src/github.com/ably/ably-go`
7 | 3. add your fork as a remote: `git remote add fork git@github.com:your-username/ably-go`
8 | 4. create your feature branch: `git checkout -b my-new-feature`
9 | 5. commit your changes (`git commit -am 'Add some feature'`)
10 | 6. ensure you have added suitable tests and the test suite is passing for both JSON and MessagePack protocols (see the Running Tests section below for more details about tests including the commands to run them).
11 | 7. push to the branch: `git push fork my-new-feature`
12 | 8. create a new Pull Request.
13 |
14 | ### Previewing Godoc Locally
15 |
16 | 1. Install [godoc](https://pkg.go.dev/golang.org/x/tools/cmd/godoc) globally via `go get` and run at root
17 |
18 | ```bash
19 | godoc -http=:8000
20 | ```
21 | - Open the link http://localhost:8000/ for viewing the documentation.
22 |
23 | 2. Export `godoc` using latest version of [gopages](https://pkg.go.dev/github.com/johnstarich/go/gopages#section-readme)
24 | ```bash
25 | gopages -brand-description "Go client library for Ably realtime messaging service." -brand-title "Ably Go SDK"
26 | ```
27 | - `godoc html` is exported to `dist` and can be served using `python3 http.server`
28 |
29 | ```bash
30 | cd dist
31 | py -m http.server 8000
32 | ```
33 | - Open the link http://localhost:8000/ for viewing the documentation.
34 |
35 | ### Running Tests
36 |
37 | This project contains two types of test. Test which use the `ablytest` package and tests which dont.
38 |
39 | At each stage of the CI pipeline, test results are uploaded to a [test observability server](https://github.com/ably-labs/test-observability-server)
40 |
41 | The tests which don't make use of the `ablytest` package are considered unit tests. These tests exist in files which are suffixed `_test.go`. They run in the CI pipeline at the step `Unit Tests`. They can be run locally with the command:
42 |
43 | ```
44 | go test -v -tags=unit ./...
45 | ```
46 |
47 | When adding new unit tests, the following build tag must be included at the top of the file to exclude these tests from running in CI as part of the Integration test step.
48 |
49 | ```
50 | //go:build !integration
51 | // +build !integration
52 | ```
53 |
54 | #### Integration tests
55 |
56 | The tests which use the package `ablytest` are considered integration tests. These tests take longer to run than the unit tests and are mostly run in a sandbox environment. They are dependent on the sandbox environment being available and will fail if the environment is experiencing issues. There are some known issues for random failures in a few of the tests, so some of these tests may fail unexpectedly from time to time.
57 |
58 | Please note that these tests are not true integration tests as rather than using the public API, they rely on `export_test.go` to expose private functionality so that it can be tested. These tests exist in files which are suffixed `_integration_test.go`. They run twice in the CI pipeline at the steps `Integration Tests with JSON Protocol` and `Integration Tests with MessagePack Protocol`. To run these tests locally, they have a dependency on a git submodule being present, so it is necessary clone the project with:
59 |
60 | ```
61 | git clone git@github.com:ably/ably-go.git --recurse-submodules
62 | ```
63 |
64 | The tests can be run with the commands:
65 |
66 | ```
67 | export ABLY_PROTOCOL="application/json" && go test -tags=integration -p 1 -race -v -timeout 120m ./...
68 | export ABLY_PROTOCOL="application/x-msgpack" && go test -tags=integration -p 1 -race -v -timeout 120m ./...
69 | ```
70 |
71 | Depending on which protocol they are to be run for. It is also necessary to clean the test cache in between runs of these tests which can be done with the command:
72 |
73 | ```
74 | go clean -testcache
75 | ```
76 |
77 | When adding new integration tests, the following build tag must be included at the top of the file to exclude these tests for running in CI as part of the Unit test step.
78 |
79 | ```
80 | //go:build !unit
81 | // +build !unit
82 | ```
83 |
84 | ### Release process
85 |
86 | Starting with release 1.2, this library uses [semantic versioning](http://semver.org/). For each release, the following needs to be done:
87 |
88 | 1. Create a branch for the release, named like `release/1.2.3` (where `1.2.3` is the new version number)
89 | 2. Replace all references of the current version number with the new version number and commit the changes
90 | 3. Run [`github_changelog_generator`](https://github.com/github-changelog-generator/github-changelog-generator) to automate the update of the [CHANGELOG](./CHANGELOG.md). This may require some manual intervention, both in terms of how the command is run and how the change log file is modified. Your mileage may vary:
91 | - The command you will need to run will look something like this: `github_changelog_generator -u ably -p ably-go --since-tag v1.2.3 --output delta.md --token $GITHUB_TOKEN_WITH_REPO_ACCESS`. Generate token [here](https://github.com/settings/tokens/new?description=GitHub%20Changelog%20Generator%20token).
92 | - Using the command above, `--output delta.md` writes changes made after `--since-tag` to a new file
93 | - The contents of that new file (`delta.md`) then need to be manually inserted at the top of the `CHANGELOG.md`, changing the "Unreleased" heading and linking with the current version numbers
94 | - Also ensure that the "Full Changelog" link points to the new version tag instead of the `HEAD`
95 | 4. Commit this change: `git add CHANGELOG.md && git commit -m "Update change log."`
96 | 5. Make a PR against `main`
97 | 6. Once the PR is approved, merge it into `main`
98 | 7. Add a tag to the new `main` head commit and push to origin such as `git tag v1.2.3 && git push origin v1.2.3`
99 | 8. Create the release on Github, from the new tag, including populating the release notes
100 | 9. Create the entry on the [Ably Changelog](https://changelog.ably.com/) (via [headwayapp](https://headwayapp.co/))
101 |
--------------------------------------------------------------------------------
/COPYRIGHT:
--------------------------------------------------------------------------------
1 | Copyright 2015-2022 Ably Real-time Ltd (ably.com)
2 |
--------------------------------------------------------------------------------
/MAINTAINERS.md:
--------------------------------------------------------------------------------
1 | This repository is owned by the Ably SDK team.
2 |
--------------------------------------------------------------------------------
/UPDATING.md:
--------------------------------------------------------------------------------
1 | # Upgrade / Migration Guide
2 |
3 | ## Version 1.1.5 to 1.2.0
4 |
5 | We have made many **breaking changes** in the version 1.2 release of this SDK.
6 |
7 | In this guide we aim to highlight the main differences you will encounter when migrating your code from the interfaces we were offering prior to the [version 1.2.0 release](https://github.com/ably/ably-go/releases/tag/v1.2.0).
8 |
9 | These include:
10 |
11 | - Changes to numerous function and method signatures - these are breaking changes, in that your code will need to change to use them in their new form
12 | - Adoption of the [Context Concurrency Pattern](https://blog.golang.org/context) using the [context package](https://pkg.go.dev/context) - e.g. [ably.RealtimeChannel.Subscribe](https://pkg.go.dev/github.com/ably/ably-go/ably#RealtimeChannel.Subscribe)
13 | - Adoption of the [Functional Options Pattern](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) - e.g. [ably.ClientOption](https://pkg.go.dev/github.com/ably/ably-go/ably#ClientOption)
14 |
15 | ### Asynchronous Operations and the `Context` Concurrency Pattern
16 |
17 | We've now moved to a model where methods are blocking in nature, cancellable with a [Context](https://pkg.go.dev/context#Context).
18 |
19 | We use `ctx` in examples to refer to a `Context` instance that you create in your code.
20 |
21 | For robust, production-ready applications you will rarely (actually, probably _never_) want to create your `Context` using the basic [Background](https://pkg.go.dev/context#Background) function, because it cannot be cancelled and remains for the lifecycle of the program. Instead, you should use [WithTimeout](https://pkg.go.dev/context#WithTimeout) or [WithDeadline](https://pkg.go.dev/context#WithDeadline).
22 |
23 | For example, a context can be created with a 10-second timeout like this:
24 |
25 | ```go
26 | context.WithTimeout(context.Background(), 10 * time.Second)
27 | ```
28 |
29 | Adding the necessary code to stay on top of cancellation, you will need something like this:
30 |
31 | ```go
32 | ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
33 | defer cancel()
34 | ```
35 |
36 | This way, the context can be cancelled at the close of the function.
37 |
38 | **Note**:
39 | For Realtime operations, cancellation isn't supported by the underlying protocol.
40 | A context cancellation in this case just means that the call returns immediately, without waiting for the operation to finish, with the error provided by the context; but the operation carries on in the background.
41 | Timeouts for these operations are handled separately, with [a configurable realtime request timeout duration](https://pkg.go.dev/github.com/ably/ably-go/ably#WithRealtimeRequestTimeout).
42 |
43 | ### Client Options now has a Functional Interface
44 |
45 | Before version 1.2.0, you instantiated a client using a `ClientOptions` instance created with the `NewClientOptions` function:
46 |
47 | ```go
48 | client, err := ably.NewRealtime(ably.NewClientOptions("xxx:xxx"))
49 | ```
50 |
51 | **Starting with version 1.2.0**, you must use the new functional interface:
52 |
53 | ```go
54 | client, err := ably.NewRealtime(ably.WithKey("xxx:xxx"))
55 | ```
56 |
57 | For a full list of client options, see all functions prefixed by `With` that return a `ClientOption` function
58 | [in the API reference](https://pkg.go.dev/github.com/ably/ably-go/ably#ClientOption).
59 |
60 | ### Subscription now uses Message Handlers
61 |
62 | Before version 1.2.0, you subscribed to receive all messages from a Go channel like this:
63 |
64 | ```go
65 | sub, _ := channel.Subscribe()
66 | for msg := range sub.MessageChannel() {
67 | fmt.Println("Received message: ", msg)
68 | }
69 | ```
70 |
71 | **Starting with version 1.2.0**, you must supply a context and your own message handler function to the new [SubscribeAll](https://pkg.go.dev/github.com/ably/ably-go/ably#RealtimeChannel.SubscribeAll) method:
72 |
73 | ```go
74 | unsubscribe, _ := channel.SubscribeAll(ctx, func(msg *ably.Message) {
75 | fmt.Println("Received message: ", msg)
76 | })
77 | ```
78 |
79 | The signature of the [Subscribe](https://pkg.go.dev/github.com/ably/ably-go/ably#RealtimeChannel.Subscribe) method has also changed. It now requires a [Context](https://pkg.go.dev/context#Context) as well as the channel name and your message handler.
80 |
81 | Both `Subscribe` and `SubscribeAll` are now blocking methods.
82 |
83 | Detail around the concurrency and routing of calls to message and event handlers is described in the package documentation
84 | under [Event Emitters](https://pkg.go.dev/github.com/ably/ably-go/ably#hdr-Event_Emitters).
85 |
86 | ### The `Publish` Method now Blocks
87 |
88 | Before version 1.2.0, you published messages to a channel by calling the `Publish` method and then waiting for the `Result`:
89 |
90 | ```go
91 | result, _ := channel.Publish("EventName1", "EventData1")
92 |
93 | // block until the publish operation completes
94 | result.Wait()
95 | ```
96 |
97 | **Starting with version 1.2.0**, you must supply a context, as this method is now blocking:
98 |
99 | ```go
100 | // block until the publish operation completes or is cancelled
101 | err := channel.Publish(ctx, "EventName1", "EventData1")
102 | ```
103 |
104 | ### Querying History
105 |
106 | Before version 1.2.0, you queried history as shown below, receiving a `PaginatedResult` from the `History` function:
107 |
108 | ```go
109 | page, err := channel.History(nil)
110 | for ; err == nil; page, err = page.Next() {
111 | for _, message := range page.Messages() {
112 | fmt.Println("Message from History: ", message)
113 | }
114 | }
115 | if err != nil {
116 | panic(err)
117 | }
118 | ```
119 |
120 | **Starting with version 1.2.0**, you must first call `History`, which can be passed [functional options](https://pkg.go.dev/github.com/ably/ably-go/ably#HistoryOption), and then use the [Pages](https://pkg.go.dev/github.com/ably/ably-go/ably#HistoryRequest.Pages) (or [Items](https://pkg.go.dev/github.com/ably/ably-go/ably#HistoryRequest.Items), if you prefer) method on the returned `HistoryRequest` instance
121 |
122 | ```go
123 | pages, err := channel.History().Pages(ctx)
124 | if err != nil {
125 | panic(err)
126 | }
127 | for pages.Next(ctx) {
128 | for _, message := range pages.Items() {
129 | fmt.Println("Message from History: ", message)
130 | }
131 | }
132 | if err := pages.Err(); err != nil {
133 | panic(err)
134 | }
135 | ```
136 |
--------------------------------------------------------------------------------
/ably/ably.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/hex"
6 | "time"
7 | )
8 |
9 | func min(i, j int) int {
10 | if i < j {
11 | return i
12 | }
13 | return j
14 | }
15 |
16 | func max(i, j int) int {
17 | if i > j {
18 | return i
19 | }
20 | return j
21 | }
22 |
23 | func nonil(err ...error) error {
24 | for _, err := range err {
25 | if err != nil {
26 | return err
27 | }
28 | }
29 | return nil
30 | }
31 |
32 | func nonempty(s ...string) string {
33 | for _, s := range s {
34 | if s != "" {
35 | return s
36 | }
37 | }
38 | return ""
39 | }
40 |
41 | func randomString(n int) string {
42 | p := make([]byte, n/2+1)
43 | rand.Read(p)
44 | return hex.EncodeToString(p)[:n]
45 | }
46 |
47 | // unixMilli returns the given time as a timestamp in milliseconds since epoch.
48 | func unixMilli(t time.Time) int64 {
49 | return t.UnixNano() / int64(time.Millisecond)
50 | }
51 |
--------------------------------------------------------------------------------
/ably/auth_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "context"
8 | "errors"
9 | "testing"
10 | "time"
11 |
12 | "github.com/ably/ably-go/ably"
13 |
14 | "github.com/stretchr/testify/assert"
15 | )
16 |
17 | func TestAuth_TimestampRSA10k(t *testing.T) {
18 | now, err := time.Parse(time.RFC822, time.RFC822)
19 | assert.NoError(t, err)
20 |
21 | t.Run("must use local time when UseQueryTime is false", func(t *testing.T) {
22 |
23 | rest, _ := ably.NewREST(
24 | ably.WithKey("fake:key"),
25 | ably.WithNow(func() time.Time {
26 | return now
27 | }))
28 | a := rest.Auth
29 | a.SetServerTimeFunc(func() (time.Time, error) {
30 | return now.Add(time.Minute), nil
31 | })
32 | stamp, err := a.Timestamp(context.Background(), false)
33 | assert.NoError(t, err)
34 | assert.True(t, stamp.Equal(now),
35 | "expected %s got %s", now, stamp)
36 | })
37 | t.Run("must use server time when UseQueryTime is true", func(t *testing.T) {
38 |
39 | rest, _ := ably.NewREST(
40 | ably.WithKey("fake:key"),
41 | ably.WithNow(func() time.Time {
42 | return now
43 | }))
44 | a := rest.Auth
45 | a.SetServerTimeFunc(func() (time.Time, error) {
46 | return now.Add(time.Minute), nil
47 | })
48 | stamp, err := rest.Timestamp(true)
49 | assert.NoError(t, err)
50 | serverTime := now.Add(time.Minute)
51 | assert.True(t, stamp.Equal(serverTime),
52 | "expected %s got %s", serverTime, stamp)
53 | })
54 | t.Run("must use server time offset ", func(t *testing.T) {
55 |
56 | now := now
57 | rest, _ := ably.NewREST(
58 | ably.WithKey("fake:key"),
59 | ably.WithNow(func() time.Time {
60 | return now
61 | }))
62 | a := rest.Auth
63 | a.SetServerTimeFunc(func() (time.Time, error) {
64 | return now.Add(time.Minute), nil
65 | })
66 | stamp, err := rest.Timestamp(true)
67 | assert.NoError(t, err)
68 | serverTime := now.Add(time.Minute)
69 | assert.True(t, stamp.Equal(serverTime),
70 | "expected %s got %s", serverTime, stamp)
71 |
72 | now = now.Add(time.Minute)
73 | a.SetServerTimeFunc(func() (time.Time, error) {
74 | return time.Time{}, errors.New("must not be called")
75 | })
76 | stamp, err = rest.Timestamp(true)
77 | assert.NoError(t, err)
78 | serverTime = now.Add(time.Minute)
79 | assert.True(t, stamp.Equal(serverTime),
80 | "expected %s got %s", serverTime, stamp)
81 | })
82 | }
83 |
84 | func TestAuth_ClientID_Error(t *testing.T) {
85 | opts := []ably.ClientOption{
86 | ably.WithClientID("*"),
87 | ably.WithKey("abc:abc"),
88 | ably.WithUseTokenAuth(true),
89 | }
90 | _, err := ably.NewRealtime(opts...)
91 | err = checkError(40102, err)
92 | assert.NoError(t, err)
93 | }
94 |
--------------------------------------------------------------------------------
/ably/crypto.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "crypto/rand"
5 | "errors"
6 | "fmt"
7 | "io"
8 | )
9 |
10 | // Crypto contains the properties required to configure the encryption of [ably.Message] payloads.
11 | var Crypto struct {
12 |
13 | // GenerateRandomKey returns a random key (as a binary/a byte array) to be used in the encryption of the channel.
14 | // If the language cryptographic randomness primitives are blocking or async, a callback is used.
15 | // The callback returns a generated binary key.
16 | // keyLength is passed as a param. It is a length of the key, in bits, to be generated.
17 | // If not specified, this is equal to the default keyLength of the default algorithm:
18 | // for AES this is 256 bits (RSE2).
19 | GenerateRandomKey func(keyLength int) ([]byte, error)
20 |
21 | // GetDefaultParams returns a [ably.CipherParams] object, using the default values for any fields
22 | // not supplied by the [ably.CipherParamOptions] object. The Key field must be provided (RSE1).
23 | GetDefaultParams func(CipherParams) CipherParams
24 | }
25 |
26 | func init() {
27 | Crypto.GenerateRandomKey = generateRandomKey
28 | Crypto.GetDefaultParams = defaultCipherParams
29 | }
30 |
31 | func generateRandomKey(keyLength int) ([]byte, error) {
32 | if keyLength <= 0 {
33 | keyLength = defaultCipherKeyLength
34 | }
35 | key := make([]byte, keyLength/8)
36 | if _, err := io.ReadFull(rand.Reader, key); err != nil {
37 | return nil, err
38 | }
39 | return key, nil
40 | }
41 |
42 | func defaultCipherParams(c CipherParams) CipherParams {
43 | if len(c.Key) == 0 {
44 | panic(errors.New("cipher key must be provided"))
45 | }
46 | if c.Algorithm == 0 {
47 | c.Algorithm = defaultCipherAlgorithm
48 | }
49 | c.KeyLength = len(c.Key) * 8
50 | if !c.Algorithm.isValidKeyLength(c.KeyLength) {
51 | panic(fmt.Sprintf("invalid key length for algorithm %v: %d", c.Algorithm, c.KeyLength))
52 | }
53 | if c.Mode == 0 {
54 | c.Mode = defaultCipherMode
55 | }
56 | return c
57 | }
58 |
--------------------------------------------------------------------------------
/ably/crypto_spec_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "crypto/aes"
8 | "testing"
9 |
10 | "github.com/ably/ably-go/ably"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func TestCrypto_RSE1_GetDefaultParams(t *testing.T) {
16 |
17 | for _, c := range []struct {
18 | name string
19 | in ably.CipherParams
20 | expected ably.CipherParams
21 | expectedPanic bool
22 | }{
23 | {
24 | name: "RSE1a, RSE1b, RSE1d: sets defaults",
25 | in: ably.CipherParams{
26 | Key: make([]byte, 256/8),
27 | },
28 | expected: ably.CipherParams{
29 | Key: make([]byte, 256/8),
30 | KeyLength: 256,
31 | Algorithm: ably.CipherAES,
32 | Mode: ably.CipherCBC,
33 | },
34 | },
35 | {
36 | name: "RSE1b: no key panics",
37 | in: ably.CipherParams{
38 | Algorithm: ably.CipherAES,
39 | Mode: ably.CipherCBC,
40 | },
41 | expectedPanic: true,
42 | },
43 | {
44 | name: "RSE1e: wrong key length panics (AES 256)",
45 | in: ably.CipherParams{
46 | Key: make([]byte, 256/8-1),
47 | },
48 | expectedPanic: true,
49 | },
50 | {
51 | name: "RSE1e: valid key length works (AES 128)",
52 | in: ably.CipherParams{
53 | Key: make([]byte, 128/8),
54 | },
55 | expected: ably.CipherParams{
56 | Key: make([]byte, 128/8),
57 | KeyLength: 128,
58 | Algorithm: ably.CipherAES,
59 | Mode: ably.CipherCBC,
60 | },
61 | },
62 | } {
63 | c := c
64 | t.Run(c.name, func(t *testing.T) {
65 |
66 | defer func() {
67 | r := recover()
68 | if r != nil && !c.expectedPanic {
69 | panic(r)
70 | } else if r == nil && c.expectedPanic {
71 | t.Fatal("expected panic")
72 | }
73 | }()
74 |
75 | got := ably.Crypto.GetDefaultParams(c.in)
76 | assert.Equal(t, c.expected, got,
77 | "expected: %#v; got: %#v", c.expected, got)
78 | })
79 | }
80 | }
81 |
82 | func TestCrypto_RSE2_GenerateRandomKey(t *testing.T) {
83 | t.Run("must use default key length", func(ts *testing.T) {
84 | key, err := ably.Crypto.GenerateRandomKey(0)
85 | assert.NoError(ts, err)
86 | bitCount := len(key) * 8
87 | assert.Equal(ts, ably.DefaultCipherKeyLength, bitCount,
88 | "expected %d got %d", ably.DefaultCipherKeyLength, bitCount)
89 | })
90 | t.Run("must use optional key length", func(ts *testing.T) {
91 | keyLength := 128
92 | key, err := ably.Crypto.GenerateRandomKey(keyLength)
93 | assert.NoError(ts, err)
94 | bitCount := len(key) * 8
95 | assert.Equal(ts, 128, bitCount,
96 | "expected %d got %d", keyLength, bitCount)
97 | })
98 | }
99 |
100 | func Test_Issue330_IVReuse(t *testing.T) {
101 |
102 | params, err := ably.DefaultCipherParams()
103 | assert.NoError(t, err)
104 | cipher, err := ably.NewCBCCipher(*params)
105 | assert.NoError(t, err)
106 | cipherText1, err := cipher.Encrypt([]byte("foo"))
107 | assert.NoError(t, err)
108 | cipherText2, err := cipher.Encrypt([]byte("foo"))
109 | assert.NoError(t, err)
110 | iv1 := string(cipherText1[:aes.BlockSize])
111 | iv2 := string(cipherText2[:aes.BlockSize])
112 | assert.NotEqual(t, iv1, iv2, "IV shouldn't be reused")
113 | }
114 |
--------------------------------------------------------------------------------
/ably/doc.go:
--------------------------------------------------------------------------------
1 | // Package ably
2 | //
3 | // # Ably Go Client Library SDK API Reference
4 | //
5 | // The Go Client Library SDK supports a realtime and a REST interface. The Go API references are generated from the [Ably Go Client Library SDK source code] using [godoc] and structured by classes.
6 | //
7 | // The realtime interface enables a client to maintain a persistent connection to Ably and publish, subscribe and be present on channels. The REST interface is stateless and typically implemented server-side. It is used to make requests such as retrieving statistics, token authentication and publishing to a channel.
8 | //
9 | // View the [Ably docs] for conceptual information on using Ably, and for API references featuring all languages. The combined [API references] are organized by features and split between the [realtime] and [REST] interfaces.
10 | //
11 | // Get started at https://github.com/ably/ably-go#using-the-realtime-api.
12 | //
13 | // # Event Emitters
14 | //
15 | // An event emitter pattern appears in multiple places in the library.
16 | //
17 | // The On method takes an event type identifier and a handler function to be
18 | // called with the event's associated data whenever an event of that type is
19 | // emitted in the event emitter. It also returns an "off" function to undo this
20 | // operation, so that the handler function isn't called anymore.
21 | //
22 | // The OnAll method is like On, but for events of all types.
23 | //
24 | // The Once method works like On, except the handler is just called once, for
25 | // the first matching event.
26 | //
27 | // OnceAll is like OnAll in the same way Once is like On.
28 | //
29 | // The Off method is like calling the "off" function returned by calls to On and
30 | // Once methods with a matching event type identifier.
31 | //
32 | // The OffAll method is like Off, except it is like calling all the "off"
33 | // functions.
34 | //
35 | // Each handler is assigned its own sequential queue of events. That is, any
36 | // given handler function will not receive calls from different goroutines that
37 | // run concurrently; you can count on the next call to a handler to happen
38 | // after the previous call has returned, and you can count on events or
39 | // messages to be delivered to the handler in the same order they were emitted.
40 | // Different handlers may be called concurrently, though.
41 | //
42 | // Calling any of these methods an "off" function inside a handler will only
43 | // have effect for subsequent events.
44 | //
45 | // For messages and presence messages, "on" is called "subscribe" and "off" is
46 | // called "unsubscribe".
47 | //
48 | // # Paginated results
49 | //
50 | // Most requests to the Ably REST API return a single page of results, with
51 | // hyperlinks to the first and next pages in the whole collection of results.
52 | // To facilitate navigating through these pages, the library provides access to
53 | // such paginated results though a common pattern.
54 | //
55 | // A method that prepares a paginated request returns a Request object with two
56 | // methods: Pages and Items. Pages returns a PaginatedResult, an iterator that,
57 | // on each iteration, yields a whole page of results. Items is simply a
58 | // convenience wrapper that yields single results instead.
59 | //
60 | // In both cases, calling the method validates the request and may return an
61 | // error.
62 | //
63 | // Then, for accessing the results, the Next method from the resulting
64 | // iterator object must be called repeatedly; each time it returns true, the
65 | // result that has been retrieved can be inspected with the Items or Item method
66 | // from the iterator object. Finally, once it returns false, the Err method must
67 | // be called to check if the iterator stopped due to some error, or else, it
68 | // just finished going through all pages.
69 | //
70 | // Calling the First method on the PaginatedResults returns the first page of the
71 | // results. However, the Next method has to be called before inspecting the items.
72 | //
73 | // For every page in the PaginatedResults, the HasNext method can be called to check if there
74 | // are more page(s) available. IsLast method checks if the page is the
75 | // last page. Both methods return a true or false value.
76 | //
77 | // See the PaginatedResults example.
78 | //
79 | // [Ably Go Client Library SDK source code]: https://github.com/ably/ably-go/
80 | // [godoc]: https://pkg.go.dev/golang.org/x/tools/cmd/godoc
81 | // [Ably docs]: https://ably.com/docs/
82 | // [API references]: https://ably.com/docs/api/
83 | // [realtime]: https://ably.com/docs/api/realtime-sdk?lang=go
84 | // [REST]: https://ably.com/docs/api/rest-sdk?lang=go
85 | package ably
86 |
--------------------------------------------------------------------------------
/ably/doc_test.go:
--------------------------------------------------------------------------------
1 | package ably_test
2 |
3 | import (
4 | "fmt"
5 |
6 | "context"
7 | "github.com/ably/ably-go/ably"
8 | )
9 |
10 | func Example_paginatedResults() {
11 | ctx := context.Background()
12 | client, err := ably.NewRealtime(ably.WithKey("xxx:xxx"))
13 | if err != nil {
14 | panic(err)
15 | }
16 | channel := client.Channels.Get("persisted:test")
17 |
18 | err = channel.Publish(ctx, "EventName1", "EventData1")
19 | if err != nil {
20 | panic(err)
21 | }
22 |
23 | pages, err := channel.History().Pages(ctx)
24 | if err != nil {
25 | panic(err)
26 | }
27 |
28 | // Returning and iterating over the first page
29 | pages.Next(ctx)
30 | pages.First(ctx)
31 | for _, message := range pages.Items() {
32 | fmt.Println(message)
33 | }
34 |
35 | // Iteration over pages in PaginatedResult
36 | for pages.Next(ctx) {
37 | fmt.Println(pages.HasNext(ctx))
38 | fmt.Println(pages.IsLast(ctx))
39 | for _, presence := range pages.Items() {
40 | fmt.Println(presence)
41 | }
42 | }
43 | if err := pages.Err(); err != nil {
44 | panic(err)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/ably/error.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "io"
7 | "mime"
8 | "net"
9 | "net/http"
10 | "strings"
11 | )
12 |
13 | func (code ErrorCode) toStatusCode() int {
14 | switch status := int(code) / 100; status {
15 | case
16 | http.StatusBadRequest,
17 | http.StatusUnauthorized,
18 | http.StatusForbidden,
19 | http.StatusNotFound,
20 | http.StatusMethodNotAllowed,
21 | http.StatusInternalServerError:
22 | return status
23 | default:
24 | return 0
25 | }
26 | }
27 |
28 | func codeFromStatus(statusCode int) ErrorCode {
29 | switch code := ErrorCode(statusCode * 100); code {
30 | case
31 | ErrBadRequest,
32 | ErrUnauthorized,
33 | ErrForbidden,
34 | ErrNotFound,
35 | ErrMethodNotAllowed,
36 | ErrInternalError:
37 | return code
38 | default:
39 | return ErrNotSet
40 | }
41 | }
42 |
43 | // ErrorInfo is a generic Ably error object that contains an Ably-specific status code, and a generic status code.
44 | // Errors returned from the Ably server are compatible with the ErrorInfo structure and should result in errors
45 | // that inherit from ErrorInfo. It may contain underlying error value which caused the failure.
46 | type ErrorInfo struct {
47 | // StatusCode is a http Status code corresponding to this error, where applicable (TI1).
48 | StatusCode int
49 | // Code is the standard [ably error code]
50 | // [ably error code]: https://github.com/ably/ably-common/blob/main/protocol/errors.json (TI1).
51 | Code ErrorCode
52 | // HRef is included for REST responses to provide a URL for additional help on the error code (TI4).
53 | HRef string
54 | // Cause provides Information pertaining to what caused the error where available (TI1).
55 | Cause *ErrorInfo
56 | // err is the application-level error we're wrapping, or just a message.
57 | // If Cause is non-nil, err == *Cause.
58 | err error
59 | }
60 |
61 | // Error implements the builtin error interface.
62 | func (e ErrorInfo) Error() string {
63 | errorHref := e.HRef
64 | if errorHref == "" && e.Code != 0 {
65 | errorHref = fmt.Sprintf("https://help.ably.io/error/%d", e.Code)
66 | }
67 | msg := e.Message()
68 | var see string
69 | if !strings.Contains(msg, errorHref) {
70 | see = " See " + errorHref
71 | }
72 | return fmt.Sprintf("[ErrorInfo :%s code=%d %[2]v statusCode=%d]%s", msg, e.Code, e.StatusCode, see)
73 | }
74 |
75 | // Unwrap implements the implicit interface that errors.Unwrap understands.
76 | func (e ErrorInfo) Unwrap() error {
77 | return e.err
78 | }
79 |
80 | // Message returns the undecorated error message.
81 | func (e ErrorInfo) Message() string {
82 | if e.err == nil {
83 | return ""
84 | }
85 | return e.err.Error()
86 | }
87 |
88 | func newError(defaultCode ErrorCode, err error) *ErrorInfo {
89 | switch err := err.(type) {
90 | case *ErrorInfo:
91 | return err
92 | case net.Error:
93 | if err.Timeout() {
94 | return &ErrorInfo{Code: ErrTimeoutError, StatusCode: 500, err: err}
95 | }
96 | }
97 | return &ErrorInfo{
98 | Code: defaultCode,
99 | StatusCode: defaultCode.toStatusCode(),
100 |
101 | err: err,
102 | }
103 | }
104 |
105 | func newErrorf(code ErrorCode, format string, v ...interface{}) *ErrorInfo {
106 | return newError(code, fmt.Errorf(format, v...))
107 | }
108 |
109 | func newErrorFromProto(err *errorInfo) *ErrorInfo {
110 | if err == nil {
111 | return nil
112 | }
113 | return &ErrorInfo{
114 | Code: ErrorCode(err.Code),
115 | StatusCode: err.StatusCode,
116 | HRef: err.HRef,
117 | err: errors.New(err.Message),
118 | }
119 | }
120 |
121 | func (e *ErrorInfo) unwrapNil() error {
122 | if e == nil {
123 | return nil
124 | }
125 | return e
126 | }
127 |
128 | func code(err error) ErrorCode {
129 | if e, ok := err.(*ErrorInfo); ok {
130 | return e.Code
131 | }
132 | return ErrNotSet
133 | }
134 |
135 | func statusCode(err error) int {
136 | if e, ok := err.(*ErrorInfo); ok {
137 | return e.StatusCode
138 | }
139 | return 0
140 | }
141 |
142 | func errFromUnprocessableBody(resp *http.Response) error {
143 | errMsg, err := io.ReadAll(resp.Body)
144 | if err == nil {
145 | err = errors.New(string(errMsg))
146 | }
147 | return &ErrorInfo{Code: ErrBadRequest, StatusCode: resp.StatusCode, err: err}
148 | }
149 |
150 | func isTimeoutOrDnsErr(err error) bool {
151 | var netErr net.Error
152 | if errors.As(err, &netErr) {
153 | if netErr.Timeout() { // RSC15l2
154 | return true
155 | }
156 | }
157 | var dnsErr *net.DNSError
158 | return errors.As(err, &dnsErr) // RSC15l1
159 | }
160 |
161 | func checkValidHTTPResponse(resp *http.Response) error {
162 | type errorBody struct {
163 | Error errorInfo `json:"error,omitempty" codec:"error,omitempty"`
164 | }
165 | if resp.StatusCode < 300 {
166 | return nil
167 | }
168 | defer resp.Body.Close()
169 | typ, _, mimeErr := mime.ParseMediaType(resp.Header.Get("Content-Type"))
170 | if mimeErr != nil {
171 | return &ErrorInfo{
172 | Code: 50000,
173 | StatusCode: resp.StatusCode,
174 | err: mimeErr,
175 | }
176 | }
177 | if typ != protocolJSON && typ != protocolMsgPack {
178 | return errFromUnprocessableBody(resp)
179 | }
180 |
181 | body := &errorBody{}
182 | if err := decode(typ, resp.Body, &body); err != nil {
183 | return newError(codeFromStatus(resp.StatusCode), errors.New(http.StatusText(resp.StatusCode)))
184 | }
185 |
186 | err := newErrorFromProto(&body.Error)
187 | if body.Error.Message != "" {
188 | err.err = errors.New(body.Error.Message)
189 | }
190 | if err.Code == 0 && err.StatusCode == 0 {
191 | err.Code, err.StatusCode = codeFromStatus(resp.StatusCode), resp.StatusCode
192 | }
193 | return err
194 | }
195 |
--------------------------------------------------------------------------------
/ably/error_names.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | // Error constants used in ably-go.
4 | // These used to be generated from the error description text.
5 | // But this means that the error name changes if we update the text, and that some names are too long
6 |
7 | const (
8 | ErrNotSet ErrorCode = 0
9 | ErrBadRequest ErrorCode = 40000
10 | ErrInvalidCredential ErrorCode = 40005
11 | ErrInvalidClientID ErrorCode = 40012
12 | ErrUnauthorized ErrorCode = 40100
13 | ErrInvalidCredentials ErrorCode = 40101
14 | ErrIncompatibleCredentials ErrorCode = 40102
15 | ErrInvalidUseOfBasicAuthOverNonTLSTransport ErrorCode = 40103
16 | ErrTokenErrorUnspecified ErrorCode = 40140
17 | ErrErrorFromClientTokenCallback ErrorCode = 40170
18 | ErrForbidden ErrorCode = 40300
19 | ErrNotFound ErrorCode = 40400
20 | ErrMethodNotAllowed ErrorCode = 40500
21 | ErrInternalError ErrorCode = 50000
22 | ErrInternalChannelError ErrorCode = 50001
23 | ErrInternalConnectionError ErrorCode = 50002
24 | ErrTimeoutError ErrorCode = 50003
25 | ErrConnectionFailed ErrorCode = 80000
26 | ErrConnectionSuspended ErrorCode = 80002
27 | ErrConnectionClosed ErrorCode = 80017
28 | ErrDisconnected ErrorCode = 80003
29 | ErrProtocolError ErrorCode = 80013
30 | ErrChannelOperationFailed ErrorCode = 90000
31 | ErrChannelOperationFailedInvalidChannelState ErrorCode = 90001
32 | )
33 |
--------------------------------------------------------------------------------
/ably/error_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "context"
8 | "errors"
9 | "fmt"
10 | "net"
11 | "net/http"
12 | "net/http/httptest"
13 | "net/url"
14 | "strconv"
15 | "strings"
16 | "testing"
17 |
18 | "github.com/ably/ably-go/ably"
19 | "github.com/stretchr/testify/assert"
20 | )
21 |
22 | func TestErrorResponseWithInvalidKey(t *testing.T) {
23 | opts := []ably.ClientOption{ably.WithKey(":")}
24 | _, e := ably.NewREST(opts...)
25 | assert.Error(t, e,
26 | "NewREST(): expected err != nil")
27 | err, ok := e.(*ably.ErrorInfo)
28 | assert.True(t, ok,
29 | "want e be *ably.Error; was %T", e)
30 | assert.Equal(t, 400, err.StatusCode,
31 | "want StatusCode=400; got %d", err.StatusCode)
32 | assert.Equal(t, ably.ErrInvalidCredential, err.Code,
33 | "want Code=40005; got %d", err.Code)
34 | assert.NotNil(t, err.Unwrap())
35 | }
36 |
37 | func TestIssue127ErrorResponse(t *testing.T) {
38 | // Start a local HTTP server
39 | errMsg := "This is an html error body"
40 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
41 | // Test request parameters
42 | assert.Equal(t, "/time", req.URL.String())
43 | // Send response to be tested
44 | rw.Header().Set("Content-Type", "text/html")
45 | rw.WriteHeader(400)
46 | rw.Write([]byte(fmt.Sprintf("
%s", errMsg)))
47 | }))
48 | // Close the server when test finishes
49 | defer server.Close()
50 |
51 | endpointURL, err := url.Parse(server.URL)
52 | assert.Nil(t, err)
53 | opts := []ably.ClientOption{
54 | ably.WithKey("xxxxxxx.yyyyyyy:zzzzzzz"),
55 | ably.WithTLS(false),
56 | ably.WithUseTokenAuth(true),
57 | ably.WithEndpoint(endpointURL.Hostname()),
58 | }
59 | port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0)
60 | opts = append(opts, ably.WithPort(int(port)))
61 | client, err := ably.NewREST(opts...)
62 | assert.NoError(t, err)
63 |
64 | _, err = client.Time(context.Background())
65 | assert.Error(t, err)
66 | assert.Contains(t, err.Error(), errMsg)
67 | }
68 |
69 | func TestErrorInfo(t *testing.T) {
70 | t.Run("without an error code", func(t *testing.T) {
71 |
72 | e := &ably.ErrorInfo{
73 | StatusCode: 401,
74 | }
75 | assert.NotContains(t, e.Error(), "help.ably.io",
76 | "expected error message not to contain \"help.ably.io\"")
77 | })
78 | t.Run("with an error code", func(t *testing.T) {
79 |
80 | e := &ably.ErrorInfo{
81 | Code: 44444,
82 | }
83 | assert.Contains(t, e.Error(), "https://help.ably.io/error/44444",
84 | "expected error message %s to contain \"https://help.ably.io/error/44444\"", e.Error())
85 | })
86 | t.Run("with an error code and an href attribute", func(t *testing.T) {
87 |
88 | e := &ably.ErrorInfo{
89 | Code: 44444,
90 | HRef: "http://foo.bar.com/",
91 | }
92 | assert.NotContains(t, e.Error(), "https://help.ably.io/error/44444",
93 | "expected error message %s not to contain \"https://help.ably.io/error/44444\"", e.Error())
94 | assert.Contains(t, e.Error(), "http://foo.bar.com/",
95 | "expected error message %s to contain \"http://foo.bar.com/\"", e.Error())
96 | })
97 |
98 | t.Run("with an error code and a message with the same error URL", func(t *testing.T) {
99 |
100 | e := ably.NewErrorInfo(44444, errors.New("error https://help.ably.io/error/44444"))
101 | assert.Contains(t, e.Error(), "https://help.ably.io/error/44444",
102 | "expected error message %s to contain \"https://help.ably.io/error/44444\"", e.Error())
103 |
104 | count := strings.Count(e.Error(), "https://help.ably.io/error/44444")
105 | assert.Equal(t, 1, count, "expected 1 occurrence of \"https://help.ably.io/error/44444\" got %d", count)
106 |
107 | })
108 | t.Run("with an error code and a message with a different error URL", func(t *testing.T) {
109 |
110 | e := ably.NewErrorInfo(44444, errors.New("error https://help.ably.io/error/123123"))
111 | assert.Contains(t, e.Error(), "https://help.ably.io/error/44444",
112 | "expected error message %s to contain \"https://help.ably.io/error/44444\"", e.Error())
113 | count := strings.Count(e.Error(), "help.ably.io")
114 | assert.Equal(t, 2, count,
115 | "expected 2 occurrences of \"help.ably.io\" got %d", count)
116 | count = strings.Count(e.Error(), "/123123")
117 | assert.Equal(t, 1, count,
118 | "expected 1 occurence of \"/123123\" got %d", count)
119 | count = strings.Count(e.Error(), "/44444")
120 | assert.Equal(t, 1, count, "expected 1 occurence of \"/44444\" got %d", count)
121 | })
122 | }
123 |
124 | func TestIssue_154(t *testing.T) {
125 | server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
126 | rw.Header().Set("Content-Type", "text/html")
127 | rw.WriteHeader(http.StatusMethodNotAllowed)
128 | }))
129 | defer server.Close()
130 |
131 | endpointURL, err := url.Parse(server.URL)
132 | assert.NoError(t, err)
133 | opts := []ably.ClientOption{
134 | ably.WithKey("xxxxxxx.yyyyyyy:zzzzzzz"),
135 | ably.WithTLS(false),
136 | ably.WithUseTokenAuth(true),
137 | ably.WithEndpoint(endpointURL.Hostname()),
138 | }
139 | port, _ := strconv.ParseInt(endpointURL.Port(), 10, 0)
140 | opts = append(opts, ably.WithPort(int(port)))
141 | client, e := ably.NewREST(opts...)
142 | assert.NoError(t, e)
143 |
144 | _, err = client.Time(context.Background())
145 | assert.Error(t, err)
146 | et := err.(*ably.ErrorInfo)
147 | assert.Equal(t, http.StatusMethodNotAllowed, et.StatusCode,
148 | "expected %d got %d: %v", http.StatusMethodNotAllowed, et.StatusCode, err)
149 | }
150 |
151 | func Test_DNSOrTimeoutErr(t *testing.T) {
152 | dnsErr := net.DNSError{
153 | Err: "Can't resolve host",
154 | Name: "Host unresolvable",
155 | Server: "rest.ably.com",
156 | IsTimeout: false,
157 | IsTemporary: false,
158 | IsNotFound: false,
159 | }
160 |
161 | WrappedDNSErr := fmt.Errorf("custom error occured %w", &dnsErr)
162 | if !ably.IsTimeoutOrDnsErr(WrappedDNSErr) {
163 | t.Fatalf("%v is a DNS error", WrappedDNSErr)
164 | }
165 |
166 | urlErr := url.Error{
167 | URL: "rest.ably.io",
168 | Err: errors.New("URL error occured"),
169 | Op: "IO read OP",
170 | }
171 |
172 | if ably.IsTimeoutOrDnsErr(&urlErr) {
173 | t.Fatalf("%v is not a DNS or timeout error", urlErr)
174 | }
175 |
176 | urlErr.Err = &dnsErr
177 |
178 | if !ably.IsTimeoutOrDnsErr(WrappedDNSErr) {
179 | t.Fatalf("%v is a DNS error", urlErr)
180 | }
181 |
182 | dnsErr.IsTimeout = true
183 |
184 | if !ably.IsTimeoutOrDnsErr(WrappedDNSErr) {
185 | t.Fatalf("%v is a timeout error", urlErr)
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/ably/event_emitter_integration_test.go:
--------------------------------------------------------------------------------
1 | //go:build !unit
2 | // +build !unit
3 |
4 | package ably_test
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/ably/ably-go/ably"
10 | "github.com/ably/ably-go/ablytest"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func TestEventEmitterConcurrency(t *testing.T) {
16 | em := ably.NewEventEmitter(ably.NewInternalLogger(ablytest.DiscardLogger))
17 |
18 | type called struct {
19 | i int
20 | goOn chan struct{}
21 | }
22 | calls := make(chan called)
23 |
24 | for i := 0; i < 2; i++ {
25 | i := i
26 | em.OnAll(func(ably.EmitterData) {
27 | c := called{i: i, goOn: make(chan struct{})}
28 | calls <- c
29 | <-c.goOn
30 | })
31 | }
32 |
33 | // Emit, and, since handlers are concurrent, expect a call per handler to
34 | // be initiated.
35 |
36 | em.Emit(ably.EmitterString("foo"), nil)
37 |
38 | var ongoingCalls []called
39 |
40 | for i := 0; i < 2; i++ {
41 | var call called
42 | ablytest.Instantly.Recv(t, &call, calls, t.Fatalf)
43 | ongoingCalls = append(ongoingCalls, call)
44 | }
45 | ablytest.Instantly.NoRecv(t, nil, calls, t.Fatalf)
46 |
47 | // While the last event is still being handled by each handler, a new event
48 | // should be enqueued.
49 |
50 | em.Emit(ably.EmitterString("foo"), nil)
51 |
52 | ablytest.Instantly.NoRecv(t, nil, calls, t.Fatalf)
53 |
54 | // Allow the first ongoing call to finish, which should then process the
55 | // enqueued event for that handler.
56 |
57 | close(ongoingCalls[0].goOn)
58 | var call called
59 | ablytest.Instantly.Recv(t, &call, calls, t.Fatalf)
60 | assert.Equal(t, ongoingCalls[0].i, call.i,
61 | "expected to unblock handler %d, got %d", ongoingCalls[0].i, call.i)
62 |
63 | close(call.goOn)
64 |
65 | // Unblock the other handler too.
66 | close(ongoingCalls[1].goOn)
67 | ablytest.Instantly.Recv(t, &call, calls, t.Fatalf)
68 | assert.Equal(t, ongoingCalls[1].i, call.i,
69 | "expected to unblock handler %d, got %d", ongoingCalls[1].i, call.i)
70 | close(call.goOn)
71 |
72 | // Make sure things still work after emptying the queues.
73 |
74 | em.Emit(ably.EmitterString("foo"), nil)
75 | for i := 0; i < 2; i++ {
76 | ablytest.Instantly.Recv(t, &call, calls, t.Fatalf)
77 | close(call.goOn)
78 | }
79 |
80 | ablytest.Instantly.NoRecv(t, nil, calls, t.Fatalf)
81 | }
82 |
--------------------------------------------------------------------------------
/ably/generate.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | //go:generate go run ../scripts/errors/main.go -json ../common/protocol/errors.json -o errors.go
4 |
--------------------------------------------------------------------------------
/ably/http_paginated_response_integration_test.go:
--------------------------------------------------------------------------------
1 | //go:build !unit
2 | // +build !unit
3 |
4 | package ably_test
5 |
6 | import (
7 | "context"
8 | "net/http"
9 | "net/url"
10 | "sort"
11 | "testing"
12 |
13 | "github.com/ably/ably-go/ably"
14 | "github.com/ably/ably-go/ablytest"
15 |
16 | "github.com/stretchr/testify/assert"
17 | )
18 |
19 | func TestHTTPPaginatedFallback(t *testing.T) {
20 | app, err := ablytest.NewSandbox(nil)
21 | assert.NoError(t, err)
22 | defer app.Close()
23 | opts := app.Options(ably.WithUseBinaryProtocol(false),
24 | ably.WithEndpoint("ably.invalid"),
25 | ably.WithFallbackHosts(nil))
26 | client, err := ably.NewREST(opts...)
27 | assert.NoError(t, err)
28 | t.Run("request_time", func(t *testing.T) {
29 | _, err := client.Request("get", "/time").Pages(context.Background())
30 | assert.Error(t, err)
31 | })
32 | }
33 |
34 | func TestHTTPPaginatedResponse(t *testing.T) {
35 | app, err := ablytest.NewSandbox(nil)
36 | assert.NoError(t, err)
37 | defer app.Close()
38 | client, err := ably.NewREST(app.Options()...)
39 | assert.NoError(t, err)
40 | t.Run("request_time", func(t *testing.T) {
41 | res, err := client.Request("get", "/time").Pages(context.Background())
42 | assert.NoError(t, err)
43 | assert.Equal(t, http.StatusOK, res.StatusCode(),
44 | "expected %d got %d", http.StatusOK, res.StatusCode())
45 | assert.True(t, res.Success(), "expected success to be true")
46 | res.Next(context.Background())
47 | var items []interface{}
48 | err = res.Items(&items)
49 | assert.NoError(t, err)
50 | assert.Equal(t, 1, len(items),
51 | "expected 1 item got %d", len(items))
52 | })
53 |
54 | t.Run("request_404", func(t *testing.T) {
55 | res, err := client.Request("get", "/keys/ablyjs.test/requestToken").Pages(context.Background())
56 | assert.NoError(t, err)
57 | assert.Equal(t, http.StatusNotFound, res.StatusCode(),
58 | "expected %d got %d", http.StatusNotFound, res.StatusCode())
59 | assert.Equal(t, ably.ErrNotFound, res.ErrorCode(),
60 | "expected %d got %d", ably.ErrNotFound, res.ErrorCode())
61 | assert.False(t, res.Success(),
62 | "expected success to be false")
63 | assert.NotEqual(t, "", res.ErrorMessage(),
64 | "expected error message")
65 | })
66 |
67 | t.Run("request_post_get_messages", func(t *testing.T) {
68 | channelName := "http-paginated-result"
69 | channelPath := "/channels/" + channelName + "/messages"
70 | msgs := []ably.Message{
71 | {Name: "faye", Data: "whittaker"},
72 | {Name: "martin", Data: "reed"},
73 | }
74 |
75 | t.Run("post", func(t *testing.T) {
76 | for _, message := range msgs {
77 | res, err := client.Request("POST", channelPath, ably.RequestWithBody(message)).Pages(context.Background())
78 | assert.NoError(t, err)
79 | assert.Equal(t, http.StatusCreated, res.StatusCode(),
80 | "expected %d got %d", http.StatusCreated, res.StatusCode())
81 | assert.True(t, res.Success(),
82 | "expected success to be true")
83 | res.Next(context.Background())
84 | var items []interface{}
85 | err = res.Items(&items)
86 | assert.NoError(t, err)
87 | assert.Equal(t, 1, len(items), "expected 1 item got %d", len(items))
88 | }
89 | })
90 |
91 | t.Run("get", func(t *testing.T) {
92 | res, err := client.Request("get", channelPath, ably.RequestWithParams(url.Values{
93 | "limit": {"1"},
94 | "direction": {"forwards"},
95 | })).Pages(context.Background())
96 | assert.NoError(t, err)
97 | assert.Equal(t, http.StatusOK, res.StatusCode(),
98 | "expected %d got %d", http.StatusOK, res.StatusCode())
99 | res.Next(context.Background())
100 | var items []map[string]interface{}
101 | err = res.Items(&items)
102 | assert.NoError(t, err)
103 | assert.Equal(t, 1, len(items), "expected 1 item got %d", len(items))
104 | item := items[0]
105 | name := item["name"]
106 | data := item["data"]
107 | assert.Equal(t, "faye", name, "expected name \"faye\" got %s", name)
108 | assert.Equal(t, "whittaker", data, "expected data \"whittaker\" got %s", data)
109 |
110 | if !res.Next(context.Background()) {
111 | t.Fatal(res.Err())
112 | }
113 | err = res.Items(&items)
114 | assert.NoError(t, err)
115 | assert.Equal(t, 1, len(items),
116 | "expected 1 item got %d", len(items))
117 | item = items[0]
118 | name = item["name"]
119 | data = item["data"]
120 | assert.Equal(t, "martin", name,
121 | "expected name \"martin\" got %s", name)
122 | assert.Equal(t, "reed", data,
123 | "expected data \"reed\" got %s", data)
124 | })
125 |
126 | var msg []*ably.Message
127 | err := ablytest.AllPages(&msg, client.Channels.Get(channelName).History())
128 | assert.NoError(t, err)
129 | sort.Slice(msg, func(i, j int) bool {
130 | return msg[i].Name < msg[j].Name
131 | })
132 | assert.Equal(t, 2, len(msg),
133 | "expected 2 items in message got %d", len(msg))
134 | assert.Equal(t, "faye", msg[0].Name,
135 | "expected name \"faye\" got %s", msg[0].Name)
136 | assert.Equal(t, "whittaker", msg[0].Data,
137 | "expected data \"whittaker\" got %s", msg[0].Data)
138 | })
139 | }
140 |
--------------------------------------------------------------------------------
/ably/internal/ablyutil/idempotent.go:
--------------------------------------------------------------------------------
1 | package ablyutil
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/base64"
6 | )
7 |
8 | // BaseID returns a base64 encoded 9 random bytes to be used in idempotent rest
9 | // publishing as part of message id.
10 | //
11 | // Spec RSL1k1
12 | func BaseID() (string, error) {
13 | r := make([]byte, 9)
14 | _, err := rand.Read(r)
15 | if err != nil {
16 | return "", err
17 | }
18 | return base64.StdEncoding.EncodeToString(r), nil
19 | }
20 |
--------------------------------------------------------------------------------
/ably/internal/ablyutil/merge.go:
--------------------------------------------------------------------------------
1 | package ablyutil
2 |
3 | import "reflect"
4 |
5 | // Merge iterates over fields of struct pointed by v and when it's non-zero,
6 | // copies its value to corresponding filed in orig.
7 | //
8 | // merge assumes both orig and v are pointers to a struct value of the
9 | // same type.
10 | //
11 | // When defaults is true, merge uses v as the source of default values for each
12 | // field; the default is copied when orig's field is a zero-value.
13 | func Merge(orig, v interface{}, defaults bool) {
14 | vv := reflect.ValueOf(v).Elem()
15 | if !vv.IsValid() {
16 | return
17 | }
18 | vorig := reflect.ValueOf(orig).Elem()
19 | for i := 0; i < vorig.NumField(); i++ {
20 | field := vv.Field(i)
21 | if defaults {
22 | field = vorig.Field(i)
23 | }
24 | var empty bool
25 | switch field.Type().Kind() {
26 | case reflect.Struct:
27 | empty = true // TODO: merge structs recursively
28 | case reflect.Chan, reflect.Func, reflect.Slice, reflect.Map:
29 | empty = field.IsNil()
30 | default:
31 | empty = field.Interface() == reflect.Zero(field.Type()).Interface()
32 | }
33 | if !empty {
34 | vorig.Field(i).Set(vv.Field(i))
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ably/internal/ablyutil/msgpack.go:
--------------------------------------------------------------------------------
1 | package ablyutil
2 |
3 | import (
4 | "bytes"
5 | "io"
6 | "reflect"
7 |
8 | "github.com/ugorji/go/codec"
9 | )
10 |
11 | var handle codec.MsgpackHandle
12 |
13 | func init() {
14 | handle.Raw = true
15 | handle.WriteExt = true
16 | handle.RawToString = true
17 | handle.MapType = reflect.TypeOf(map[string]interface{}(nil))
18 | }
19 |
20 | // UnmarshalMsgpack decodes the MessagePack-encoded data and stores the result in the
21 | // value pointed to by v.
22 | func UnmarshalMsgpack(data []byte, v interface{}) error {
23 | return decodeMsg(bytes.NewReader(data), v)
24 | }
25 |
26 | // decodeMsg decodes msgpack message read from r into v.
27 | func decodeMsg(r io.Reader, v interface{}) error {
28 | dec := codec.NewDecoder(r, &handle)
29 | return dec.Decode(v)
30 | }
31 |
32 | // MarshalMsgpack returns msgpack encoding of v
33 | func MarshalMsgpack(v interface{}) ([]byte, error) {
34 | var buf bytes.Buffer
35 | err := encodeMsg(&buf, v)
36 | if err != nil {
37 | return nil, err
38 | }
39 | return buf.Bytes(), nil
40 | }
41 |
42 | // encodeMsg encodes v into msgpack format and writes the output to w.
43 | func encodeMsg(w io.Writer, v interface{}) error {
44 | enc := codec.NewEncoder(w, &handle)
45 | return enc.Encode(v)
46 | }
47 |
--------------------------------------------------------------------------------
/ably/internal/ablyutil/msgpack_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ablyutil
5 |
6 | import (
7 | "bytes"
8 | "encoding/json"
9 | "testing"
10 | )
11 |
12 | func TestMsgpack(t *testing.T) {
13 | type Object1 struct {
14 | Key int64 `codec:"my_key"`
15 | }
16 | type Object2 struct {
17 | Key float64 `codec:"my_key"`
18 | }
19 | t.Run("must decode int64 into float64", func(ts *testing.T) {
20 | var buf bytes.Buffer
21 | err := encodeMsg(&buf, &Object1{Key: 12})
22 | if err != nil {
23 | ts.Fatal(err)
24 | }
25 | b := &Object2{}
26 | err = decodeMsg(&buf, &b)
27 | if err != nil {
28 | ts.Fatal(err)
29 | }
30 | if b.Key != 12 {
31 | ts.Errorf("expected 12 got %v", b.Key)
32 | }
33 | })
34 | }
35 |
36 | func TestMsgpackJson(t *testing.T) {
37 | extras := map[string]interface{}{
38 | "headers": map[string]interface{}{
39 | "version": 1,
40 | },
41 | }
42 |
43 | b, err := MarshalMsgpack(extras)
44 | if err != nil {
45 | t.Fatal(err)
46 | }
47 |
48 | var got map[string]interface{}
49 | err = UnmarshalMsgpack(b, &got)
50 | if err != nil {
51 | t.Fatal(err)
52 | }
53 |
54 | buff := bytes.Buffer{}
55 | err = json.NewEncoder(&buff).Encode(got)
56 | if err != nil {
57 | t.Fatal(err)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ably/internal/ablyutil/regex.go:
--------------------------------------------------------------------------------
1 | package ablyutil
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "regexp"
7 | )
8 |
9 | // derivedChannelMatch provides the qualifyingParam and channelName from a channel regex match for derived channels
10 | type derivedChannelMatch struct {
11 | QualifierParam string
12 | ChannelName string
13 | }
14 |
15 | // This regex check is to retain existing channel params if any e.g [?rewind=1]foo to
16 | // [filter=xyz?rewind=1]foo. This is to keep channel compatibility around use of
17 | // channel params that work with derived channels.
18 | func MatchDerivedChannel(name string) (*derivedChannelMatch, error) {
19 | regex := `^(\[([^?]*)(?:(.*))\])?(.+)$`
20 | r, err := regexp.Compile(regex)
21 | if err != nil {
22 | err := errors.New("regex compilation failed")
23 | return nil, err
24 | }
25 | match := r.FindStringSubmatch(name)
26 |
27 | if len(match) == 0 || len(match) < 5 {
28 | err := errors.New("regex match failed")
29 | return nil, err
30 | }
31 | // Fail if there is already a channel qualifier,
32 | // eg [meta]foo should fail instead of just overriding with [filter=xyz]foo
33 | if len(match[2]) > 0 {
34 | err := fmt.Errorf("cannot use a derived option with a %s channel", match[2])
35 | return nil, err
36 | }
37 |
38 | // Return match values to be added to derive channel quantifier.
39 | return &derivedChannelMatch{
40 | QualifierParam: match[3],
41 | ChannelName: match[4],
42 | }, nil
43 | }
44 |
--------------------------------------------------------------------------------
/ably/internal/ablyutil/regex_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ablyutil
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestMatchDerivedChannel(t *testing.T) {
13 | var channels = []struct {
14 | name string
15 | input string
16 | want *derivedChannelMatch
17 | }{
18 | {"valid with base channel name", "foo", &derivedChannelMatch{QualifierParam: "", ChannelName: "foo"}},
19 | {"valid with base channel namespace", "foo:bar", &derivedChannelMatch{QualifierParam: "", ChannelName: "foo:bar"}},
20 | {"valid with existing qualifying option", "[?rewind=1]foo", &derivedChannelMatch{QualifierParam: "?rewind=1", ChannelName: "foo"}},
21 | {"valid with existing qualifying option with channel namespace", "[?rewind=1]foo:bar", &derivedChannelMatch{QualifierParam: "?rewind=1", ChannelName: "foo:bar"}},
22 | {"fail with invalid param with channel namespace", "[param:invalid]foo:bar", nil},
23 | {"fail with wrong channel option param", "[param=1]foo", nil},
24 | {"fail with invalid qualifying option", "[meta]foo", nil},
25 | {"fail with invalid regex match", "[failed-match]foo", nil},
26 | }
27 |
28 | for _, tt := range channels {
29 | t.Run(tt.name, func(t *testing.T) {
30 | match, err := MatchDerivedChannel(tt.input)
31 | if err != nil {
32 | assert.Error(t, err, "An error is expected for the regex match")
33 | }
34 | assert.Equal(t, tt.want, match, "invalid output received")
35 | })
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ably/internal/ablyutil/strings.go:
--------------------------------------------------------------------------------
1 | package ablyutil
2 |
3 | import (
4 | "math/rand"
5 | "sort"
6 | "strings"
7 | "time"
8 | )
9 |
10 | const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
11 |
12 | var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
13 |
14 | func GenerateRandomString(length int) string {
15 | b := make([]byte, length)
16 | for i := range b {
17 | b[i] = charset[seededRand.Intn(len(charset))]
18 | }
19 | return string(b)
20 | }
21 |
22 | type HashSet map[string]struct{} // struct {} has zero space complexity
23 |
24 | func NewHashSet() HashSet {
25 | return make(HashSet)
26 | }
27 |
28 | func (s HashSet) Add(item string) {
29 | s[item] = struct{}{}
30 | }
31 |
32 | func (s HashSet) Remove(item string) {
33 | delete(s, item)
34 | }
35 |
36 | func (s HashSet) Has(item string) bool {
37 | _, ok := s[item]
38 | return ok
39 | }
40 |
41 | func Copy(list []string) []string {
42 | copiedList := make([]string, len(list))
43 | copy(copiedList, list)
44 | return copiedList
45 | }
46 |
47 | func Sort(list []string) []string {
48 | copiedList := Copy(list)
49 | sort.Strings(copiedList)
50 | return copiedList
51 | }
52 |
53 | func Shuffle(list []string) []string {
54 | copiedList := Copy(list)
55 | if len(copiedList) <= 1 {
56 | return copiedList
57 | }
58 | rand.Seed(time.Now().UnixNano())
59 | rand.Shuffle(len(copiedList), func(i, j int) { copiedList[i], copiedList[j] = copiedList[j], copiedList[i] })
60 | return copiedList
61 | }
62 |
63 | func SliceContains(s []string, str string) bool {
64 | for _, v := range s {
65 | if v == str {
66 | return true
67 | }
68 | }
69 | return false
70 | }
71 |
72 | func Empty(s string) bool {
73 | return len(strings.TrimSpace(s)) == 0
74 | }
75 |
--------------------------------------------------------------------------------
/ably/internal/ablyutil/strings_test.go:
--------------------------------------------------------------------------------
1 | package ablyutil_test
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/ably/ably-go/ably/internal/ablyutil"
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func Test_string(t *testing.T) {
11 | t.Run("String array Shuffle", func(t *testing.T) {
12 | t.Parallel()
13 |
14 | strList := []string{}
15 | shuffledList := ablyutil.Shuffle(strList)
16 | assert.Equal(t, strList, shuffledList)
17 |
18 | strList = []string{"a"}
19 | shuffledList = ablyutil.Shuffle(strList)
20 | assert.Equal(t, strList, shuffledList)
21 |
22 | strList = []string{"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p"}
23 | shuffledList = ablyutil.Shuffle(strList)
24 | assert.NotEqual(t, strList, shuffledList)
25 | assert.Equal(t, ablyutil.Sort(strList), ablyutil.Sort(shuffledList))
26 | })
27 |
28 | t.Run("String array contains", func(t *testing.T) {
29 | t.Parallel()
30 | strarr := []string{"apple", "banana", "dragonfruit"}
31 |
32 | if !ablyutil.SliceContains(strarr, "apple") {
33 | t.Error("String array should contain apple")
34 | }
35 | if ablyutil.SliceContains(strarr, "orange") {
36 | t.Error("String array should not contain orange")
37 | }
38 | })
39 |
40 | t.Run("Empty String", func(t *testing.T) {
41 | t.Parallel()
42 | str := ""
43 | if !ablyutil.Empty(str) {
44 | t.Error("String should be empty")
45 | }
46 | str = " "
47 | if !ablyutil.Empty(str) {
48 | t.Error("String should be empty")
49 | }
50 | str = "ab"
51 | if ablyutil.Empty(str) {
52 | t.Error("String should not be empty")
53 | }
54 | })
55 | }
56 |
57 | func TestHashSet(t *testing.T) {
58 | t.Run("Add should not duplicate entries", func(t *testing.T) {
59 | hashSet := ablyutil.NewHashSet()
60 | hashSet.Add("apple")
61 | hashSet.Add("apple")
62 | assert.Len(t, hashSet, 1)
63 |
64 | hashSet.Add("banana")
65 | assert.Len(t, hashSet, 2)
66 |
67 | hashSet.Add("orange")
68 | assert.Len(t, hashSet, 3)
69 |
70 | hashSet.Add("banana")
71 | hashSet.Add("apple")
72 | hashSet.Add("orange")
73 | hashSet.Add("orange")
74 |
75 | assert.Len(t, hashSet, 3)
76 | })
77 |
78 | t.Run("Should check if item is present", func(t *testing.T) {
79 | hashSet := ablyutil.NewHashSet()
80 | hashSet.Add("apple")
81 | hashSet.Add("orange")
82 | if !hashSet.Has("apple") {
83 | t.Fatalf("Set should contain apple")
84 | }
85 | if hashSet.Has("banana") {
86 | t.Fatalf("Set shouldm't contain banana")
87 | }
88 | if !hashSet.Has("orange") {
89 | t.Fatalf("Set should contain orange")
90 | }
91 | })
92 |
93 | t.Run("Should remove element", func(t *testing.T) {
94 | hashSet := ablyutil.NewHashSet()
95 | hashSet.Add("apple")
96 | assert.Len(t, hashSet, 1)
97 |
98 | hashSet.Add("orange")
99 | assert.Len(t, hashSet, 2)
100 |
101 | hashSet.Remove("apple")
102 | assert.Len(t, hashSet, 1)
103 |
104 | if hashSet.Has("apple") {
105 | t.Fatalf("Set shouldm't contain apple")
106 | }
107 | hashSet.Remove("orange")
108 | assert.Len(t, hashSet, 0)
109 |
110 | })
111 | }
112 |
--------------------------------------------------------------------------------
/ably/internal/ablyutil/time.go:
--------------------------------------------------------------------------------
1 | package ablyutil
2 |
3 | import (
4 | "context"
5 | "time"
6 | )
7 |
8 | // After returns a channel that is sent the current time once the given
9 | // duration has passed. If the context is cancelled before that, the channel
10 | // is immediately closed.
11 | func After(ctx context.Context, d time.Duration) <-chan time.Time {
12 | timer := time.NewTimer(d)
13 |
14 | ch := make(chan time.Time, 1)
15 |
16 | go func() {
17 | defer timer.Stop()
18 | select {
19 | case <-ctx.Done():
20 | close(ch)
21 | case t := <-timer.C:
22 | ch <- t
23 | }
24 | }()
25 |
26 | return ch
27 | }
28 |
29 | type TimerFunc func(context.Context, time.Duration) <-chan time.Time
30 |
31 | // NewTicker repeatedly calls the given TimerFunc, which should behave like
32 | // After, until the context it cancelled. It returns a channel to which it sends
33 | // every value produced by the TimerFunc.
34 | func NewTicker(after TimerFunc) TimerFunc {
35 | return func(ctx context.Context, d time.Duration) <-chan time.Time {
36 | ch := make(chan time.Time, 1)
37 |
38 | go func() {
39 | for {
40 | t, ok := <-after(ctx, d)
41 | if !ok {
42 | close(ch)
43 | return
44 | }
45 | ch <- t
46 | }
47 | }()
48 |
49 | return ch
50 | }
51 | }
52 |
53 | // ContextWithTimeout is like context.WithTimeout, but using the provided
54 | // TimerFunc for setting the timer.
55 | func ContextWithTimeout(ctx context.Context, after TimerFunc, timeout time.Duration) (context.Context, context.CancelFunc) {
56 | ctx, cancel := context.WithCancel(ctx)
57 |
58 | // If the timer expires, we cancel the context. But then we need the context's
59 | // error to be context.DeadlineExceeded instead of context.Canceled.
60 | // uses same context as above, doesn't create new context
61 | ctx, setErr := contextWithCustomError(ctx)
62 |
63 | go func() {
64 | _, timerFired := <-after(ctx, timeout)
65 | if timerFired {
66 | setErr(context.DeadlineExceeded)
67 | }
68 | cancel()
69 | }()
70 | return ctx, cancel
71 | }
72 |
73 | type contextWithCustomErr struct {
74 | context.Context
75 | err error
76 | }
77 |
78 | func contextWithCustomError(ctx context.Context) (_ context.Context, setError func(error)) {
79 | customContext := contextWithCustomErr{ctx, nil}
80 | return &customContext, func(err error) {
81 | customContext.err = err
82 | }
83 | }
84 |
85 | func (ctx *contextWithCustomErr) Err() error {
86 | if ctx.err != nil {
87 | return ctx.err
88 | }
89 | return ctx.Context.Err()
90 | }
91 |
--------------------------------------------------------------------------------
/ably/logger.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | )
7 |
8 | type LogLevel uint
9 |
10 | const (
11 | LogNone LogLevel = iota
12 | LogError
13 | LogWarning
14 | LogInfo
15 | LogVerbose
16 | LogDebug
17 | )
18 |
19 | var logLevelNames = map[LogLevel]string{
20 | LogError: "ERROR",
21 | LogWarning: "WARN",
22 | LogInfo: "INFO",
23 | LogVerbose: "VERBOSE",
24 | LogDebug: "DEBUG",
25 | }
26 |
27 | func (l LogLevel) String() string {
28 | return logLevelNames[l]
29 | }
30 |
31 | type filteredLogger struct {
32 | Logger Logger
33 | Level LogLevel
34 | }
35 |
36 | func (l filteredLogger) Is(level LogLevel) bool {
37 | return l.Level != LogNone && l.Level >= level
38 | }
39 |
40 | func (l filteredLogger) Printf(level LogLevel, format string, v ...interface{}) {
41 | if l.Is(level) {
42 | l.Logger.Printf(level, format, v...)
43 | }
44 | }
45 |
46 | // Logger is an interface for ably loggers.
47 | type Logger interface {
48 | Printf(level LogLevel, format string, v ...interface{})
49 | }
50 |
51 | // stdLogger wraps log.Logger to satisfy the Logger interface.
52 | type stdLogger struct {
53 | *log.Logger
54 | }
55 |
56 | func (s *stdLogger) Printf(level LogLevel, format string, v ...interface{}) {
57 | s.Logger.Printf(fmt.Sprintf("[%s] %s", level, format), v...)
58 | }
59 |
60 | // logger is the internal logger type, with helper methods that wrap the raw Logger interface.
61 | type logger struct {
62 | l Logger
63 | }
64 |
65 | func (l logger) Error(v ...interface{}) {
66 | l.print(LogError, v...)
67 | }
68 |
69 | func (l logger) Errorf(fmt string, v ...interface{}) {
70 | l.l.Printf(LogError, fmt, v...)
71 | }
72 |
73 | func (l logger) Warn(v ...interface{}) {
74 | l.print(LogWarning, v...)
75 | }
76 |
77 | func (l logger) Warnf(fmt string, v ...interface{}) {
78 | l.l.Printf(LogWarning, fmt, v...)
79 | }
80 |
81 | func (l logger) Info(v ...interface{}) {
82 | l.print(LogInfo, v...)
83 | }
84 |
85 | func (l logger) Infof(fmt string, v ...interface{}) {
86 | l.l.Printf(LogInfo, fmt, v...)
87 | }
88 |
89 | func (l logger) Verbose(v ...interface{}) {
90 | l.print(LogVerbose, v...)
91 | }
92 |
93 | func (l logger) Verbosef(fmt string, v ...interface{}) {
94 | l.l.Printf(LogVerbose, fmt, v...)
95 | }
96 |
97 | func (l logger) Debugf(fmt string, v ...interface{}) {
98 | l.l.Printf(LogDebug, fmt, v...)
99 | }
100 |
101 | func (l logger) Debug(v ...interface{}) {
102 | l.print(LogDebug, v...)
103 | }
104 |
105 | func (l logger) print(level LogLevel, v ...interface{}) {
106 | l.l.Printf(level, fmt.Sprint(v...))
107 | }
108 |
--------------------------------------------------------------------------------
/ably/logger_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/ably/ably-go/ably"
10 |
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | type dummyLogger struct {
15 | printf int
16 | }
17 |
18 | func (d *dummyLogger) Printf(level ably.LogLevel, format string, v ...interface{}) {
19 | d.printf++
20 | }
21 |
22 | func TestFilteredLogger(t *testing.T) {
23 | t.Run("must log smaller or same level", func(t *testing.T) {
24 |
25 | l := &dummyLogger{}
26 | lg := &ably.FilteredLogger{
27 | Level: ably.LogDebug,
28 | Logger: l,
29 | }
30 | say := "log this"
31 | lg.Printf(ably.LogVerbose, say)
32 | lg.Printf(ably.LogInfo, say)
33 | lg.Printf(ably.LogWarning, say)
34 | lg.Printf(ably.LogError, say)
35 | assert.Equal(t, 4, l.printf,
36 | "expected 4 log messages got %d", l.printf)
37 | })
38 | t.Run("must log nothing for LogNone", func(t *testing.T) {
39 |
40 | l := &dummyLogger{}
41 | lg := &ably.FilteredLogger{
42 | Logger: l,
43 | }
44 | say := "log this"
45 | lg.Printf(ably.LogVerbose, say)
46 | lg.Printf(ably.LogInfo, say)
47 | lg.Printf(ably.LogWarning, say)
48 | lg.Printf(ably.LogError, say)
49 | assert.Equal(t, 0, l.printf,
50 | "expected nothing to be logged")
51 | })
52 | }
53 |
--------------------------------------------------------------------------------
/ably/os_version_other.go:
--------------------------------------------------------------------------------
1 | //go:build !linux && !darwin && !windows
2 | // +build !linux,!darwin,!windows
3 |
4 | package ably
5 |
6 | func goOSIdentifier() string {
7 | return ""
8 | }
9 |
--------------------------------------------------------------------------------
/ably/os_version_unix.go:
--------------------------------------------------------------------------------
1 | //go:build linux || darwin
2 | // +build linux darwin
3 |
4 | package ably
5 |
6 | import (
7 | "bytes"
8 | "fmt"
9 | "runtime"
10 |
11 | "golang.org/x/sys/unix"
12 | )
13 |
14 | func goOSIdentifier() string {
15 | utsname := unix.Utsname{}
16 | err := unix.Uname(&utsname)
17 | if err != nil {
18 | return ""
19 | }
20 | release := bytes.Trim(utsname.Release[:], "\x00")
21 | return fmt.Sprintf("%s/%s", runtime.GOOS, release)
22 | }
23 |
--------------------------------------------------------------------------------
/ably/os_version_windows.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 |
7 | "golang.org/x/sys/windows"
8 | )
9 |
10 | func goOSIdentifier() string {
11 | v := windows.RtlGetVersion()
12 | return fmt.Sprintf("%s/%d.%d.%d", runtime.GOOS, v.MajorVersion, v.MinorVersion, v.BuildNumber)
13 | }
14 |
--------------------------------------------------------------------------------
/ably/paginated_result.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "net/url"
7 | "path"
8 | "reflect"
9 | "regexp"
10 | )
11 |
12 | type Direction string
13 |
14 | const (
15 | Backwards Direction = "backwards"
16 | Forwards Direction = "forwards"
17 | )
18 |
19 | type paginatedRequest struct {
20 | path string
21 | rawPath string
22 | params url.Values
23 |
24 | query queryFunc
25 | }
26 |
27 | func (r *REST) newPaginatedRequest(path, rawPath string, params url.Values) paginatedRequest {
28 | return paginatedRequest{
29 | path: path,
30 | rawPath: rawPath,
31 | params: params,
32 |
33 | query: query(r.get),
34 | }
35 | }
36 |
37 | // PaginatedResult is a generic iterator for PaginatedResult pagination.
38 | // Items decoding is delegated to type-specific wrappers.
39 | //
40 | // See "Paginated results" section in the package-level documentation.
41 | type PaginatedResult struct {
42 | basePath string
43 | nextLink string
44 | firstLink string
45 | res *http.Response
46 | err error
47 |
48 | query queryFunc
49 | first bool
50 | }
51 |
52 | // load - It loads first page of results. Must be called from the type-specific
53 | // wrapper Pages method that creates the PaginatedResult object.
54 | func (p *PaginatedResult) load(ctx context.Context, r paginatedRequest) error {
55 | p.basePath = path.Dir(r.path)
56 | p.firstLink = (&url.URL{
57 | Path: r.path,
58 | RawPath: r.rawPath,
59 | RawQuery: r.params.Encode(),
60 | }).String()
61 | p.query = r.query
62 | return p.First(ctx)
63 | }
64 |
65 | // loadItems loads the first page of results and returns a next function. Must
66 | // be called from the type-specific wrapper Items method that creates the
67 | // PaginatedItems object.
68 | //
69 | // The returned next function must be called from the wrapper's Next method, and
70 | // returns the index of the object that should be returned by the Item method,
71 | // previously loading the next page if necessary.
72 | //
73 | // pageDecoder will be called each time a new page is retrieved under the hood.
74 | // It should return a destination object on which the page of results will be
75 | // decoded, and a pageLength function that, when called after the page has been
76 | // decoded, must return the length of the page.
77 | func (p *PaginatedResult) loadItems(
78 | ctx context.Context,
79 | r paginatedRequest,
80 | pageDecoder func() (page interface{}, pageLength func() int),
81 | ) (
82 | next func(context.Context) (int, bool),
83 | err error,
84 | ) {
85 | err = p.load(ctx, r)
86 | if err != nil {
87 | return nil, err
88 | }
89 |
90 | var page interface{}
91 | var pageLen int
92 | nextItem := 0
93 |
94 | return func(ctx context.Context) (int, bool) {
95 | if nextItem == 0 {
96 | var getLen func() int
97 | page, getLen = pageDecoder()
98 | hasNext := p.next(ctx, page)
99 | if !hasNext {
100 | return 0, false
101 | }
102 | pageLen = getLen()
103 | if pageLen == 0 { // compatible with hasNext if first page is empty
104 | return 0, false
105 | }
106 | }
107 |
108 | idx := nextItem
109 | nextItem = (nextItem + 1) % pageLen
110 | return idx, true
111 | }, nil
112 | }
113 |
114 | func (p *PaginatedResult) goTo(ctx context.Context, link string) error {
115 | var err error
116 | p.res, err = p.query(ctx, link)
117 | if err != nil {
118 | return err
119 | }
120 | p.nextLink = ""
121 | for _, rawLink := range p.res.Header["Link"] {
122 | m := relLinkRegexp.FindStringSubmatch(rawLink)
123 | if len(m) == 0 {
124 | continue
125 | }
126 | relPath, rel := m[1], m[2]
127 | linkPath := path.Join(p.basePath, relPath)
128 | switch rel {
129 | case "first":
130 | p.firstLink = linkPath
131 | case "next":
132 | p.nextLink = linkPath
133 | }
134 | }
135 | return nil
136 | }
137 |
138 | // next loads the next page of items, if there is one. It returns whether a page
139 | // was successfully loaded or not; after it returns false, Err should be
140 | // called to check for any errors.
141 | //
142 | // Items can then be inspected with the type-specific Items method.
143 | //
144 | // For items iterators, use the next function returned by loadItems instead.
145 | func (p *PaginatedResult) next(ctx context.Context, into interface{}) bool {
146 | if !p.first {
147 | if p.nextLink == "" {
148 | return false
149 | }
150 | p.err = p.goTo(ctx, p.nextLink)
151 | if p.err != nil {
152 | return false
153 | }
154 | }
155 | p.first = false
156 |
157 | p.err = decodeResp(p.res, into)
158 | return p.err == nil
159 | }
160 |
161 | // First loads the first page of items. Next should be called before inspecting
162 | // the Items.
163 | func (p *PaginatedResult) First(ctx context.Context) error {
164 | p.first = true
165 | return p.goTo(ctx, p.firstLink)
166 | }
167 |
168 | // Err returns the error that caused Next to fail, if there was one.
169 | func (p *PaginatedResult) Err() error {
170 | return p.err
171 | }
172 |
173 | // relLinkRegexp is the regexp that matches our pagination format
174 | var relLinkRegexp = regexp.MustCompile(`<(?P[^>]+)>; rel="(?P[^"]+)"`)
175 |
176 | type errInvalidType struct {
177 | typ reflect.Type
178 | }
179 |
180 | func (err errInvalidType) Error() string {
181 | return "requested value of incompatible type: " + err.typ.String()
182 | }
183 |
184 | // queryFunc queries the given URL and gives non-nil HTTP response if no error
185 | // occurred.
186 | type queryFunc func(ctx context.Context, url string) (*http.Response, error)
187 |
188 | func copyHeader(dest, src http.Header) {
189 | for k, v := range src {
190 | d := make([]string, len(v))
191 | copy(d, v)
192 | dest[k] = v
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/ably/proto_action.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | type protoAction int8
4 |
5 | const (
6 | actionHeartbeat protoAction = iota
7 | actionAck
8 | actionNack
9 | actionConnect
10 | actionConnected
11 | actionDisconnect
12 | actionDisconnected
13 | actionClose
14 | actionClosed
15 | actionError
16 | actionAttach
17 | actionAttached
18 | actionDetach
19 | actionDetached
20 | actionPresence
21 | actionMessage
22 | actionSync
23 | actionAuth
24 | )
25 |
26 | var actions = map[protoAction]string{
27 | actionHeartbeat: "heartbeat",
28 | actionAck: "ack",
29 | actionNack: "nack",
30 | actionConnect: "connect",
31 | actionConnected: "connected",
32 | actionDisconnect: "disconnect",
33 | actionDisconnected: "disconnected",
34 | actionClose: "close",
35 | actionClosed: "closed",
36 | actionError: "error",
37 | actionAttach: "attach",
38 | actionAttached: "attached",
39 | actionDetach: "detach",
40 | actionDetached: "detached",
41 | actionPresence: "presence",
42 | actionMessage: "message",
43 | actionSync: "sync",
44 | actionAuth: "auth",
45 | }
46 |
47 | func (a protoAction) String() string {
48 | return actions[a]
49 | }
50 |
--------------------------------------------------------------------------------
/ably/proto_channel_options.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | type channelParams map[string]string
4 |
5 | // ChannelMode Describes the possible flags used to configure client capabilities, using [ably.ChannelOption].
6 | type ChannelMode int64
7 |
8 | const (
9 | // ChannelModePresence allows the attached channel to enter Presence.
10 | ChannelModePresence ChannelMode = iota + 1
11 | // ChannelModePublish allows for messages to be published on the attached channel.
12 | ChannelModePublish
13 | // ChannelModeSubscribe allows the attached channel to subscribe to messages.
14 | ChannelModeSubscribe
15 | // ChannelModePresenceSubscribe allows the attached channel to subscribe to Presence updates.
16 | ChannelModePresenceSubscribe
17 | )
18 |
19 | func (mode ChannelMode) toFlag() protoFlag {
20 | switch mode {
21 | case ChannelModePresence:
22 | return flagPresence
23 | case ChannelModePublish:
24 | return flagPublish
25 | case ChannelModeSubscribe:
26 | return flagSubscribe
27 | case ChannelModePresenceSubscribe:
28 | return flagPresenceSubscribe
29 | default:
30 | return 0
31 | }
32 | }
33 |
34 | func channelModeFromFlag(flags protoFlag) []ChannelMode {
35 | var modes []ChannelMode
36 | if flags.Has(flagPresence) {
37 | modes = append(modes, ChannelModePresence)
38 | }
39 | if flags.Has(flagPublish) {
40 | modes = append(modes, ChannelModePublish)
41 | }
42 | if flags.Has(flagSubscribe) {
43 | modes = append(modes, ChannelModeSubscribe)
44 | }
45 | if flags.Has(flagPresenceSubscribe) {
46 | modes = append(modes, ChannelModePresenceSubscribe)
47 | }
48 | return modes
49 | }
50 |
51 | // protoChannelOptions defines additional properties to a [ably.RESTChannel] or [ably.RealtimeChannel] object,
52 | // such as encryption, [ably.ChannelMode] and channel parameters.
53 | // It defines options provided for creating a new channel.
54 | type protoChannelOptions struct {
55 | Cipher CipherParams
56 | cipher channelCipher
57 | Params channelParams
58 | Modes []ChannelMode
59 | }
60 |
--------------------------------------------------------------------------------
/ably/proto_channel_options_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "reflect"
8 | "testing"
9 |
10 | "github.com/ably/ably-go/ably"
11 |
12 | "github.com/stretchr/testify/assert"
13 | )
14 |
15 | func TestRealtimeChannelModes_ToFlag(t *testing.T) {
16 | mode := ably.ChannelModePresence
17 | flag := ably.ChannelModeToFlag(mode)
18 | assert.Equal(t, ably.FlagPresence, flag,
19 | "Expected %v, received %v", ably.FlagPresence, flag)
20 | mode = ably.ChannelModeSubscribe
21 | flag = ably.ChannelModeToFlag(mode)
22 | assert.Equal(t, ably.FlagSubscribe, flag,
23 | "Expected %v, received %v", ably.FlagSubscribe, flag)
24 | mode = ably.ChannelModePublish
25 | flag = ably.ChannelModeToFlag(mode)
26 | assert.Equal(t, ably.FlagPublish, flag,
27 | "Expected %v, received %v", ably.FlagPublish, flag)
28 | mode = ably.ChannelModePresenceSubscribe
29 | flag = ably.ChannelModeToFlag(mode)
30 | assert.Equal(t, ably.FlagPresenceSubscribe, flag,
31 | "Expected %v, received %v", ably.FlagPresenceSubscribe, flag)
32 | }
33 |
34 | func TestRealtimeChannelModes_FromFlag(t *testing.T) {
35 |
36 | // checks if element is present in the array
37 | inArray := func(v interface{}, in interface{}) (ok bool) {
38 | i := 0
39 | val := reflect.Indirect(reflect.ValueOf(in))
40 | switch val.Kind() {
41 | case reflect.Slice, reflect.Array:
42 | for ; i < val.Len(); i++ {
43 | if ok = v == val.Index(i).Interface(); ok {
44 | return
45 | }
46 | }
47 | }
48 | return
49 | }
50 |
51 | flags := ably.FlagPresence | ably.FlagPresenceSubscribe | ably.FlagSubscribe
52 | modes := ably.ChannelModeFromFlag(flags)
53 |
54 | assert.True(t, inArray(ably.ChannelModePresence, modes),
55 | "Expected %v to be present in %v", ably.ChannelModePresence, modes)
56 | assert.True(t, inArray(ably.ChannelModePresenceSubscribe, modes),
57 | "Expected %v to be present in %v", ably.ChannelModePresenceSubscribe, modes)
58 | assert.True(t, inArray(ably.ChannelModeSubscribe, modes),
59 | "Expected %v to be present in %v", ably.ChannelModeSubscribe, modes)
60 | assert.False(t, inArray(ably.ChannelModePublish, modes),
61 | "Expected %v not to be present in %v", ably.ChannelModePublish, modes)
62 | }
63 |
--------------------------------------------------------------------------------
/ably/proto_channel_propeties.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | // CP2
4 | type ChannelProperties struct {
5 | // AttachSerial contains the channelSerial from latest ATTACHED ProtocolMessage received on the channel, see CP2a, RTL15a
6 | AttachSerial string
7 |
8 | // ChannelSerial contains the channelSerial from latest ProtocolMessage of action type Message/PresenceMessage received on the channel, see CP2b, RTL15b.
9 | ChannelSerial string
10 | }
11 |
--------------------------------------------------------------------------------
/ably/proto_error.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | // errorInfo describes an error object returned via ProtocolMessage.
4 | type errorInfo struct {
5 | StatusCode int `json:"statusCode,omitempty" codec:"statusCode,omitempty"`
6 | Code int `json:"code,omitempty" codec:"code,omitempty"`
7 | HRef string `json:"href,omitempty" codec:"href,omitempty"` //spec TI4
8 | Message string `json:"message,omitempty" codec:"message,omitempty"`
9 | Server string `json:"serverId,omitempty" codec:"serverId,omitempty"`
10 | }
11 |
12 | func (e *errorInfo) FromMap(ctx map[string]interface{}) {
13 | if v, ok := ctx["statusCode"]; ok {
14 | e.StatusCode = coerceInt(v)
15 | }
16 | if v, ok := ctx["code"]; ok {
17 | e.Code = coerceInt(v)
18 | }
19 | if v, ok := ctx["href"]; ok {
20 | e.HRef = v.(string)
21 | }
22 | if v, ok := ctx["message"]; ok {
23 | e.Message = v.(string)
24 | }
25 | if v, ok := ctx["serverId"]; ok {
26 | e.Server = v.(string)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ably/proto_http.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "fmt"
5 | "runtime"
6 | "strings"
7 | )
8 |
9 | // constants for rsc7
10 | const (
11 | ablyProtocolVersionHeader = "X-Ably-Version"
12 | ablyErrorCodeHeader = "X-Ably-Errorcode"
13 | ablyErrorMessageHeader = "X-Ably-Errormessage"
14 | clientLibraryVersion = "1.2.22"
15 | clientRuntimeName = "go"
16 | ablyProtocolVersion = "2" // CSV2
17 | ablyClientIDHeader = "X-Ably-ClientId"
18 | hostHeader = "Host"
19 | ablyAgentHeader = "Ably-Agent" // RSC7d
20 | ablySDKIdentifier = "ably-go/" + clientLibraryVersion // RSC7d1
21 | )
22 |
23 | var goRuntimeIdentifier = func() string {
24 | return fmt.Sprintf("%s/%s", clientRuntimeName, runtime.Version()[2:])
25 | }()
26 |
27 | func ablyAgentIdentifier(agents map[string]string) string {
28 | identifiers := []string{
29 | ablySDKIdentifier,
30 | goRuntimeIdentifier,
31 | }
32 |
33 | osIdentifier := goOSIdentifier()
34 | if !empty(osIdentifier) {
35 | identifiers = append(identifiers, osIdentifier)
36 | }
37 |
38 | for product, version := range agents {
39 | if empty(version) {
40 | identifiers = append(identifiers, product)
41 | } else {
42 | identifiers = append(identifiers, product+"/"+version)
43 | }
44 | }
45 |
46 | return strings.Join(identifiers, " ")
47 | }
48 |
--------------------------------------------------------------------------------
/ably/proto_message_decoding_test.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "encoding/hex"
7 | "encoding/json"
8 | "os"
9 | "strings"
10 | "testing"
11 |
12 | "github.com/ably/ably-go/ably/internal/ablyutil"
13 | "github.com/stretchr/testify/assert"
14 | "github.com/stretchr/testify/require"
15 | )
16 |
17 | type fixture struct {
18 | Data string `json:"data"`
19 | Encoding string `json:"encoding"`
20 | ExpectedType string `json:"expectedType"`
21 | ExpectedValue any `json:"expectedValue"`
22 | ExpectedHexValue string `json:"expectedHexValue"`
23 | }
24 |
25 | func loadFixtures() ([]fixture, error) {
26 | var dec struct {
27 | Messages []fixture
28 | }
29 |
30 | // We can't embed the fixtures as go:embed forbids embedding of resources above the current directory.
31 | text, err := os.ReadFile("../common/test-resources/messages-encoding.json")
32 | if err != nil {
33 | return nil, err
34 | }
35 |
36 | err = json.Unmarshal(text, &dec)
37 | return dec.Messages, err
38 | }
39 |
40 | func Test_decodeMessage(t *testing.T) {
41 | fixtures, err := loadFixtures()
42 | require.NoError(t, err, "failed to load test fixtures")
43 | for _, f := range fixtures {
44 | t.Run(f.Data, func(t *testing.T) {
45 | msg := Message{
46 | Data: f.Data,
47 | Encoding: f.Encoding,
48 | }
49 | decodedMsg, err := msg.withDecodedData(nil)
50 | require.NoError(t, err)
51 | switch f.ExpectedType {
52 | case "string":
53 | assert.IsType(t, "string", decodedMsg.Data)
54 | assert.Equal(t, f.ExpectedValue, decodedMsg.Data)
55 | case "jsonObject":
56 | assert.IsType(t, map[string]any{}, decodedMsg.Data)
57 | assert.Equal(t, f.ExpectedValue, decodedMsg.Data)
58 |
59 | case "jsonArray":
60 | assert.IsType(t, []any{}, decodedMsg.Data)
61 | assert.Equal(t, f.ExpectedValue, decodedMsg.Data)
62 | case "binary":
63 | assert.IsType(t, []byte{}, decodedMsg.Data)
64 | if f.ExpectedHexValue != "" {
65 | value, err := hex.DecodeString(f.ExpectedHexValue)
66 | require.NoError(t, err)
67 | assert.Equal(t, value, decodedMsg.Data)
68 | } else if f.ExpectedValue != nil {
69 | assert.Equal(t, []byte(f.ExpectedValue.(string)), decodedMsg.Data)
70 | }
71 | }
72 |
73 | // Test that the re-encoding of the decoded message gives us back the original fixture.
74 | reEncoded, err := decodedMsg.withEncodedData(nil)
75 | require.NoError(t, err)
76 | assert.Equal(t, f.Encoding, reEncoded.Encoding)
77 | if f.Encoding == "json" {
78 | require.IsType(t, "", reEncoded.Data)
79 | // json fields could be re-ordered so assert.Equal could fail.
80 | assert.JSONEq(t, f.Data, reEncoded.Data.(string))
81 | } else {
82 | assert.Equal(t, f.Data, reEncoded.Data)
83 | }
84 | })
85 | }
86 | }
87 |
88 | type MsgpackTestFixture struct {
89 | Name string `json:"name"`
90 | Data any `json:"data"`
91 | Encoding string `json:"encoding"`
92 | NumRepeat int `json:"numRepeat"`
93 | Type string `json:"type"`
94 | MsgPack string `json:"msgpack"`
95 | }
96 |
97 | func loadMsgpackFixtures() ([]MsgpackTestFixture, error) {
98 | var o []MsgpackTestFixture
99 |
100 | // We can't embed the fixtures as go:embed forbids embedding of resources above the current directory.
101 | text, err := os.ReadFile("../common/test-resources/msgpack_test_fixtures.json")
102 | if err != nil {
103 | return nil, err
104 | }
105 |
106 | err = json.Unmarshal(text, &o)
107 | return o, err
108 | }
109 |
110 | func TestMsgpackExtrasJsonCompatible(t *testing.T) {
111 | p := protocolMessage{
112 | Messages: []*Message{
113 | {
114 | Name: "my event",
115 | Data: "hello world",
116 | Extras: map[string]interface{}{
117 | "headers": map[string]interface{}{
118 | "version": 1,
119 | },
120 | },
121 | },
122 | },
123 | }
124 |
125 | b, err := ablyutil.MarshalMsgpack(p)
126 | assert.NoError(t, err)
127 |
128 | var got ProtocolMessage
129 | assert.NoError(t, ablyutil.UnmarshalMsgpack(b, &got))
130 |
131 | msg, err := got.Messages[0].withDecodedData(nil)
132 | assert.NoError(t, err)
133 |
134 | buff := bytes.Buffer{}
135 | assert.NoError(t, json.NewEncoder(&buff).Encode(msg))
136 | }
137 |
138 | func TestMsgpackDecoding(t *testing.T) {
139 | msgpackTestFixtures, err := loadMsgpackFixtures()
140 | require.NoError(t, err)
141 |
142 | for _, f := range msgpackTestFixtures {
143 | t.Run(f.Name, func(t *testing.T) {
144 | msgpackData := make([]byte, len(f.MsgPack))
145 | n, err := base64.StdEncoding.Decode(msgpackData, []byte(f.MsgPack))
146 | require.NoError(t, err)
147 | msgpackData = msgpackData[:n]
148 |
149 | var protoMsg ProtocolMessage
150 | err = ablyutil.UnmarshalMsgpack(msgpackData, &protoMsg)
151 | require.NoError(t, err)
152 |
153 | msg := protoMsg.Messages[0]
154 | decodedMsg, err := msg.withDecodedData(nil)
155 | switch f.Type {
156 | case "string":
157 | require.IsType(t, "string", decodedMsg.Data)
158 | assert.Equal(t, f.NumRepeat, len(decodedMsg.Data.(string)))
159 | assert.Equal(t, strings.Repeat(f.Data.(string), f.NumRepeat), decodedMsg.Data.(string))
160 | case "binary":
161 | require.IsType(t, []byte{}, decodedMsg.Data)
162 | assert.Equal(t, f.NumRepeat, len(decodedMsg.Data.([]byte)))
163 | assert.Equal(t, bytes.Repeat([]byte(f.Data.(string)), f.NumRepeat), decodedMsg.Data.([]byte))
164 | case "jsonObject", "jsonArray":
165 | assert.Equal(t, f.Data, decodedMsg.Data)
166 | }
167 |
168 | // Now re-encode and check that we get back the original message.
169 | reencodedMsg, err := msg.withEncodedData(nil)
170 | require.NoError(t, err)
171 | newMsg := ProtocolMessage{
172 | Messages: []*Message{&reencodedMsg},
173 | }
174 | newBytes, err := ablyutil.MarshalMsgpack(newMsg)
175 | require.NoError(t, err)
176 | assert.Equal(t, msgpackData, newBytes)
177 |
178 | })
179 | }
180 | }
181 |
--------------------------------------------------------------------------------
/ably/proto_message_doc.txt:
--------------------------------------------------------------------------------
1 | README
2 | ======
3 |
4 | This is the output of generate-test-data.js. Tests are based on the following output.
5 |
6 | Generating key:
7 | 00000000: b325 0600 5c9d 0027 817d df81 f37f aaa7 3%..\..'.}_.s.*'
8 | (16 bytes)
9 |
10 |
11 | Generating iv:
12 | 00000000: 88c2 93d8 819d 6453 753e b966 ab02 2993 .B.X..dSu>9f+.).
13 | (16 bytes)
14 |
15 |
16 | *******************************************************************
17 | Generating test data for: { string: 'utf-8™' }
18 |
19 |
20 | Unencoded message:
21 | { name: 'example', data: { string: 'utf-8™' } }
22 |
23 |
24 | Encoded message:
25 | { name: 'example',
26 | data: '{"string":"utf-8™"}',
27 | encoding: 'json' }
28 |
29 |
30 | Unencrypted JSON message format:
31 | {"name":"example","data":"{\"string\":\"utf-8™\"}","encoding":"json"}
32 |
33 |
34 | Unencrypted msgpack message format:
35 | 00000000: 83a4 6e61 6d65 a765 7861 6d70 6c65 a464 .$name'example$d
36 | 00000010: 6174 61b5 7b22 7374 7269 6e67 223a 2275 ata5{"string":"u
37 | 00000020: 7466 2d38 e284 a222 7da8 656e 636f 6469 tf-8b.""}(encodi
38 | 00000030: 6e67 a46a 736f 6e ng$json
39 | (55 bytes)
40 |
41 |
42 | Plaintext before encryption (without padding):
43 | 00000000: 7b22 7374 7269 6e67 223a 2275 7466 2d38 {"string":"utf-8
44 | 00000010: e284 a222 7d b.""}
45 | (21 bytes)
46 |
47 |
48 | Plaintext before encryption (including padding):
49 | 00000000: 7b22 7374 7269 6e67 223a 2275 7466 2d38 {"string":"utf-8
50 | 00000010: e284 a222 7d0b 0b0b 0b0b 0b0b 0b0b 0b0b b.""}...........
51 | (32 bytes)
52 |
53 |
54 | Raw cipher output:
55 | 00000000: cd5c bad4 a9ba 2b87 3da1 3622 b462 44f6 M\:T):+.=!6"4bDv
56 | 00000010: e83b e528 22bf dc01 8d22 df3d 2850 eb36 h;e("?\.."_=(Pk6
57 | (32 bytes)
58 |
59 |
60 | Encrypted value, iv + cipher output:
61 | 00000000: 88c2 93d8 819d 6453 753e b966 ab02 2993 .B.X..dSu>9f+.).
62 | 00000010: cd5c bad4 a9ba 2b87 3da1 3622 b462 44f6 M\:T):+.=!6"4bDv
63 | 00000020: e83b e528 22bf dc01 8d22 df3d 2850 eb36 h;e("?\.."_=(Pk6
64 | (48 bytes)
65 |
66 |
67 | Encrypted message format:
68 | { name: 'example',
69 | data: ,
70 | encoding: 'json/utf-8/cipher+aes-128-cbc' }
71 |
72 |
73 | Encrypted JSON message format:
74 | {"name":"example","data":"iMKT2IGdZFN1PrlmqwIpk81cutSpuiuHPaE2IrRiRPboO+UoIr/cAY0i3z0oUOs2","encoding":"json/utf-8/cipher+aes-128-cbc/base64"}
75 |
76 |
77 | Encrypted msgpack message format:
78 | 00000000: 83a4 6e61 6d65 a765 7861 6d70 6c65 a464 .$name'example$d
79 | 00000010: 6174 61c4 3088 c293 d881 9d64 5375 3eb9 ataD0.B.X..dSu>9
80 | 00000020: 66ab 0229 93cd 5cba d4a9 ba2b 873d a136 f+.).M\:T):+.=!6
81 | 00000030: 22b4 6244 f6e8 3be5 2822 bfdc 018d 22df "4bDvh;e("?\.."_
82 | 00000040: 3d28 50eb 36a8 656e 636f 6469 6e67 bd6a =(Pk6(encoding=j
83 | 00000050: 736f 6e2f 7574 662d 382f 6369 7068 6572 son/utf-8/cipher
84 | 00000060: 2b61 6573 2d31 3238 2d63 6263 +aes-128-cbc
85 | (108 bytes)
86 |
--------------------------------------------------------------------------------
/ably/proto_message_integration_test.go:
--------------------------------------------------------------------------------
1 | //go:build !unit
2 | // +build !unit
3 |
4 | package ably_test
5 |
6 | import (
7 | "encoding/json"
8 | "testing"
9 |
10 | "github.com/ably/ably-go/ably"
11 | "github.com/ably/ably-go/ablytest"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func TestMessage_CryptoDataFixtures_RSL6a1_RSL5b_RSL5c(t *testing.T) {
17 | fixtures := []struct {
18 | desc, file string
19 | keylength int
20 | }{
21 | {"with a 128 keylength", "test-resources/crypto-data-128.json", 128},
22 | {"with a 256 keylength", "test-resources/crypto-data-256.json", 126},
23 | }
24 |
25 | for _, fixture := range fixtures {
26 | // pin
27 | fixture := fixture
28 | t.Run(fixture.desc, func(t *testing.T) {
29 | test, key, iv, err := ablytest.LoadCryptoData(fixture.file)
30 | if err != nil {
31 | t.Fatal(err)
32 | }
33 | params := ably.CipherParams{
34 | Algorithm: ably.CipherAES,
35 | Key: key,
36 | }
37 | params.SetIV(iv)
38 | opts := &ably.ProtoChannelOptions{
39 | Cipher: params,
40 | }
41 | t.Run("fixture encode", func(t *testing.T) {
42 | for _, item := range test.Items {
43 | cipher, _ := opts.GetCipher()
44 |
45 | var encoded ably.Message
46 | err := json.Unmarshal(item.Encoded, &encoded)
47 | assert.NoError(t, err)
48 |
49 | encoded, err = ably.MessageWithDecodedData(encoded, cipher)
50 | assert.NoError(t, err)
51 |
52 | var encrypted ably.Message
53 | err = json.Unmarshal(item.Encoded, &encrypted)
54 | assert.NoError(t, err)
55 |
56 | encrypted, err = ably.MessageWithDecodedData(encrypted, cipher)
57 | assert.NoError(t, err)
58 | assert.Equal(t, encoded.Name, encrypted.Name,
59 | "expected %s got %s", encoded.Name, encrypted.Name)
60 | assert.Equal(t, encoded.Data, encrypted.Data,
61 | "expected %s got %s :encoding %s", encoded.Data, encrypted.Data, encoded.Encoding)
62 | }
63 | })
64 | })
65 | }
66 | }
67 |
68 | func TestMessage_CryptoDataFixtures_RSL6a1_RSL5b_RSL5c_TM3(t *testing.T) {
69 | fixtures := []struct {
70 | desc, file string
71 | keylength int
72 | }{
73 | {"with a 128 keylength", "test-resources/crypto-data-128.json", 128},
74 | {"with a 256 keylength", "test-resources/crypto-data-256.json", 126},
75 | }
76 |
77 | for _, fixture := range fixtures {
78 | // pin
79 | fixture := fixture
80 | t.Run(fixture.desc, func(t *testing.T) {
81 | test, key, iv, err := ablytest.LoadCryptoData(fixture.file)
82 | assert.NoError(t, err)
83 |
84 | params := ably.CipherParams{
85 | Algorithm: ably.CipherAES,
86 | Key: key,
87 | }
88 | params.SetIV(iv)
89 | opts := &ably.ProtoChannelOptions{
90 | Cipher: params,
91 | }
92 | t.Run("fixture encode", func(t *testing.T) {
93 | for _, item := range test.Items {
94 | cipher, _ := opts.GetCipher()
95 |
96 | var encoded ably.Message
97 | err := json.Unmarshal(item.Encoded, &encoded)
98 | assert.NoError(t, err)
99 |
100 | encoded, err = ably.MessageWithDecodedData(encoded, cipher)
101 | assert.NoError(t, err)
102 |
103 | var encrypted ably.Message
104 | err = json.Unmarshal(item.Encoded, &encrypted)
105 | assert.NoError(t, err)
106 |
107 | encrypted, err = ably.MessageWithDecodedData(encrypted, cipher)
108 | assert.NoError(t, err)
109 | assert.Equal(t, encoded.Name, encrypted.Name)
110 | assert.Equal(t, encoded.Data, encrypted.Data)
111 | }
112 | })
113 | })
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/ably/proto_message_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "encoding/base64"
8 | "encoding/json"
9 | "testing"
10 |
11 | "github.com/ably/ably-go/ably"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | type custom struct{}
17 |
18 | func (custom) MarshalJSON() ([]byte, error) {
19 | return json.Marshal([]string{"custom"})
20 | }
21 |
22 | func TestMessage_EncodeDecode_TM3(t *testing.T) {
23 | key, err := base64.StdEncoding.DecodeString("WUP6u0K7MXI5Zeo0VppPwg==")
24 | assert.NoError(t, err)
25 | iv, err := base64.StdEncoding.DecodeString("HO4cYSP8LybPYBPZPHQOtg==")
26 | assert.NoError(t, err)
27 | params := ably.CipherParams{
28 | Key: key,
29 | KeyLength: 128,
30 | Algorithm: ably.CipherAES,
31 | }
32 | params.SetIV(iv)
33 | opts := &ably.ProtoChannelOptions{
34 | Cipher: params,
35 | }
36 | sample := []struct {
37 | desc string
38 | data interface{}
39 | opts *ably.ProtoChannelOptions
40 | encodedJSON string
41 | decoded interface{}
42 | }{
43 | {
44 | // utf-8 string data should not have an encoding set, see:
45 | // https://github.com/ably/docs/issues/1165
46 | desc: "with valid utf-8 string data",
47 | data: "a string",
48 | decoded: "a string",
49 | encodedJSON: `{"data":"a string"}`,
50 | },
51 | {
52 | // invalid utf-8 string data should be base64 encoded
53 | desc: "with invalid utf-8 string data",
54 | data: "\xf0\x80\x80",
55 | decoded: []byte("\xf0\x80\x80"),
56 | encodedJSON: `{"data":"8ICA","encoding":"base64"}`,
57 | },
58 | {
59 | desc: "with a json encoding RSL4d3 map data",
60 | data: map[string]interface{}{
61 | "string": ably.EncUTF8,
62 | },
63 | decoded: map[string]interface{}{
64 | "string": ably.EncUTF8,
65 | },
66 | encodedJSON: `{"data":"{\"string\":\"utf-8\"}","encoding":"json"}`,
67 | },
68 | {
69 | desc: "with a json encoding RSL4d3 array data",
70 | data: []int64{1, 2, 3},
71 | decoded: []interface{}{
72 | float64(1.0),
73 | float64(2.0),
74 | float64(3.0),
75 | },
76 | encodedJSON: `{"data":"[1,2,3]","encoding":"json"}`,
77 | },
78 | {
79 | desc: "with a json encoding RSL4d3 json.Marshaler data",
80 | data: custom{},
81 | encodedJSON: `{"data":"[\"custom\"]","encoding":"json"}`,
82 | decoded: []interface{}{"custom"},
83 | },
84 | {
85 | desc: "with a base64 encoding RSL4d3 binary data",
86 | data: []byte(ably.EncBase64),
87 | decoded: []byte(ably.EncBase64),
88 | encodedJSON: `{"data":"YmFzZTY0","encoding":"base64"}`,
89 | },
90 | {
91 | desc: "with json/utf-8/cipher+aes-128-cbc/base64",
92 | data: map[string]interface{}{
93 | "string": `The quick brown fox jumped over the lazy dog`,
94 | },
95 | decoded: map[string]interface{}{
96 | "string": `The quick brown fox jumped over the lazy dog`,
97 | },
98 | opts: opts,
99 | encodedJSON: `{"data":"HO4cYSP8LybPYBPZPHQOtlT0v5P4AF9H1o0CEftPkErqe+ebUOoIPB9eMrSy092XGb9jaq3PdU2qLwz1lRqtEuUMgX8zDmtkTkweJEpE81Y=","encoding":"json/utf-8/cipher+aes-128-cbc/base64"}`,
100 | },
101 | }
102 |
103 | for _, v := range sample {
104 | // pin
105 | v := v
106 | t.Run(v.desc, func(t *testing.T) {
107 | cipher, _ := v.opts.GetCipher()
108 | msg, err := ably.MessageWithEncodedData(ably.Message{
109 | Data: v.data,
110 | }, cipher)
111 | assert.NoError(t, err)
112 | b, err := json.Marshal(msg)
113 | assert.NoError(t, err)
114 | assert.Equal(t, v.encodedJSON, string(b),
115 | "expected %s got %s", v.encodedJSON, string(b))
116 |
117 | var encoded ably.Message
118 | err = json.Unmarshal(b, &encoded)
119 | assert.NoError(t, err)
120 | decoded, err := ably.MessageWithDecodedData(encoded, cipher)
121 | assert.NoError(t, err)
122 | assert.Equal(t, v.decoded, decoded.Data,
123 | "expected %#v got %#v", v.decoded, decoded.Data)
124 | })
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/ably/proto_presence_message.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | )
8 |
9 | // PresenceAction describes the possible actions members in the presence set can emit (TP2).
10 | type PresenceAction int64
11 |
12 | const (
13 | // PresenceActionAbsent specifies a member is not present in the channel.
14 | PresenceActionAbsent PresenceAction = iota
15 | // PresenceActionPresent denotes when subscribing to presence events on a channel that already has members present,
16 | // this event is emitted for every member already present on the channel before the subscribe listener was registered.
17 | PresenceActionPresent
18 | // PresenceActionEnter denotes that new member has entered the channel.
19 | PresenceActionEnter
20 | // PresenceActionLeave is a member who was present has now left the channel. This may be a result of an explicit
21 | // request to leave or implicitly when detaching from the channel. Alternatively, if a member's connection is
22 | // abruptly disconnected and they do not resume their connection within a minute, Ably treats this as a
23 | // leave event as the client is no longer present.
24 | PresenceActionLeave
25 | // PresenceActionUpdate is a already present member has updated their member data. Being notified of member data
26 | // updates can be very useful, for example, it can be used to update the status of a user when they are
27 | // typing a message.
28 | PresenceActionUpdate
29 | )
30 |
31 | func (e PresenceAction) String() string {
32 | switch e {
33 | case PresenceActionAbsent:
34 | return "ABSENT"
35 | case PresenceActionPresent:
36 | return "PRESENT"
37 | case PresenceActionEnter:
38 | return "ENTER"
39 | case PresenceActionLeave:
40 | return "LEAVE"
41 | case PresenceActionUpdate:
42 | return "UPDATE"
43 | }
44 | return ""
45 | }
46 |
47 | func (PresenceAction) isEmitterEvent() {}
48 |
49 | type PresenceMessage struct {
50 | Message
51 | Action PresenceAction `json:"action" codec:"action"`
52 | }
53 |
54 | func (m PresenceMessage) String() string {
55 | return fmt.Sprintf("", [...]string{
56 | "absent",
57 | "present",
58 | "enter",
59 | "leave",
60 | "update",
61 | }[m.Action], m.ID, m.ConnectionID, m.ClientID, m.Data)
62 | }
63 |
64 | func (msg *PresenceMessage) isServerSynthesized() bool {
65 | return !strings.HasPrefix(msg.ID, msg.ConnectionID)
66 | }
67 |
68 | func (msg *PresenceMessage) getMsgSerialAndIndex() (int64, int64, error) {
69 | msgIds := strings.Split(msg.ID, ":")
70 | if len(msgIds) != 3 {
71 | return 0, 0, fmt.Errorf("parsing error, the presence message has invalid id %v", msg.ID)
72 | }
73 | msgSerial, err := strconv.ParseInt(msgIds[1], 10, 64)
74 | if err != nil {
75 | return 0, 0, fmt.Errorf("parsing error, the presence message has invalid msgSerial, for msgId %v", msg.ID)
76 | }
77 | msgIndex, err := strconv.ParseInt(msgIds[2], 10, 64)
78 | if err != nil {
79 | return 0, 0, fmt.Errorf("parsing error, the presence message has invalid msgIndex, for msgId %v", msg.ID)
80 | }
81 | return msgSerial, msgIndex, nil
82 | }
83 |
84 | // RTP2b, RTP2c
85 | func (msg1 *PresenceMessage) IsNewerThan(msg2 *PresenceMessage) (bool, error) {
86 | // RTP2b1
87 | if msg1.isServerSynthesized() || msg2.isServerSynthesized() {
88 | return msg1.Timestamp >= msg2.Timestamp, nil
89 | }
90 |
91 | // RTP2b2
92 | msg1Serial, msg1Index, err := msg1.getMsgSerialAndIndex()
93 | if err != nil {
94 | return false, err
95 | }
96 | msg2Serial, msg2Index, err := msg2.getMsgSerialAndIndex()
97 | if err != nil {
98 | return true, err
99 | }
100 | if msg1Serial == msg2Serial {
101 | return msg1Index > msg2Index, nil
102 | }
103 | return msg1Serial > msg2Serial, nil
104 | }
105 |
--------------------------------------------------------------------------------
/ably/proto_protocol_message_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "bytes"
8 | "strconv"
9 | "testing"
10 |
11 | "github.com/ably/ably-go/ably"
12 | "github.com/ably/ably-go/ably/internal/ablyutil"
13 |
14 | "github.com/stretchr/testify/assert"
15 | )
16 |
17 | // TestProtocolMessageEncodeZeroSerials tests that zero-valued serials are
18 | // explicitly encoded into msgpack (as required by the realtime API)
19 | func TestProtocolMessageEncodeZeroSerials(t *testing.T) {
20 | msg := ably.ProtocolMessage{
21 | ID: "test",
22 | MsgSerial: 0,
23 | }
24 | encoded, err := ablyutil.MarshalMsgpack(msg)
25 | assert.NoError(t, err)
26 | // expect a 2-element map with both the serial fields set to zero
27 | expected := []byte("\x82\xa2id\xa4test\xa9msgSerial\x00")
28 | assert.True(t, bytes.Equal(encoded, expected),
29 | "unexpected msgpack encoding\nexpected: %x\nactual: %x", expected, encoded)
30 | }
31 |
32 | func TestUpdateEmptyMessageFields_TM2a_TM2c_TM2f(t *testing.T) {
33 | messages := []*ably.Message{
34 | {
35 | ID: "",
36 | ConnectionID: "",
37 | Timestamp: 0,
38 | },
39 | {
40 | ID: "",
41 | ConnectionID: "",
42 | Timestamp: 0,
43 | },
44 | {
45 | ID: "",
46 | ConnectionID: "",
47 | Timestamp: 0,
48 | },
49 | }
50 |
51 | presenceMessages := []*ably.PresenceMessage{
52 | {
53 | Message: ably.Message{
54 | ID: "",
55 | ConnectionID: "",
56 | Timestamp: 0,
57 | },
58 | Action: 0,
59 | },
60 | {
61 | Message: ably.Message{
62 | ID: "",
63 | ConnectionID: "",
64 | Timestamp: 0,
65 | },
66 | Action: 0,
67 | },
68 | {
69 | Message: ably.Message{
70 | ID: "",
71 | ConnectionID: "",
72 | Timestamp: 0,
73 | },
74 | Action: 0,
75 | },
76 | {
77 | Message: ably.Message{
78 | ID: "",
79 | ConnectionID: "",
80 | Timestamp: 0,
81 | },
82 | Action: 0,
83 | },
84 | }
85 | protoMsg := ably.ProtocolMessage{
86 | Messages: messages,
87 | Presence: presenceMessages,
88 | ID: "msg-id",
89 | ConnectionID: "conn-id",
90 | Timestamp: 3453,
91 | }
92 |
93 | protoMsg.UpdateEmptyFields()
94 |
95 | for msgIndex, msg := range protoMsg.Messages {
96 | assert.Equal(t, protoMsg.ID+":"+strconv.Itoa(msgIndex), msg.ID)
97 | assert.Equal(t, protoMsg.ConnectionID, msg.ConnectionID)
98 | assert.Equal(t, protoMsg.Timestamp, msg.Timestamp)
99 | }
100 |
101 | for presenceMsgIndex, presenceMessage := range protoMsg.Presence {
102 | assert.Equal(t, protoMsg.ID+":"+strconv.Itoa(presenceMsgIndex), presenceMessage.ID)
103 | assert.Equal(t, protoMsg.ConnectionID, presenceMessage.ConnectionID)
104 | assert.Equal(t, protoMsg.Timestamp, presenceMessage.Timestamp)
105 | }
106 | }
107 |
108 | func TestIfFlagIsSet(t *testing.T) {
109 | flags := ably.FlagAttachResume
110 | flags.Set(ably.FlagPresence)
111 | flags.Set(ably.FlagPublish)
112 | flags.Set(ably.FlagSubscribe)
113 | flags.Set(ably.FlagPresenceSubscribe)
114 |
115 | assert.Equal(t, ably.FlagPresence, flags&ably.FlagPresence,
116 | "Expected %v, actual %v", ably.FlagPresence, flags&ably.FlagPresence)
117 | assert.Equal(t, ably.FlagPublish, flags&ably.FlagPublish,
118 | "Expected %v, actual %v", ably.FlagPublish, flags&ably.FlagPublish)
119 | assert.Equal(t, ably.FlagSubscribe, flags&ably.FlagSubscribe,
120 | "Expected %v, actual %v", ably.FlagSubscribe, flags&ably.FlagSubscribe)
121 | assert.Equal(t, ably.FlagPresenceSubscribe, flags&ably.FlagPresenceSubscribe,
122 | "Expected %v, actual %v", ably.FlagPresenceSubscribe, flags&ably.FlagPresenceSubscribe)
123 | assert.Equal(t, ably.FlagAttachResume, flags&ably.FlagAttachResume,
124 | "Expected %v, actual %v", ably.FlagAttachResume, flags&ably.FlagAttachResume)
125 | assert.NotEqual(t, ably.FlagHasBacklog, flags&ably.FlagAttachResume,
126 | "Shouldn't contain flag %v", ably.FlagHasBacklog)
127 | }
128 |
129 | func TestIfHasFlg(t *testing.T) {
130 | flags := ably.FlagAttachResume | ably.FlagPresence | ably.FlagPublish
131 | assert.True(t, flags.Has(ably.FlagAttachResume),
132 | "Should contain flag %v", ably.FlagAttachResume)
133 | assert.True(t, flags.Has(ably.FlagPresence),
134 | "Should contain flag %v", ably.FlagPresence)
135 | assert.True(t, flags.Has(ably.FlagPublish),
136 | "Should contain flag %v", ably.FlagPublish)
137 | assert.False(t, flags.Has(ably.FlagHasBacklog),
138 | "Shouldn't contain flag %v", ably.FlagHasBacklog)
139 | }
140 |
--------------------------------------------------------------------------------
/ably/proto_types.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "encoding/json"
5 | "time"
6 |
7 | "github.com/ugorji/go/codec"
8 | )
9 |
10 | // durationFromMsecs is a time.Duration that is marshaled as a JSON whole number of milliseconds.
11 | type durationFromMsecs time.Duration
12 |
13 | var _ interface {
14 | json.Marshaler
15 | json.Unmarshaler
16 | codec.Selfer
17 | } = (*durationFromMsecs)(nil)
18 |
19 | func (t durationFromMsecs) asMsecs() int64 {
20 | return time.Duration(t).Milliseconds()
21 | }
22 |
23 | func (t *durationFromMsecs) setFromMsecs(ms int64) {
24 | *t = durationFromMsecs(time.Duration(ms) * time.Millisecond)
25 | }
26 |
27 | func (t durationFromMsecs) MarshalJSON() ([]byte, error) {
28 | return json.Marshal(t.asMsecs())
29 | }
30 |
31 | func (t *durationFromMsecs) UnmarshalJSON(js []byte) error {
32 | var ms int64
33 | err := json.Unmarshal(js, &ms)
34 | if err != nil {
35 | return err
36 | }
37 | t.setFromMsecs(ms)
38 | return nil
39 | }
40 |
41 | func (t durationFromMsecs) CodecEncodeSelf(encoder *codec.Encoder) {
42 | encoder.MustEncode(t.asMsecs())
43 | }
44 |
45 | func (t *durationFromMsecs) CodecDecodeSelf(decoder *codec.Decoder) {
46 | var ms int64
47 | decoder.MustDecode(&ms)
48 | t.setFromMsecs(ms)
49 | }
50 |
--------------------------------------------------------------------------------
/ably/proto_types_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "encoding/json"
8 | "testing"
9 | "time"
10 |
11 | "github.com/ably/ably-go/ably"
12 | "github.com/ably/ably-go/ably/internal/ablyutil"
13 |
14 | "github.com/stretchr/testify/assert"
15 | )
16 |
17 | func TestDurationFromMsecsMarshal(t *testing.T) {
18 |
19 | for _, codec := range []struct {
20 | name string
21 | marshal func(interface{}) ([]byte, error)
22 | unmarshal func([]byte, interface{}) error
23 | }{
24 | {"JSON", json.Marshal, json.Unmarshal},
25 | {"Msgpack", ablyutil.MarshalMsgpack, ablyutil.UnmarshalMsgpack},
26 | } {
27 | t.Run(codec.name, func(t *testing.T) {
28 |
29 | js, err := codec.marshal(ably.DurationFromMsecs((123456 * time.Millisecond)))
30 | assert.NoError(t, err)
31 |
32 | var msecs int64
33 | err = codec.unmarshal(js, &msecs)
34 | assert.NoError(t, err)
35 | assert.Equal(t, int64(123456), msecs, "expected marshaling as JSON number of milliseconds; got %d (JSON: %q)", msecs, js)
36 |
37 | var decoded ably.DurationFromMsecs
38 | err = codec.unmarshal(js, &decoded)
39 | assert.NoError(t, err)
40 | assert.Equal(t, (123456 * time.Millisecond), time.Duration(decoded),
41 | "expected json.Unmarshal after Marshal to produce the same duration; got %v", time.Duration(decoded))
42 | })
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/ably/realtime_channel_internal_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestChannelOptionChannelWithCipherKey(t *testing.T) {
13 | tests := map[string]struct {
14 | key []byte
15 | expectedResult *channelOptions
16 | }{
17 | "Can inject a cipher key of length 128 into cipher params": {
18 | key: []byte{82, 27, 7, 33, 130, 101, 79, 22, 63, 95, 15, 154, 98, 29, 114, 19},
19 | expectedResult: &channelOptions{
20 | Cipher: CipherParams{
21 | Algorithm: CipherAES,
22 | KeyLength: 128,
23 | Key: []uint8{0x52, 0x1b, 0x7, 0x21, 0x82, 0x65, 0x4f, 0x16, 0x3f, 0x5f, 0xf, 0x9a, 0x62, 0x1d, 0x72, 0x13},
24 | Mode: CipherCBC,
25 | },
26 | },
27 | },
28 |
29 | "Can inject a cipher key of length 256 into cipher params": {
30 | key: []byte{82, 27, 7, 33, 130, 101, 79, 22, 63, 95, 15, 154, 98, 29, 114, 19, 10, 23, 45, 56, 76, 29, 111, 23, 93, 22, 44, 66, 88, 43, 72, 42},
31 | expectedResult: &channelOptions{
32 | Cipher: CipherParams{
33 | Algorithm: CipherAES,
34 | KeyLength: 256,
35 | Key: []uint8{0x52, 0x1b, 0x7, 0x21, 0x82, 0x65, 0x4f, 0x16, 0x3f, 0x5f, 0xf, 0x9a, 0x62, 0x1d, 0x72, 0x13, 0xa, 0x17, 0x2d, 0x38, 0x4c, 0x1d, 0x6f, 0x17, 0x5d, 0x16, 0x2c, 0x42, 0x58, 0x2b, 0x48, 0x2a},
36 | Mode: CipherCBC,
37 | },
38 | },
39 | },
40 | }
41 |
42 | for testName, test := range tests {
43 | t.Run(testName, func(t *testing.T) {
44 | opt := ChannelWithCipherKey(test.key)
45 | result := applyChannelOptions(opt)
46 | assert.Equal(t, test.expectedResult, result)
47 | })
48 | }
49 | }
50 |
51 | func TestChannelOptionChannelWithCipher(t *testing.T) {
52 | tests := map[string]struct {
53 | params CipherParams
54 | expectedResult *channelOptions
55 | }{
56 | "Can set cipher params as channel options": {
57 | params: CipherParams{
58 | Algorithm: CipherAES,
59 | KeyLength: 128,
60 | Key: []uint8{0x52, 0x1b, 0x7, 0x21, 0x82, 0x65, 0x4f, 0x16, 0x3f, 0x5f, 0xf, 0x9a, 0x62, 0x1d, 0x72, 0x13},
61 | Mode: CipherCBC,
62 | },
63 | expectedResult: &channelOptions{
64 | Cipher: CipherParams{
65 | Algorithm: CipherAES,
66 | KeyLength: 128,
67 | Key: []uint8{0x52, 0x1b, 0x7, 0x21, 0x82, 0x65, 0x4f, 0x16, 0x3f, 0x5f, 0xf, 0x9a, 0x62, 0x1d, 0x72, 0x13},
68 | Mode: CipherCBC,
69 | },
70 | },
71 | },
72 | }
73 |
74 | for testName, test := range tests {
75 | t.Run(testName, func(t *testing.T) {
76 | opt := ChannelWithCipher(test.params)
77 | result := applyChannelOptions(opt)
78 | assert.Equal(t, test.expectedResult, result)
79 | })
80 | }
81 | }
82 |
83 | func TestChannelOptionChannelWithParams(t *testing.T) {
84 | tests := map[string]struct {
85 | key string
86 | value string
87 | expectedResult *channelOptions
88 | }{
89 | "Can set a key and a value as channel options": {
90 | key: "aKey",
91 | value: "aValue",
92 | expectedResult: &channelOptions{
93 | Params: map[string]string{"aKey": "aValue"},
94 | },
95 | },
96 | }
97 |
98 | for testName, test := range tests {
99 | t.Run(testName, func(t *testing.T) {
100 | opt := ChannelWithParams(test.key, test.value)
101 | result := applyChannelOptions(opt)
102 | assert.Equal(t, test.expectedResult, result)
103 | })
104 | }
105 | }
106 |
107 | func TestChannelOptionChannelWithModes(t *testing.T) {
108 | tests := map[string]struct {
109 | modes []ChannelMode
110 | expectedResult *channelOptions
111 | }{
112 | "Can set a channel mode as channel options": {
113 | modes: []ChannelMode{ChannelModePresence},
114 | expectedResult: &channelOptions{
115 | Modes: []ChannelMode{ChannelModePresence},
116 | },
117 | },
118 | "Can set multiple channel mode as channel options": {
119 | modes: []ChannelMode{ChannelModePresence, ChannelModePublish},
120 | expectedResult: &channelOptions{
121 | Modes: []ChannelMode{ChannelModePresence, ChannelModePublish},
122 | },
123 | },
124 | }
125 |
126 | for testName, test := range tests {
127 | t.Run(testName, func(t *testing.T) {
128 | opt := ChannelWithModes(test.modes...)
129 | result := applyChannelOptions(opt)
130 | assert.Equal(t, test.expectedResult, result)
131 | })
132 | }
133 | }
134 |
135 | func TestChannelGet(t *testing.T) {
136 | tests := map[string]struct {
137 | mock *RealtimeChannels
138 | name string
139 | expectedChannelName string
140 | expectedChannelState ChannelState
141 | }{
142 | "If channel does not exist, it is created and initialised": {
143 | mock: &RealtimeChannels{
144 | chans: map[string]*RealtimeChannel{},
145 | client: &Realtime{
146 | rest: &REST{
147 | log: logger{l: &stdLogger{mocklogger}},
148 | },
149 | },
150 | },
151 | name: "new",
152 | expectedChannelName: "new",
153 | expectedChannelState: ChannelStateInitialized,
154 | },
155 | }
156 |
157 | for testName, test := range tests {
158 | t.Run(testName, func(t *testing.T) {
159 | result := test.mock.Get(test.name)
160 | assert.Equal(t, test.expectedChannelName, result.Name)
161 | assert.Equal(t, test.expectedChannelState, result.state)
162 | })
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/ably/realtime_channel_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "context"
8 | "fmt"
9 | "time"
10 |
11 | "github.com/ably/ably-go/ably"
12 | )
13 |
14 | // When publishing a message to a channel, data can be either a single string or
15 | // a struct of type Message. This example shows the different ways to publish a message.
16 | func ExampleRealtimeChannel_Publish() {
17 |
18 | // Create a new realtime client.
19 | client, err := ably.NewRealtime(
20 | ably.WithKey("ABLY_PRIVATE_KEY"),
21 | ably.WithClientID("Client A"),
22 | )
23 | if err != nil {
24 | fmt.Println(err)
25 | return
26 | }
27 |
28 | // Initialise a new channel.
29 | channel := client.Channels.Get("chat")
30 |
31 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
32 | defer cancel()
33 |
34 | // Publish a string to the channel
35 | if err := channel.Publish(ctx, "chat_message", "Hello, how are you?"); err != nil {
36 | fmt.Println(err)
37 | return
38 | }
39 |
40 | // Publish a Message to the channel
41 | newChatMessage := ably.Message{
42 | Name: "chat_message",
43 | Data: "Hello, how are you?",
44 | }
45 |
46 | if err := channel.Publish(ctx, "chat_message", newChatMessage); err != nil {
47 | fmt.Println(err)
48 | return
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/ably/realtime_client.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "context"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | // Realtime is an ably realtime client that extends the functionality of the [ably.REST] and provides
10 | // additional realtime-specific features.
11 | type Realtime struct {
12 | // An [ably.Auth] object (RTC4).
13 | Auth *Auth
14 | // A [ably.RealtimeChannels] object (RTC3, RTS1).
15 | Channels *RealtimeChannels
16 | // A [ably.Connection] object (RTC2).
17 | Connection *Connection
18 | rest *REST
19 | }
20 |
21 | // NewRealtime constructs a new [ably.Realtime] client object using an Ably [ably.ClientOption] object (RSC1)
22 | func NewRealtime(options ...ClientOption) (*Realtime, error) {
23 | c := &Realtime{}
24 | rest, err := NewREST(options...) //options validated in NewREST
25 | if err != nil {
26 | return nil, err
27 | }
28 | c.rest = rest
29 | c.Auth = rest.Auth
30 | c.Channels = newChannels(c)
31 | conn := newConn(c.opts(), rest.Auth, connCallbacks{
32 | c.onChannelMsg,
33 | c.onReconnected,
34 | c.onReconnectionFailed,
35 | }, c)
36 | conn.internalEmitter.OnAll(func(change ConnectionStateChange) {
37 | c.Channels.broadcastConnStateChange(change)
38 | })
39 | c.Connection = conn
40 |
41 | // RTN16
42 | if !empty(c.opts().Recover) {
43 | recoverKeyContext, err := DecodeRecoveryKey(c.opts().Recover)
44 | if err != nil {
45 | // Ignoring error since no recover will be used for new connection
46 | c.log().Errorf("Error decoding recover with error %v", err)
47 | c.log().Errorf("Trying a fresh connection instead")
48 | } else {
49 | c.Channels.SetChannelSerialsFromRecoverOption(recoverKeyContext.ChannelSerials) // RTN16j
50 | c.Connection.msgSerial = recoverKeyContext.MsgSerial // RTN16f
51 | }
52 | }
53 | return c, nil
54 | }
55 |
56 | // Connect calls Connection.Connect and causes the connection to open, entering the connecting state.
57 | // Explicitly calling Connect() is needed if the ClientOptions.NoConnect is set true (proxy for RTN11).
58 | func (c *Realtime) Connect() {
59 | c.Connection.Connect()
60 | }
61 |
62 | // Close calls Connection.Close and causes the connection to close, entering the closing state. Once closed,
63 | // the library will not attempt to re-establish the connection without an explicit call to Connection.Connect
64 | // proxy for RTN12
65 | func (c *Realtime) Close() {
66 | c.Connection.Close()
67 | }
68 |
69 | // Stats queries the REST /stats API and retrieves your application's usage statistics.
70 | // Returns a [ably.PaginatedResult] object, containing an array of [ably.Stats] objects (RTC5).
71 | //
72 | // See package-level documentation => [ably] Pagination for handling stats pagination.
73 | func (c *Realtime) Stats(o ...StatsOption) StatsRequest {
74 | return c.rest.Stats(o...)
75 | }
76 |
77 | // Time retrieves the time from the Ably service as milliseconds since the Unix epoch.
78 | // Clients that do not have access to a sufficiently well maintained time source and wish to issue Ably
79 | // multiple [ably.TokenRequest] with a more accurate timestamp should use the clientOptions.UseQueryTime property
80 | // instead of this method (RTC6a).
81 | func (c *Realtime) Time(ctx context.Context) (time.Time, error) {
82 | return c.rest.Time(ctx)
83 | }
84 |
85 | func (c *Realtime) onChannelMsg(msg *protocolMessage) {
86 | c.Channels.Get(msg.Channel).notify(msg)
87 | }
88 |
89 | func (c *Realtime) onReconnected(failedResumeOrRecover bool) {
90 | for _, ch := range c.Channels.Iterate() {
91 | switch ch.State() {
92 | // RTN15g3, RTN15c6, RTN15c7, RTN16l
93 | case ChannelStateAttaching, ChannelStateAttached, ChannelStateSuspended:
94 | ch.mayAttach(false)
95 | case ChannelStateDetaching: //RTN19b
96 | ch.detachSkipVerifyActive()
97 | }
98 | }
99 |
100 | if failedResumeOrRecover { //RTN19a1
101 | c.Connection.resendPending()
102 | } else { //RTN19a2 - successful resume, msgSerial doesn't change
103 | c.Connection.resendAcks()
104 | }
105 | }
106 |
107 | func (c *Realtime) onReconnectionFailed(err *errorInfo) {
108 | for _, ch := range c.Channels.Iterate() {
109 | ch.setState(ChannelStateFailed, newErrorFromProto(err), false)
110 | }
111 | }
112 |
113 | func isTokenError(err *errorInfo) bool {
114 | return err != nil && err.StatusCode == http.StatusUnauthorized && (40140 <= err.Code && err.Code < 40150)
115 | }
116 |
117 | func (c *Realtime) opts() *clientOptions {
118 | return c.rest.opts
119 | }
120 |
121 | func (c *Realtime) log() logger {
122 | return c.rest.log
123 | }
124 |
--------------------------------------------------------------------------------
/ably/realtime_client_internal_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably
5 |
6 | import (
7 | "errors"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestNewRealtime(t *testing.T) {
14 | tests := map[string]struct {
15 | options []ClientOption
16 | expectedErr error
17 | }{
18 | "Can handle invalid key error when WithKey option is not provided": {
19 | options: []ClientOption{},
20 | expectedErr: &ErrorInfo{
21 | StatusCode: 400,
22 | Code: 40005,
23 | err: errors.New("invalid key"),
24 | },
25 | },
26 | "Can create a new realtime client with a valid key": {
27 | options: []ClientOption{WithKey("abc:def")},
28 | },
29 | }
30 |
31 | for testName, test := range tests {
32 | t.Run(testName, func(t *testing.T) {
33 |
34 | client, err := NewRealtime(test.options...)
35 | assert.Equal(t, test.expectedErr, err)
36 |
37 | if client != nil {
38 | assert.Equal(t, "abc:def", client.Auth.opts().Key)
39 | // Assert all client fields have been populated
40 | assert.NotNil(t, client.rest)
41 | assert.NotNil(t, client.Auth)
42 | assert.NotNil(t, client.Channels)
43 | assert.NotNil(t, client.Channels.client)
44 | assert.NotNil(t, client.Channels.chans)
45 | assert.NotNil(t, client.Connection)
46 | }
47 | })
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/ably/realtime_presence_internal_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably
5 |
6 | import (
7 | "bytes"
8 | "context"
9 | "errors"
10 | "log"
11 | "testing"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | var (
17 | buffer bytes.Buffer
18 | mocklogger = log.New(&buffer, "logger: ", log.Lshortfile)
19 | )
20 |
21 | // mockChannelWithState is a test helper that returns a mock channel in a specified state
22 | func mockChannelWithState(channelState *ChannelState, connectionState *ConnectionState) *RealtimeChannel {
23 | mockChannel := RealtimeChannel{
24 | client: &Realtime{
25 | rest: &REST{
26 | log: logger{l: &stdLogger{mocklogger}},
27 | },
28 | Connection: &Connection{},
29 | },
30 | }
31 | if channelState != nil {
32 | mockChannel.state = *channelState
33 | }
34 | if connectionState != nil {
35 | mockChannel.client.Connection.state = *connectionState
36 | }
37 | return &mockChannel
38 | }
39 |
40 | func TestVerifyChanState_RTP16(t *testing.T) {
41 | tests := map[string]struct {
42 | channel *RealtimeChannel
43 | expectedErr error
44 | }{
45 | `No error if the channel is in state: "INITIALIZED"`: {
46 | channel: mockChannelWithState(&ChannelStateInitialized, nil),
47 | expectedErr: nil,
48 | },
49 | `No error if the channel is in state: "ATTACHING"`: {
50 | channel: mockChannelWithState(&ChannelStateAttaching, nil),
51 | expectedErr: nil,
52 | },
53 | `No error if the channel is in state: "ATTACHED"`: {
54 | channel: mockChannelWithState(&ChannelStateAttached, nil),
55 | expectedErr: nil,
56 | },
57 | `Error if the channel is in state: "SUSPENDED"`: {
58 | channel: mockChannelWithState(&ChannelStateSuspended, nil),
59 | expectedErr: newError(91001, errors.New("unable to enter presence channel (invalid channel state: SUSPENDED)")),
60 | },
61 | `Error if the channel is in state: "DETACHING"`: {
62 | channel: mockChannelWithState(&ChannelStateDetaching, nil),
63 | expectedErr: newError(91001, errors.New("unable to enter presence channel (invalid channel state: DETACHING)")),
64 | },
65 | `Error if the channel is in state: "DETACHED"`: {
66 | channel: mockChannelWithState(&ChannelStateDetached, nil),
67 | expectedErr: newError(91001, errors.New("unable to enter presence channel (invalid channel state: DETACHED)")),
68 | },
69 | `Error if the channel is in state: "FAILED"`: {
70 | channel: mockChannelWithState(&ChannelStateFailed, nil),
71 | expectedErr: newError(91001, errors.New("unable to enter presence channel (invalid channel state: FAILED)")),
72 | },
73 | }
74 |
75 | for testName, test := range tests {
76 | t.Run(testName, func(t *testing.T) {
77 | presence := newRealtimePresence(test.channel)
78 | err := presence.isValidChannelState()
79 | assert.Equal(t, test.expectedErr, err)
80 | })
81 | }
82 | }
83 |
84 | func TestSend(t *testing.T) {
85 | tests := map[string]struct {
86 | channel *RealtimeChannel
87 | msg Message
88 | expectedResult result
89 | expectedErr error
90 | }{
91 | `No error sending presence if the channel is in state: "ATTACHED"`: {
92 | channel: mockChannelWithState(&ChannelStateAttached, nil),
93 | msg: Message{Name: "Hello"},
94 | expectedErr: (*ErrorInfo)(nil),
95 | },
96 | `Error if channel is: "ATTACHED" and connection is :"CLOSED"`: {
97 | channel: mockChannelWithState(&ChannelStateAttached, &ConnectionStateClosed),
98 | msg: Message{Name: "Hello"},
99 | expectedErr: newError(80017, errors.New("Connection unavailable")),
100 | },
101 | `Error if channel is: "DETACHED" and connection is :"CLOSED"`: {
102 | channel: mockChannelWithState(&ChannelStateDetached, &ConnectionStateClosed),
103 | msg: Message{Name: "Hello"},
104 | expectedErr: newError(91001, errors.New("unable to enter presence channel (invalid channel state: DETACHED)")),
105 | },
106 | }
107 |
108 | for testName, test := range tests {
109 | t.Run(testName, func(t *testing.T) {
110 | presence := newRealtimePresence(test.channel)
111 | err := presence.EnterClient(context.Background(), "clientId", &test.msg)
112 | assert.Equal(t, test.expectedErr, err.(*ErrorInfo))
113 | })
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/ably/recovery_context.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import "encoding/json"
4 |
5 | // RecoveryKeyContext contains the properties required to recover existing connection.
6 | type RecoveryKeyContext struct {
7 | ConnectionKey string `json:"connectionKey" codec:"connectionKey"`
8 | MsgSerial int64 `json:"msgSerial" codec:"msgSerial"`
9 | ChannelSerials map[string]string `json:"channelSerials" codec:"channelSerials"`
10 | }
11 |
12 | func (r *RecoveryKeyContext) Encode() (serializedRecoveryKey string, err error) {
13 | result, err := json.Marshal(r)
14 | if err != nil {
15 | serializedRecoveryKey = ""
16 | } else {
17 | serializedRecoveryKey = string(result)
18 | }
19 | return
20 | }
21 |
22 | func DecodeRecoveryKey(recoveryKey string) (rCtx *RecoveryKeyContext, err error) {
23 | err = json.Unmarshal([]byte(recoveryKey), &rCtx)
24 | if err != nil {
25 | rCtx = nil
26 | }
27 | return
28 | }
29 |
--------------------------------------------------------------------------------
/ably/recovery_context_test.go:
--------------------------------------------------------------------------------
1 | //go:build !unit
2 | // +build !unit
3 |
4 | package ably_test
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/ably/ably-go/ably"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func Test_ShouldEncodeRecoveryKeyContextObject(t *testing.T) {
14 | var expectedRecoveryKey = "{\"connectionKey\":\"uniqueKey\",\"msgSerial\":1,\"channelSerials\":{\"channel1\":\"1\",\"channel2\":\"2\",\"channel3\":\"3\"}}"
15 | var recoveryKey = &ably.RecoveryKeyContext{
16 | ConnectionKey: "uniqueKey",
17 | MsgSerial: 1,
18 | ChannelSerials: map[string]string{
19 | "channel1": "1",
20 | "channel2": "2",
21 | "channel3": "3",
22 | },
23 | }
24 | key, err := recoveryKey.Encode()
25 | assert.Nil(t, err)
26 | assert.Equal(t, expectedRecoveryKey, key)
27 | }
28 |
29 | func Test_ShouldDecodeRecoveryKeyToRecoveryKeyContextObject(t *testing.T) {
30 | var recoveryKey = "{\"connectionKey\":\"uniqueKey\",\"msgSerial\":1,\"channelSerials\":{\"channel1\":\"1\",\"channel2\":\"2\",\"channel3\":\"3\"}}"
31 | keyContext, err := ably.DecodeRecoveryKey(recoveryKey)
32 | assert.Nil(t, err)
33 | assert.Equal(t, int64(1), keyContext.MsgSerial)
34 | assert.Equal(t, "uniqueKey", keyContext.ConnectionKey)
35 | assert.Equal(t, "1", keyContext.ChannelSerials["channel1"])
36 | assert.Equal(t, "2", keyContext.ChannelSerials["channel2"])
37 | assert.Equal(t, "3", keyContext.ChannelSerials["channel3"])
38 | }
39 |
40 | func Test_ShouldReturnNullRecoveryContextWhileDecodingFaultyRecoveryKey(t *testing.T) {
41 | var recoveryKey = "{\"connectionKey\":\"uniqueKey\",\"msgSerial\":\"incorrectStringSerial\",\"channelSerials\":{\"channel1\":\"1\",\"channel2\":\"2\",\"channel3\":\"3\"}}"
42 | keyContext, err := ably.DecodeRecoveryKey(recoveryKey)
43 | assert.NotNil(t, err)
44 | assert.Nil(t, keyContext)
45 | }
46 |
--------------------------------------------------------------------------------
/ably/rest_channel_spec_integration_test.go:
--------------------------------------------------------------------------------
1 | //go:build !unit
2 | // +build !unit
3 |
4 | package ably_test
5 |
6 | import (
7 | "context"
8 | "fmt"
9 | "reflect"
10 | "testing"
11 |
12 | "github.com/ably/ably-go/ably"
13 | "github.com/ably/ably-go/ablytest"
14 |
15 | "github.com/stretchr/testify/assert"
16 | )
17 |
18 | func TestRSL1f1(t *testing.T) {
19 | app, err := ablytest.NewSandbox(nil)
20 | assert.NoError(t, err)
21 | defer app.Close()
22 | opts := app.Options()
23 | // RSL1f
24 | opts = append(opts, ably.WithUseTokenAuth(false))
25 | client, err := ably.NewREST(opts...)
26 | assert.NoError(t, err)
27 | channel := client.Channels.Get("RSL1f")
28 | var msgs []*ably.Message
29 | size := 10
30 | for i := 0; i < size; i++ {
31 | msgs = append(msgs, &ably.Message{
32 | ClientID: "any_client_id",
33 | Data: fmt.Sprint(i),
34 | })
35 | }
36 | err = channel.PublishMultiple(context.Background(), msgs)
37 | assert.NoError(t, err)
38 | var m []*ably.Message
39 | err = ablytest.AllPages(&m, channel.History())
40 | assert.NoError(t, err)
41 | assert.Equal(t, 10, len(m),
42 | "expected 10 messages got %d", len(m))
43 | for _, v := range m {
44 | assert.Equal(t, "any_client_id", v.ClientID,
45 | "expected clientId \"any_client_id\" got %s data:%v", v.ClientID, v.Data)
46 | }
47 | }
48 |
49 | func TestRSL1g(t *testing.T) {
50 | app, err := ablytest.NewSandbox(nil)
51 | assert.NoError(t, err)
52 | defer app.Close()
53 | opts := append(app.Options(),
54 | ably.WithUseTokenAuth(true),
55 | )
56 | opts = append(opts, ably.WithClientID("some_client_id"))
57 | client, err := ably.NewREST(opts...)
58 | assert.NoError(t, err)
59 | t.Run("RSL1g1b", func(t *testing.T) {
60 | channel := client.Channels.Get("RSL1g1b")
61 | err := channel.PublishMultiple(context.Background(), []*ably.Message{
62 | {Name: "some 1"},
63 | {Name: "some 2"},
64 | {Name: "some 3"},
65 | })
66 | if err != nil {
67 | t.Fatal(err)
68 | }
69 | var history []*ably.Message
70 | err = ablytest.AllPages(&history, channel.History())
71 | assert.NoError(t, err)
72 | for _, m := range history {
73 | assert.Equal(t, "some_client_id", m.ClientID,
74 | "expected \"some_client_id\" got %s", m.ClientID)
75 | }
76 | })
77 | t.Run("RSL1g2", func(t *testing.T) {
78 | channel := client.Channels.Get("RSL1g2")
79 | err := channel.PublishMultiple(context.Background(), []*ably.Message{
80 | {Name: "1", ClientID: "some_client_id"},
81 | {Name: "2", ClientID: "some_client_id"},
82 | {Name: "3", ClientID: "some_client_id"},
83 | })
84 | if err != nil {
85 | t.Fatal(err)
86 | }
87 | var history []*ably.Message
88 | err = ablytest.AllPages(&history, channel.History())
89 | if err != nil {
90 | t.Fatal(err)
91 | }
92 | for _, m := range history {
93 | assert.Equal(t, "some_client_id", m.ClientID,
94 | "expected \"some_client_id\" got %s", m.ClientID)
95 | }
96 | })
97 | t.Run("RSL1g3", func(t *testing.T) {
98 | channel := client.Channels.Get("RSL1g3")
99 | err := channel.PublishMultiple(context.Background(), []*ably.Message{
100 | {Name: "1", ClientID: "some_client_id"},
101 | {Name: "2", ClientID: "other client"},
102 | {Name: "3", ClientID: "some_client_id"},
103 | })
104 | assert.Error(t, err,
105 | "expected an error")
106 | })
107 | }
108 |
109 | func TestHistory_RSL2_RSL2b3(t *testing.T) {
110 |
111 | for _, limit := range []int{2, 3, 20} {
112 | t.Run(fmt.Sprintf("limit=%d", limit), func(t *testing.T) {
113 | app, rest := ablytest.NewREST()
114 | defer app.Close()
115 | channel := rest.Channels.Get("persisted:test")
116 |
117 | fixtures := historyFixtures()
118 | channel.PublishMultiple(context.Background(), fixtures)
119 |
120 | err := ablytest.TestPagination(
121 | reverseMessages(fixtures),
122 | channel.History(ably.HistoryWithLimit(limit)),
123 | limit,
124 | ablytest.PaginationWithEqual(messagesEqual),
125 | )
126 | assert.NoError(t, err)
127 | })
128 | }
129 | }
130 |
131 | func TestHistory_Direction_RSL2b2(t *testing.T) {
132 | for _, c := range []struct {
133 | direction ably.Direction
134 | expected []*ably.Message
135 | }{
136 | {
137 | direction: ably.Backwards,
138 | expected: reverseMessages(historyFixtures()),
139 | },
140 | {
141 | direction: ably.Forwards,
142 | expected: historyFixtures(),
143 | },
144 | } {
145 | c := c
146 | t.Run(fmt.Sprintf("direction=%v", c.direction), func(t *testing.T) {
147 | app, rest := ablytest.NewREST()
148 | defer app.Close()
149 | channel := rest.Channels.Get("persisted:test")
150 |
151 | fixtures := historyFixtures()
152 | channel.PublishMultiple(context.Background(), fixtures)
153 |
154 | expected := c.expected
155 |
156 | err := ablytest.TestPagination(expected, channel.History(
157 | ably.HistoryWithLimit(len(expected)),
158 | ably.HistoryWithDirection(c.direction),
159 | ), 100, ablytest.PaginationWithEqual(messagesEqual))
160 | assert.NoError(t, err)
161 | })
162 | }
163 | }
164 |
165 | func TestGetChannelLifecycleStatus_RSL8(t *testing.T) {
166 | ctx := context.Background()
167 | app, rest := ablytest.NewREST()
168 | defer app.Close()
169 |
170 | t.Run("Test Channel Status after Publish", func(t *testing.T) {
171 | channel := rest.Channels.Get("lifecycle:test")
172 | err := channel.Publish(ctx, "event", "data")
173 | assert.NoError(t, err)
174 | status, err := channel.Status(ctx)
175 | assert.NoError(t, err)
176 | assert.NotNil(t, status.ChannelId)
177 | assert.True(t, status.Status.IsActive)
178 | assert.Equal(t, "lifecycle:test", status.ChannelId)
179 | })
180 | }
181 |
182 | func historyFixtures() []*ably.Message {
183 | var fixtures []*ably.Message
184 | for i := 0; i < 10; i++ {
185 | fixtures = append(fixtures, &ably.Message{Name: fmt.Sprintf("msg%d", i)})
186 | }
187 | return fixtures
188 | }
189 |
190 | func reverseMessages(msgs []*ably.Message) []*ably.Message {
191 | var reversed []*ably.Message
192 | for i := len(msgs) - 1; i >= 0; i-- {
193 | reversed = append(reversed, msgs[i])
194 | }
195 | return reversed
196 | }
197 |
198 | func messagesEqual(x, y interface{}) bool {
199 | mx, my := x.(*ably.Message), y.(*ably.Message)
200 | return mx.Name == my.Name && reflect.DeepEqual(mx.Data, my.Data)
201 | }
202 |
--------------------------------------------------------------------------------
/ably/rest_client_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably_test
5 |
6 | import (
7 | "context"
8 | "fmt"
9 | "time"
10 |
11 | "github.com/ably/ably-go/ably"
12 | )
13 |
14 | // When publishing a message to a channel, data can be either a single string or
15 | // a struct of type Message. This example shows the different ways to publish a message.
16 | func ExampleRESTChannel_Publish() {
17 |
18 | // Create a new REST client.
19 | client, err := ably.NewREST(
20 | ably.WithKey("ABLY_PRIVATE_KEY"),
21 | ably.WithClientID("Client A"),
22 | )
23 | if err != nil {
24 | fmt.Println(err)
25 | return
26 | }
27 |
28 | // Initialise a new channel.
29 | channel := client.Channels.Get("chat")
30 |
31 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
32 | defer cancel()
33 |
34 | // Publish a string to the channel.
35 | if err := channel.Publish(ctx, "chat_message", "Hello, how are you?"); err != nil {
36 | fmt.Println(err)
37 | return
38 | }
39 |
40 | // Publish a single message to the channel.
41 | newChatMessage := ably.Message{
42 | Name: "chat_message",
43 | Data: "Hello, how are you?",
44 | }
45 |
46 | if err := channel.Publish(ctx, "chat_message", newChatMessage); err != nil {
47 | fmt.Println(err)
48 | return
49 | }
50 |
51 | // Publish multiple messages in a single request.
52 | if err := channel.PublishMultiple(ctx, []*ably.Message{
53 | {Name: "HelloEvent", Data: "Hello!"},
54 | {Name: "ByeEvent", Data: "Bye!"},
55 | }); err != nil {
56 | fmt.Println(err)
57 | return
58 | }
59 |
60 | // Publish a message on behalf of a different client.
61 | if err := channel.Publish(ctx, "temperature", "12.7",
62 | ably.PublishWithConnectionKey("connectionKeyOfAnotherClient"),
63 | ); err != nil {
64 | fmt.Println(err)
65 | return
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/ably/token.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "crypto/hmac"
5 | "crypto/sha256"
6 | "encoding/base64"
7 | "fmt"
8 | "net/url"
9 | "strconv"
10 | "time"
11 | )
12 |
13 | // TokenParams contains token params to be sent to ably to get auth token
14 | type TokenParams struct {
15 | // TTL is a requested time to live for the token in milliseconds. If the token request
16 | // is successful, the TTL of the returned token will be less than or equal
17 | // to this value depending on application settings and the attributes
18 | // of the issuing key.
19 | // The default is 60 minutes (RSA9e, TK2a).
20 | TTL int64 `json:"ttl,omitempty" codec:"ttl,omitempty"`
21 |
22 | // Capability represents encoded channel access rights associated with this Ably Token.
23 | // The capabilities value is a JSON-encoded representation of the resource paths and associated operations.
24 | // Read more about capabilities in the [capabilities docs].
25 | // default '{"*":["*"]}' (RSA9f, TK2b)
26 | //
27 | // [capabilities docs]: https://ably.com/docs/core-features/authentication/#capabilities-explained
28 | Capability string `json:"capability,omitempty" codec:"capability,omitempty"`
29 |
30 | // ClientID is used for identifying this client when publishing messages or for presence purposes.
31 | // The clientId can be any non-empty string, except it cannot contain a *. This option is primarily intended
32 | // to be used in situations where the library is instantiated with a key.
33 | // Note that a clientId may also be implicit in a token used to instantiate the library.
34 | // An error is raised if a clientId specified here conflicts with the clientId implicit in the token.
35 | // Find out more about [identified clients] (TK2c).
36 | //
37 | // [identified clients]: https://ably.com/docs/core-features/authentication#identified-clients
38 | ClientID string `json:"clientId,omitempty" codec:"clientId,omitempty"`
39 |
40 | // Timestamp of the token request as milliseconds since the Unix epoch.
41 | // Timestamps, in conjunction with the nonce, are used to prevent requests from being replayed.
42 | // timestamp is a "one-time" value, and is valid in a request, but is not validly a member of
43 | // any default token params such as ClientOptions.defaultTokenParams (RSA9d, Tk2d).
44 | Timestamp int64 `json:"timestamp,omitempty" codec:"timestamp,omitempty"`
45 | }
46 |
47 | // Query encodes the params to query params value. If a field of params is
48 | // a zero-value, it's omitted. If params is zero-value, nil is returned.
49 | func (params *TokenParams) Query() url.Values {
50 | q := make(url.Values)
51 | if params == nil {
52 | return q
53 | }
54 | if params.TTL != 0 {
55 | q.Set("ttl", strconv.FormatInt(params.TTL, 10))
56 | }
57 | if params.Capability != "" {
58 | q.Set("capability", params.Capability)
59 | }
60 | if params.ClientID != "" {
61 | q.Set("clientId", params.ClientID)
62 | }
63 | if params.Timestamp != 0 {
64 | q.Set("timestamp", strconv.FormatInt(params.Timestamp, 10))
65 | }
66 | return q
67 | }
68 |
69 | // TokenRequest contains tokenparams with extra details, sent to ably for getting auth token
70 | type TokenRequest struct {
71 | TokenParams `codec:",inline"`
72 |
73 | // KeyName is the name of the key against which this request is made.
74 | // The key name is public, whereas the key secret is private (TE2).
75 | KeyName string `json:"keyName,omitempty" codec:"keyName,omitempty"`
76 |
77 | // Nonce is a cryptographically secure random string of at least 16 characters,
78 | // used to ensure the TokenRequest cannot be reused (TE2).
79 | Nonce string `json:"nonce,omitempty" codec:"nonce,omitempty"`
80 |
81 | // MAC is the Message Authentication Code for this request.
82 | MAC string `json:"mac,omitempty" codec:"mac,omitempty"`
83 | }
84 |
85 | func (TokenRequest) IsTokener() {}
86 | func (TokenRequest) isTokener() {}
87 |
88 | func (req *TokenRequest) sign(secret []byte) {
89 | mac := hmac.New(sha256.New, secret)
90 | fmt.Fprintln(mac, req.KeyName)
91 | fmt.Fprintln(mac, req.TTL)
92 | fmt.Fprintln(mac, req.Capability)
93 | fmt.Fprintln(mac, req.ClientID)
94 | fmt.Fprintln(mac, req.Timestamp)
95 | fmt.Fprintln(mac, req.Nonce)
96 | req.MAC = base64.StdEncoding.EncodeToString(mac.Sum(nil))
97 | }
98 |
99 | // TokenDetails contains an Ably Token and its associated metadata.
100 | type TokenDetails struct {
101 |
102 | // Token is the ably Token itself (TD2).
103 | // A typical Ably Token string appears with the form xVLyHw.A-pwh7wicf3afTfgiw4k2Ku33kcnSA7z6y8FjuYpe3QaNRTEo4.
104 | Token string `json:"token,omitempty" codec:"token,omitempty"`
105 |
106 | // KeyName is a string part of ABLY_KEY before :
107 | KeyName string `json:"keyName,omitempty" codec:"keyName,omitempty"`
108 |
109 | // Expires is the timestamp at which this token expires as milliseconds since the Unix epoch (TD3).
110 | Expires int64 `json:"expires,omitempty" codec:"expires,omitempty"`
111 |
112 | // ClientID, if any, bound to this Ably Token. If a client ID is included, then the Ably Token authenticates
113 | // its bearer as that client ID, and the Ably Token may only be used to perform operations on behalf
114 | // of that client ID. The client is then considered to be an identified client (TD6).
115 | ClientID string `json:"clientId,omitempty" codec:"clientId,omitempty"`
116 |
117 | // Issued is the timestamp at which this token was issued as milliseconds since the Unix epoch.
118 | Issued int64 `json:"issued,omitempty" codec:"issued,omitempty"`
119 |
120 | // Capability is the capabilities associated with this Ably Token.
121 | // The capabilities value is a JSON-encoded representation of the resource paths and associated operations.
122 | // Read more about capabilities in the [capabilities docs] (TD5).
123 | //
124 | // [capabilities docs]: https://ably.com/docs/core-features/authentication/#capabilities-explained
125 | Capability string `json:"capability,omitempty" codec:"capability,omitempty"`
126 | }
127 |
128 | func (TokenDetails) IsTokener() {}
129 | func (TokenDetails) isTokener() {}
130 |
131 | func (tok *TokenDetails) expired(now time.Time) bool {
132 | return tok.Expires != 0 && tok.Expires <= unixMilli(now)
133 | }
134 |
135 | func (tok *TokenDetails) IssueTime() time.Time {
136 | return time.Unix(tok.Issued/1000, tok.Issued%1000*int64(time.Millisecond))
137 | }
138 |
139 | func (tok *TokenDetails) ExpireTime() time.Time {
140 | return time.Unix(tok.Expires/1000, tok.Expires%1000*int64(time.Millisecond))
141 | }
142 |
143 | func newTokenDetails(token string) *TokenDetails {
144 | return &TokenDetails{
145 | Token: token,
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/ably/websocket.go:
--------------------------------------------------------------------------------
1 | package ably
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "net/http"
8 | "net/url"
9 | "time"
10 |
11 | "github.com/ably/ably-go/ably/internal/ablyutil"
12 | "nhooyr.io/websocket"
13 | )
14 |
15 | type proto int
16 |
17 | const (
18 | jsonProto proto = iota
19 | msgpackProto
20 | )
21 |
22 | type websocketConn struct {
23 | conn *websocket.Conn
24 | proto proto
25 | }
26 |
27 | type websocketErr struct {
28 | err error
29 | resp *http.Response
30 | }
31 |
32 | // websocketErr implements the builtin error interface.
33 | func (e *websocketErr) Error() string {
34 | return e.err.Error()
35 | }
36 |
37 | // Unwrap implements the implicit interface that errors.Unwrap understands.
38 | func (e *websocketErr) Unwrap() error {
39 | return e.err
40 | }
41 |
42 | func (ws *websocketConn) Send(msg *protocolMessage) error {
43 | switch ws.proto {
44 | case jsonProto:
45 | p, err := json.Marshal(msg)
46 | if err != nil {
47 | return err
48 | }
49 | return ws.conn.Write(context.Background(), websocket.MessageText, p)
50 | case msgpackProto:
51 | p, err := ablyutil.MarshalMsgpack(msg)
52 | if err != nil {
53 | return err
54 | }
55 | return ws.conn.Write(context.Background(), websocket.MessageBinary, p)
56 | }
57 | return nil
58 | }
59 |
60 | func (ws *websocketConn) Receive(deadline time.Time) (*protocolMessage, error) {
61 | msg := &protocolMessage{}
62 | var (
63 | ctx context.Context
64 | cancel context.CancelFunc
65 | )
66 | if deadline.IsZero() {
67 | ctx = context.Background()
68 | } else {
69 | ctx, cancel = context.WithDeadline(context.Background(), deadline)
70 | defer cancel()
71 | }
72 | _, data, err := ws.conn.Read(ctx)
73 | if err != nil {
74 | return nil, err
75 | }
76 | switch ws.proto {
77 | case jsonProto:
78 | err := json.Unmarshal(data, msg)
79 | if err != nil {
80 | return nil, err
81 | }
82 | case msgpackProto:
83 | err := ablyutil.UnmarshalMsgpack(data, msg)
84 | if err != nil {
85 | return nil, err
86 | }
87 | }
88 | return msg, nil
89 | }
90 |
91 | func (ws *websocketConn) Close() error {
92 | return ws.conn.Close(websocket.StatusNormalClosure, "")
93 | }
94 |
95 | func dialWebsocket(proto string, u *url.URL, timeout time.Duration, agents map[string]string) (*websocketConn, error) {
96 | ws := &websocketConn{}
97 | switch proto {
98 | case "application/json":
99 | ws.proto = jsonProto
100 | case "application/x-msgpack":
101 | ws.proto = msgpackProto
102 | default:
103 | return nil, errors.New(`invalid protocol "` + proto + `"`)
104 | }
105 | // Starts a raw websocket connection with server
106 | conn, resp, err := dialWebsocketTimeout(u.String(), "https://"+u.Host, timeout, agents)
107 | if err != nil {
108 | return nil, &websocketErr{err: err, resp: resp}
109 | }
110 | ws.conn = conn
111 | return ws, nil
112 | }
113 |
114 | // dialWebsocketTimeout dials the websocket with a timeout.
115 | func dialWebsocketTimeout(uri, origin string, timeout time.Duration, agents map[string]string) (*websocket.Conn, *http.Response, error) {
116 | ctx, cancel := context.WithTimeout(context.Background(), timeout)
117 | defer cancel()
118 |
119 | var ops websocket.DialOptions
120 | ops.HTTPHeader = make(http.Header)
121 | ops.HTTPHeader.Add(ablyAgentHeader, ablyAgentIdentifier(agents))
122 |
123 | c, resp, err := websocket.Dial(ctx, uri, &ops)
124 |
125 | if err != nil {
126 | return nil, resp, err
127 | }
128 |
129 | return c, resp, nil
130 | }
131 |
132 | func unwrapConn(c conn) conn {
133 | u, ok := c.(interface {
134 | Unwrap() conn
135 | })
136 | if !ok {
137 | return c
138 | }
139 | return unwrapConn(u.Unwrap())
140 | }
141 |
142 | func extractHttpResponseFromError(err error) *http.Response {
143 | wsErr, ok := err.(*websocketErr)
144 | if ok {
145 | return wsErr.resp
146 | }
147 | return nil
148 | }
149 |
150 | func setConnectionReadLimit(c conn, readLimit int64) error {
151 | unwrappedConn := unwrapConn(c)
152 | websocketConn, ok := unwrappedConn.(*websocketConn)
153 | if !ok {
154 | return errors.New("cannot set readlimit for connection, connection does not use nhooyr.io/websocket")
155 | }
156 | websocketConn.conn.SetReadLimit(readLimit)
157 | return nil
158 | }
159 |
--------------------------------------------------------------------------------
/ably/websocket_internal_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ably
5 |
6 | import (
7 | "bytes"
8 | "context"
9 | "encoding/json"
10 | "errors"
11 | "fmt"
12 | "log"
13 | "net/http"
14 | "net/http/httptest"
15 | "net/url"
16 | "strings"
17 | "testing"
18 | "time"
19 |
20 | "github.com/stretchr/testify/assert"
21 | "github.com/ugorji/go/codec"
22 | "nhooyr.io/websocket"
23 | )
24 |
25 | var timeout = time.Second * 30
26 |
27 | // handleWebsocketHandshake is a handler that can handle a websocket handshake and connection from a client.
28 | func handleWebsocketHandshake(w http.ResponseWriter, r *http.Request) {
29 | conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{})
30 | if err != nil {
31 | log.Printf("test server: error accepting websocket handshake: %+v\n", err)
32 | }
33 | defer conn.Close(websocket.StatusNormalClosure, "")
34 | }
35 |
36 | // handleWebsocketMessages is a handler that receives a protocol message over a websocket connection then
37 | // responds by sending a protocol message back to the client including the message type and original message.
38 | func handleWebsocketMessage(w http.ResponseWriter, r *http.Request) {
39 | conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{})
40 | if err != nil {
41 | log.Printf("test server: error accepting websocket handshake: %+v\n", err)
42 | }
43 | defer conn.Close(websocket.StatusInternalError, "connection closed")
44 |
45 | ctx, cancel := context.WithTimeout(r.Context(), time.Second*10)
46 | defer cancel()
47 |
48 | // Read message received from client.
49 | msgType, received, err := conn.Read(ctx)
50 | if err != nil {
51 | log.Printf("test server: error reading received message: %+v\n", err)
52 | }
53 |
54 | // Reply to message with the type of message received and the message string.
55 | reply := protocolMessage{
56 | Action: actionMessage,
57 | Messages: []*Message{{Name: msgType.String(), Data: string(received)}},
58 | }
59 |
60 | var payloadType websocket.MessageType
61 | var payload []byte
62 |
63 | switch msgType.String() {
64 | case "MessageText":
65 | payloadType = websocket.MessageText
66 |
67 | payload, err = json.Marshal(reply)
68 | if err != nil {
69 | log.Printf("test server: error marshalling json: %+v\n", err)
70 | }
71 |
72 | case "MessageBinary":
73 | payloadType = websocket.MessageBinary
74 |
75 | var handle codec.MsgpackHandle
76 | var buf bytes.Buffer
77 | enc := codec.NewEncoder(&buf, &handle)
78 | if err := enc.Encode(reply); err != nil {
79 | log.Printf("test server: error encoding msg: %+v\n", err)
80 | }
81 | payload = buf.Bytes()
82 | }
83 |
84 | if err := conn.Write(ctx, payloadType, payload); err != nil {
85 | log.Println("test server: error sending message\n", err)
86 | }
87 | }
88 |
89 | func TestWebsocketDial(t *testing.T) {
90 | // Create test server with a handler that will accept a websocket handshake.
91 | ts := httptest.NewServer(http.HandlerFunc(handleWebsocketHandshake))
92 | defer ts.Close()
93 |
94 | // Convert http:// to ws:// and parse the test server url
95 | websocketUrl := fmt.Sprintf("ws%s", strings.TrimPrefix(ts.URL, "http"))
96 | testServerURL, _ := url.Parse(websocketUrl)
97 |
98 | tests := map[string]struct {
99 | dialProtocol string
100 | expectedErr error
101 | expectedProto proto
102 | }{
103 | "Can dial for protocol application/json": {
104 | dialProtocol: "application/json",
105 | expectedErr: nil,
106 | expectedProto: jsonProto,
107 | },
108 | "Can dial for protocol application/x-msgpack": {
109 | dialProtocol: "application/x-msgpack",
110 | expectedErr: nil,
111 | expectedProto: msgpackProto,
112 | },
113 | "Can handle an error when dialing for an invalid protocol": {
114 | dialProtocol: "aProtocol",
115 | expectedErr: errors.New(`invalid protocol "aProtocol"`),
116 | },
117 | }
118 |
119 | for testName, test := range tests {
120 | t.Run(testName, func(t *testing.T) {
121 |
122 | result, err := dialWebsocket(test.dialProtocol, testServerURL, timeout, nil)
123 | assert.Equal(t, test.expectedErr, err)
124 |
125 | if result != nil {
126 | assert.NotNil(t, result.conn)
127 | assert.Equal(t, test.expectedProto, result.proto)
128 | }
129 | })
130 | }
131 | }
132 |
133 | func TestWebsocketSendAndReceive(t *testing.T) {
134 | // Create test server with a handler that can receive a message.
135 | ts := httptest.NewServer(http.HandlerFunc(handleWebsocketMessage))
136 | defer ts.Close()
137 |
138 | // Convert http:// to ws:// and parse the test server url
139 | websocketUrl := fmt.Sprintf("ws%s", strings.TrimPrefix(ts.URL, "http"))
140 | testServerURL, _ := url.Parse(websocketUrl)
141 |
142 | tests := map[string]struct {
143 | dialProtocol string
144 | expectedMessageType string
145 | expectedErr error
146 | }{
147 | "Can send and receive a message using protocol application/json": {
148 | dialProtocol: "application/json",
149 | expectedMessageType: "MessageText",
150 | expectedErr: nil,
151 | },
152 | "Can send and receive a message using protocol application/x-msgpack": {
153 | dialProtocol: "application/x-msgpack",
154 | expectedMessageType: "MessageBinary",
155 | expectedErr: nil,
156 | },
157 | }
158 |
159 | for testName, test := range tests {
160 | t.Run(testName, func(t *testing.T) {
161 |
162 | ws, err := dialWebsocket(test.dialProtocol, testServerURL, timeout, nil)
163 | assert.NoError(t, err)
164 | msg := protocolMessage{
165 | Messages: []*Message{{Name: "temperature", Data: "22.7"}},
166 | }
167 |
168 | ws.Send(&msg)
169 | result, err := ws.Receive(time.Now().Add(timeout))
170 |
171 | assert.Equal(t, test.expectedErr, err)
172 | assert.Equal(t, test.expectedMessageType, result.Messages[0].Name)
173 | assert.Equal(t, 1, len(result.Messages))
174 | assert.Equal(t, actionMessage, result.Action)
175 | assert.Contains(t, result.Messages[0].Data, "temperature")
176 | assert.Contains(t, result.Messages[0].Data, "22.7")
177 | })
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/ablytest/ablytest.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "net/http"
8 | "os"
9 | "reflect"
10 | "strconv"
11 | "sync"
12 | "time"
13 |
14 | "github.com/ably/ably-go/ably"
15 | )
16 |
17 | var Timeout = 30 * time.Second
18 | var NoBinaryProtocol bool
19 | var DefaultLogLevel = ably.LogNone
20 | var Endpoint = "nonprod:sandbox"
21 |
22 | func nonil(err ...error) error {
23 | for _, err := range err {
24 | if err != nil {
25 | return err
26 | }
27 | }
28 | return nil
29 | }
30 |
31 | func init() {
32 | if s := os.Getenv("ABLY_TIMEOUT"); s != "" {
33 | if t, err := time.ParseDuration(s); err == nil {
34 | Timeout = t
35 | }
36 | }
37 | if os.Getenv("ABLY_PROTOCOL") == "application/json" {
38 | NoBinaryProtocol = true
39 | }
40 | if n, err := strconv.Atoi(os.Getenv("ABLY_LOGLEVEL")); err == nil {
41 | DefaultLogLevel = ably.LogLevel(n)
42 | }
43 | if s := os.Getenv("ABLY_ENDPOINT"); s != "" {
44 | Endpoint = s
45 | }
46 | }
47 |
48 | func MergeOptions(opts ...[]ably.ClientOption) []ably.ClientOption {
49 | var merged []ably.ClientOption
50 | for _, opt := range opts {
51 | merged = append(merged, opt...)
52 | }
53 | return merged
54 | }
55 |
56 | func encode(typ string, in interface{}) ([]byte, error) {
57 | switch typ {
58 | case "application/json":
59 | return json.Marshal(in)
60 | case "application/x-msgpack":
61 | return marshalMsgpack(in)
62 | case "text/plain":
63 | return []byte(fmt.Sprintf("%v", in)), nil
64 | default:
65 | return nil, fmt.Errorf("encoding error: unrecognized Content-Type: %q", typ)
66 | }
67 | }
68 |
69 | var ClientOptionsInspector struct {
70 | UseBinaryProtocol func([]ably.ClientOption) bool
71 | HTTPClient func([]ably.ClientOption) *http.Client
72 | }
73 |
74 | func protocol(opts []ably.ClientOption) string {
75 | if ClientOptionsInspector.UseBinaryProtocol(opts) {
76 | return "application/x-msgpack"
77 | }
78 | return "application/json"
79 | }
80 |
81 | func Contains(s, sub interface{}) bool {
82 | return reflectContains(reflect.ValueOf(s), reflect.ValueOf(sub))
83 | }
84 |
85 | func reflectContains(s, sub reflect.Value) bool {
86 | for i := 0; i+sub.Len() <= s.Len(); i++ {
87 | if reflect.DeepEqual(
88 | s.Slice(i, i+sub.Len()).Interface(),
89 | sub.Interface(),
90 | ) {
91 | return true
92 | }
93 | }
94 | return false
95 | }
96 |
97 | type MessageChannel chan *ably.Message
98 |
99 | func (ch MessageChannel) Receive(m *ably.Message) {
100 | ch <- m
101 | }
102 |
103 | func ReceiveMessages(channel *ably.RealtimeChannel, name string) (messages <-chan *ably.Message, unsubscribe func(), err error) {
104 | ch := make(MessageChannel, 100)
105 | if name == "" {
106 | unsubscribe, err = channel.SubscribeAll(context.Background(), ch.Receive)
107 | } else {
108 | unsubscribe, err = channel.Subscribe(context.Background(), name, ch.Receive)
109 | }
110 | return ch, unsubscribe, err
111 | }
112 |
113 | type PresenceChannel chan *ably.PresenceMessage
114 |
115 | func (ch PresenceChannel) Receive(m *ably.PresenceMessage) {
116 | ch <- m
117 | }
118 |
119 | func ReceivePresenceMessages(channel *ably.RealtimeChannel, action *ably.PresenceAction) (messages <-chan *ably.PresenceMessage, unsubscribe func(), err error) {
120 | ch := make(PresenceChannel, 100)
121 | if action == nil {
122 | unsubscribe, err = channel.Presence.SubscribeAll(context.Background(), ch.Receive)
123 | } else {
124 | unsubscribe, err = channel.Presence.Subscribe(context.Background(), *action, ch.Receive)
125 | }
126 | return ch, unsubscribe, err
127 | }
128 |
129 | type AfterCall struct {
130 | Ctx context.Context
131 | D time.Duration
132 | Deadline time.Time
133 | Time chan<- time.Time
134 | triggered *bool
135 | mtx *sync.Mutex
136 | }
137 |
138 | func (c AfterCall) Fire() {
139 | c.Time <- c.Deadline
140 | }
141 |
142 | func (a AfterCall) setTriggered(value bool) {
143 | a.mtx.Lock()
144 | defer a.mtx.Unlock()
145 | *a.triggered = value
146 | }
147 |
148 | func (a AfterCall) IsTriggered() bool {
149 | a.mtx.Lock()
150 | defer a.mtx.Unlock()
151 | return *a.triggered
152 | }
153 |
154 | // TimeFuncs returns time functions to be passed as options.
155 | //
156 | // Now returns a stable time that is only updated with the times that the
157 | // returned After produces.
158 | //
159 | // After forwards calls to the given channel. The receiver is in charge of
160 | // sending the resulting time to the AfterCall.Time channel.
161 | func TimeFuncs(afterCalls chan<- AfterCall) (
162 | now func() time.Time,
163 | after func(context.Context, time.Duration) <-chan time.Time,
164 | ) {
165 | var mtx sync.Mutex
166 | currentTime := time.Now()
167 | now = func() time.Time {
168 | mtx.Lock()
169 | defer mtx.Unlock()
170 | return currentTime
171 | }
172 |
173 | after = func(ctx context.Context, d time.Duration) <-chan time.Time {
174 | ch := make(chan time.Time, 1)
175 |
176 | timeUpdate := make(chan time.Time, 1)
177 | go func() {
178 | mtx.Lock()
179 | t := currentTime
180 | mtx.Unlock()
181 | afterCall := AfterCall{Ctx: ctx, D: d, Deadline: t.Add(d), Time: timeUpdate, triggered: new(bool), mtx: &sync.Mutex{}}
182 | select {
183 | case afterCalls <- afterCall:
184 | case <-ctx.Done():
185 | // This allows tests to ignore a call if they expect the timer to
186 | // be cancelled.
187 | return
188 | }
189 |
190 | select {
191 | case <-ctx.Done():
192 | close(ch)
193 |
194 | case t, ok := <-timeUpdate:
195 | if !ok {
196 | close(ch)
197 | return
198 | }
199 | mtx.Lock()
200 | currentTime = t
201 | mtx.Unlock()
202 | ch <- t
203 | afterCall.setTriggered(true)
204 | }
205 | }()
206 |
207 | return ch
208 | }
209 |
210 | return now, after
211 | }
212 |
--------------------------------------------------------------------------------
/ablytest/cryptodata.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "encoding/base64"
5 | "encoding/json"
6 | "errors"
7 | "os"
8 | "path/filepath"
9 | )
10 |
11 | type CryptoData struct {
12 | Algorithm string `json:"algorithm"`
13 | Mode string `json:"mode"`
14 | KeyLen int `json:"keylength"`
15 | Key string `json:"key"`
16 | IV string `json:"iv"`
17 | Items []struct {
18 | Encoded json.RawMessage `json:"encoded"`
19 | Encrypted json.RawMessage `json:"encrypted"`
20 | } `json:"items"`
21 | }
22 |
23 | func LoadCryptoData(rel string) (*CryptoData, []byte, []byte, error) {
24 | data := &CryptoData{}
25 | f, err := os.Open(filepath.Join("..", "common", filepath.FromSlash(rel)))
26 | if err != nil {
27 | return nil, nil, nil, errors.New("missing common subrepo - ensure git submodules are initialized")
28 | }
29 | err = json.NewDecoder(f).Decode(data)
30 | f.Close()
31 | if err != nil {
32 | return nil, nil, nil, errors.New("unable to unmarshal test cases: " + err.Error())
33 | }
34 | key, err := base64.StdEncoding.DecodeString(data.Key)
35 | if err != nil {
36 | return nil, nil, nil, errors.New("unable to unbase64 key" + err.Error())
37 | }
38 | iv, err := base64.StdEncoding.DecodeString(data.IV)
39 | if err != nil {
40 | return nil, nil, nil, errors.New("unable to unbase64 IV" + err.Error())
41 | }
42 | return data, key, iv, nil
43 | }
44 |
--------------------------------------------------------------------------------
/ablytest/fmt.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | // FmtFunc is a non-failing function analogous to fmt.Printf.
9 | type FmtFunc func(format string, args ...interface{})
10 |
11 | // Wrap wraps a FmtFunc in another. The wrapper FmtFunc first uses the fixed
12 | // format and args, plus the format it's called with, to create a new format
13 | // string by calling fmt.Sprintf. Then, it calls the wrapped FmtFunc with this
14 | // format string and the args the wrapper is called with.
15 | //
16 | // It's useful to have some fixed context on everything that is formatted with
17 | // a FmtFunc, e.g. test scope context.
18 | func (f FmtFunc) Wrap(t *testing.T, format string, args ...interface{}) FmtFunc {
19 | return func(wrappedFormat string, wrappedArgs ...interface{}) {
20 | if t != nil {
21 | t.Helper()
22 | }
23 | f(fmt.Sprintf(format, append(args, wrappedFormat)...), wrappedArgs...)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ablytest/fmt_test.go:
--------------------------------------------------------------------------------
1 | //go:build !integration
2 | // +build !integration
3 |
4 | package ablytest_test
5 |
6 | import (
7 | "fmt"
8 |
9 | "github.com/ably/ably-go/ablytest"
10 | )
11 |
12 | func ExampleFmtFunc_Wrap() {
13 | myPrintln := ablytest.FmtFunc(func(format string, args ...interface{}) {
14 | fmt.Println(fmt.Sprintf(format, args...))
15 | })
16 |
17 | id := 42
18 | wrapped := myPrintln.Wrap(nil, "for ID %d: %s", id)
19 |
20 | wrapped("everything's OK")
21 |
22 | errMsg := "all hell broke loose"
23 | wrapped("something failed: %s", errMsg)
24 |
25 | // Output:
26 | // for ID 42: everything's OK
27 | // for ID 42: something failed: all hell broke loose
28 | }
29 |
--------------------------------------------------------------------------------
/ablytest/logger.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ably/ably-go/ably"
7 | )
8 |
9 | type LogMessage struct {
10 | Level ably.LogLevel
11 | Message string
12 | }
13 |
14 | func NewLogger(messages chan<- LogMessage) ably.Logger {
15 | return testLogger{messages: messages}
16 | }
17 |
18 | type testLogger struct {
19 | messages chan<- LogMessage
20 | }
21 |
22 | func (l testLogger) Printf(level ably.LogLevel, format string, v ...interface{}) {
23 | l.messages <- LogMessage{
24 | Level: level,
25 | Message: fmt.Sprintf(format, v...),
26 | }
27 | }
28 |
29 | var DiscardLogger ably.Logger = discardLogger{}
30 |
31 | type discardLogger struct{}
32 |
33 | func (discardLogger) Printf(level ably.LogLevel, format string, v ...interface{}) {}
34 |
--------------------------------------------------------------------------------
/ablytest/msgpack.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "bytes"
5 | "io"
6 |
7 | "github.com/ugorji/go/codec"
8 | )
9 |
10 | var handle codec.MsgpackHandle
11 |
12 | func init() {
13 | handle.Raw = true
14 | handle.WriteExt = true
15 | handle.RawToString = true
16 | }
17 |
18 | // marshalMsgpack returns msgpack encoding of v
19 | func marshalMsgpack(v interface{}) ([]byte, error) {
20 | var buf bytes.Buffer
21 | err := encodeMsg(&buf, v)
22 | if err != nil {
23 | return nil, err
24 | }
25 | return buf.Bytes(), nil
26 | }
27 |
28 | // encodeMsg encodes v into msgpack format and writes the output to w.
29 | func encodeMsg(w io.Writer, v interface{}) error {
30 | enc := codec.NewEncoder(w, &handle)
31 | return enc.Encode(v)
32 | }
33 |
--------------------------------------------------------------------------------
/ablytest/proxies.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "io"
8 | "net"
9 | "net/http"
10 | "net/url"
11 | "strconv"
12 |
13 | "github.com/ably/ably-go/ably"
14 | )
15 |
16 | var hopHeaders = map[string]struct{}{
17 | "Connection": {},
18 | "Keep-Alive": {},
19 | "Proxy-Authenticate": {},
20 | "Proxy-Authorization": {},
21 | "Te": {},
22 | "Trailers": {},
23 | "Transfer-Encoding": {},
24 | "Upgrade": {},
25 | }
26 |
27 | func NewTokenParams(query url.Values) *ably.TokenParams {
28 | params := &ably.TokenParams{}
29 | if n, err := strconv.ParseInt(query.Get("ttl"), 10, 64); err == nil {
30 | params.TTL = n
31 | }
32 | if s := query.Get("capability"); s != "" {
33 | params.Capability = s
34 | }
35 | if s := query.Get("clientId"); s != "" {
36 | params.ClientID = s
37 | }
38 | if n, err := strconv.ParseInt(query.Get("timestamp"), 10, 64); err == nil {
39 | params.Timestamp = n
40 | }
41 | return params
42 | }
43 |
44 | func Query(req *http.Request) (url.Values, error) {
45 | switch req.Method {
46 | case "GET":
47 | return req.URL.Query(), nil
48 | case "POST":
49 | p, err := io.ReadAll(req.Body)
50 | if err != nil {
51 | return nil, err
52 | }
53 | return url.ParseQuery(string(p))
54 | default:
55 | return nil, errors.New(http.StatusText(405))
56 | }
57 | }
58 |
59 | func MustQuery(req *http.Request) url.Values {
60 | query, err := Query(req)
61 | if err != nil {
62 | panic(err)
63 | }
64 | return query
65 | }
66 |
67 | // AuthReverseProxy serves token requests by reverse proxying them to
68 | // the Ably servers. Use URL method for creating values for AuthURL
69 | // option and Callback method - for AuthCallback ones.
70 | type AuthReverseProxy struct {
71 | TokenQueue []*ably.TokenDetails // when non-nil pops the token from the queue instead querying Ably servers
72 | Listener net.Listener // listener which accepts token request connections
73 |
74 | auth *ably.Auth
75 | proto string
76 | }
77 |
78 | // NewAuthReverseProxy creates new auth reverse proxy. The given opts
79 | // are used to create a Auth client, used to reverse proxying token requests.
80 | func NewAuthReverseProxy(opts ...ably.ClientOption) (*AuthReverseProxy, error) {
81 | client, err := ably.NewREST(append(opts,
82 | ably.WithUseTokenAuth(true),
83 | )...)
84 | if err != nil {
85 | return nil, err
86 | }
87 | lis, err := net.Listen("tcp4", ":0")
88 | if err != nil {
89 | return nil, err
90 | }
91 | srv := &AuthReverseProxy{
92 | Listener: lis,
93 | auth: client.Auth,
94 | proto: protocol(opts),
95 | }
96 | go http.Serve(lis, srv)
97 | return srv, nil
98 | }
99 |
100 | // MustAuthReverseProxy panics when creating the proxy fails.
101 | func MustAuthReverseProxy(opts ...ably.ClientOption) *AuthReverseProxy {
102 | srv, err := NewAuthReverseProxy(opts...)
103 | if err != nil {
104 | panic(err)
105 | }
106 | return srv
107 | }
108 |
109 | // URL gives new AuthURL for the requested responseType. Available response
110 | // types are:
111 | //
112 | // - "token", which responds with (ably.TokenDetails).Token as a string
113 | // - "details", which responds with ably.TokenDetails
114 | // - "request", which responds with ably.TokenRequest
115 | func (srv *AuthReverseProxy) URL(responseType string) string {
116 | return "http://" + srv.Listener.Addr().String() + "/" + responseType
117 | }
118 |
119 | // Callback gives new AuthCallback. Available response types are the same
120 | // as for URL method.
121 | func (srv *AuthReverseProxy) Callback(responseType string) func(context.Context, ably.TokenParams) (ably.Tokener, error) {
122 | return func(ctx context.Context, params ably.TokenParams) (ably.Tokener, error) {
123 | token, _, err := srv.handleAuth(ctx, responseType, params)
124 | return token, err
125 | }
126 | }
127 |
128 | // Close makes the proxy server stop accepting connections.
129 | func (srv *AuthReverseProxy) Close() error {
130 | return srv.Listener.Close()
131 | }
132 |
133 | // ServeHTTP implements the http.Handler interface.
134 | func (srv *AuthReverseProxy) ServeHTTP(w http.ResponseWriter, req *http.Request) {
135 | query, err := Query(req)
136 | if err != nil {
137 | http.Error(w, err.Error(), 400)
138 | return
139 | }
140 | token, contentType, err := srv.handleAuth(req.Context(), req.URL.Path[1:], *NewTokenParams(query))
141 | if err != nil {
142 | http.Error(w, err.Error(), 400)
143 | return
144 | }
145 | p, err := encode(contentType, token)
146 | if err != nil {
147 | http.Error(w, err.Error(), 400)
148 | return
149 | }
150 | for k, v := range req.Header {
151 | if _, ok := hopHeaders[k]; !ok {
152 | w.Header()[k] = v
153 | }
154 | }
155 | w.Header().Set("Content-Type", contentType)
156 | w.Header().Set("Content-Length", strconv.Itoa(len(p)))
157 | w.WriteHeader(200)
158 | if _, err = io.Copy(w, bytes.NewReader(p)); err != nil {
159 | panic(err)
160 | }
161 | }
162 |
163 | func (srv *AuthReverseProxy) handleAuth(ctx context.Context, responseType string, params ably.TokenParams) (token ably.Tokener, typ string, err error) {
164 | switch responseType {
165 | case "token", "details":
166 | var tok *ably.TokenDetails
167 | if len(srv.TokenQueue) != 0 {
168 | tok, srv.TokenQueue = srv.TokenQueue[0], srv.TokenQueue[1:]
169 | } else {
170 | tok, err = srv.auth.Authorize(ctx, ¶ms)
171 | if err != nil {
172 | return nil, "", err
173 | }
174 | }
175 | if responseType == "token" {
176 | return ably.TokenString(tok.Token), "text/plain", nil
177 | }
178 | return tok, srv.proto, nil
179 | case "request":
180 | tokReq, err := srv.auth.CreateTokenRequest(¶ms)
181 | if err != nil {
182 | return nil, "", err
183 | }
184 | return tokReq, srv.proto, nil
185 | default:
186 | return nil, "", errors.New("unexpected token value type: " + typ)
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/ablytest/recorders.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "io"
7 | "net/http"
8 | "sync"
9 | "sync/atomic"
10 |
11 | "github.com/ably/ably-go/ably"
12 | )
13 |
14 | // RoundTripRecorder is a http.Transport wrapper which records
15 | // HTTP request/response pairs.
16 | type RoundTripRecorder struct {
17 | *http.Transport
18 |
19 | mtx sync.Mutex
20 | reqs []*http.Request
21 | resps []*http.Response
22 | stopped int32
23 | }
24 |
25 | func NewHttpRecorder() (*RoundTripRecorder, []ably.ClientOption) {
26 | rec := &RoundTripRecorder{}
27 | httpClient := &http.Client{Transport: &http.Transport{}}
28 | httpClient.Transport = rec.Hijack(httpClient.Transport)
29 | return rec, []ably.ClientOption{ably.WithHTTPClient(httpClient)}
30 | }
31 |
32 | var _ http.RoundTripper = (*RoundTripRecorder)(nil)
33 |
34 | // Len gives number of recorded request/response pairs.
35 | //
36 | // It is save to call Len() before calling Stop().
37 | func (rec *RoundTripRecorder) Len() int {
38 | rec.mtx.Lock()
39 | defer rec.mtx.Unlock()
40 | return len(rec.reqs)
41 | }
42 |
43 | // Request gives nth recorded http.Request.
44 | func (rec *RoundTripRecorder) Request(n int) *http.Request {
45 | rec.mtx.Lock()
46 | defer rec.mtx.Unlock()
47 | return rec.reqs[n]
48 | }
49 |
50 | // Response gives nth recorded http.Response.
51 | func (rec *RoundTripRecorder) Response(n int) *http.Response {
52 | rec.mtx.Lock()
53 | defer rec.mtx.Unlock()
54 | return rec.resps[n]
55 | }
56 |
57 | // Requests gives all HTTP requests in order they were recorded.
58 | func (rec *RoundTripRecorder) Requests() []*http.Request {
59 | rec.mtx.Lock()
60 | defer rec.mtx.Unlock()
61 | reqs := make([]*http.Request, len(rec.reqs))
62 | copy(reqs, rec.reqs)
63 | return reqs
64 | }
65 |
66 | // Responses gives all HTTP responses in order they were recorded.
67 | func (rec *RoundTripRecorder) Responses() []*http.Response {
68 | rec.mtx.Lock()
69 | defer rec.mtx.Unlock()
70 | resps := make([]*http.Response, len(rec.resps))
71 | copy(resps, rec.resps)
72 | return resps
73 | }
74 |
75 | // RoundTrip implements the http.RoundTripper interface.
76 | func (rec *RoundTripRecorder) RoundTrip(req *http.Request) (*http.Response, error) {
77 | if atomic.LoadInt32(&rec.stopped) == 0 {
78 | return rec.roundTrip(req)
79 | }
80 | return rec.Transport.RoundTrip(req)
81 | }
82 |
83 | // Stop makes the recorder stop recording new requests/responses.
84 | func (rec *RoundTripRecorder) Stop() {
85 | atomic.StoreInt32(&rec.stopped, 1)
86 | }
87 |
88 | // Hijack injects http.Transport into the wrapper.
89 | func (rec *RoundTripRecorder) Hijack(rt http.RoundTripper) http.RoundTripper {
90 | if tr, ok := rt.(*http.Transport); ok {
91 | rec.Transport = tr
92 | }
93 | return rec
94 | }
95 |
96 | // Reset resets the recorder requests and responses.
97 | func (rec *RoundTripRecorder) Reset() {
98 | rec.mtx.Lock()
99 | rec.reqs = nil
100 | rec.resps = nil
101 | rec.mtx.Unlock()
102 | }
103 |
104 | func (rec *RoundTripRecorder) roundTrip(req *http.Request) (*http.Response, error) {
105 | var buf bytes.Buffer
106 | if req.Body != nil {
107 | req.Body = io.NopCloser(io.TeeReader(req.Body, &buf))
108 | }
109 | resp, err := rec.Transport.RoundTrip(req)
110 | req.Body = body(buf.Bytes())
111 | buf.Reset()
112 | if resp != nil && resp.Body != nil {
113 | _, e := io.Copy(&buf, resp.Body)
114 | err = nonil(err, e, resp.Body.Close())
115 | resp.Body = body(buf.Bytes())
116 | }
117 | rec.mtx.Lock()
118 | respCopy := *resp
119 | respCopy.Body = body(buf.Bytes())
120 | rec.reqs = append(rec.reqs, req)
121 | rec.resps = append(rec.resps, &respCopy)
122 | rec.mtx.Unlock()
123 | return resp, err
124 | }
125 |
126 | type ConnStatesRecorder struct {
127 | mtx sync.Mutex
128 | states []ably.ConnectionState
129 | }
130 |
131 | func (cs *ConnStatesRecorder) append(state ably.ConnectionState) {
132 | cs.mtx.Lock()
133 | defer cs.mtx.Unlock()
134 | cs.states = append(cs.states, state)
135 | }
136 |
137 | func (cs *ConnStatesRecorder) Listen(r *ably.Realtime) (off func()) {
138 | cs.append(r.Connection.State())
139 | return r.Connection.OnAll(func(c ably.ConnectionStateChange) {
140 | cs.append(c.Current)
141 | })
142 | }
143 |
144 | func (cs *ConnStatesRecorder) States() []ably.ConnectionState {
145 | cs.mtx.Lock()
146 | defer cs.mtx.Unlock()
147 | return cs.states
148 | }
149 |
150 | type ChanStatesRecorder struct {
151 | mtx sync.Mutex
152 | states []ably.ChannelState
153 | }
154 |
155 | func (cs *ChanStatesRecorder) append(state ably.ChannelState) {
156 | cs.mtx.Lock()
157 | defer cs.mtx.Unlock()
158 | cs.states = append(cs.states, state)
159 | }
160 |
161 | func (cs *ChanStatesRecorder) Listen(channel *ably.RealtimeChannel) (off func()) {
162 | cs.append(channel.State())
163 | return channel.OnAll(func(c ably.ChannelStateChange) {
164 | cs.append(c.Current)
165 | })
166 | }
167 |
168 | func (cs *ChanStatesRecorder) States() []ably.ChannelState {
169 | cs.mtx.Lock()
170 | defer cs.mtx.Unlock()
171 | return cs.states
172 | }
173 |
174 | type ConnErrorsRecorder struct {
175 | mtx sync.Mutex
176 | errors []*ably.ErrorInfo
177 | }
178 |
179 | func (ce *ConnErrorsRecorder) appendNonNil(err *ably.ErrorInfo) {
180 | ce.mtx.Lock()
181 | defer ce.mtx.Unlock()
182 | if err != nil {
183 | ce.errors = append(ce.errors, err)
184 | }
185 | }
186 |
187 | func (ce *ConnErrorsRecorder) Listen(r *ably.Realtime) (off func()) {
188 | ce.appendNonNil(r.Connection.ErrorReason())
189 | return r.Connection.OnAll(func(c ably.ConnectionStateChange) {
190 | ce.appendNonNil(c.Reason)
191 | })
192 | }
193 |
194 | func (ce *ConnErrorsRecorder) Errors() []*ably.ErrorInfo {
195 | ce.mtx.Lock()
196 | defer ce.mtx.Unlock()
197 | return ce.errors
198 | }
199 |
200 | // FullRealtimeCloser returns an io.Closer that, on Close, calls Close on the
201 | // Realtime instance and waits for its effects.
202 | func FullRealtimeCloser(c *ably.Realtime) io.Closer {
203 | return realtimeIOCloser{c: c}
204 | }
205 |
206 | type realtimeIOCloser struct {
207 | c *ably.Realtime
208 | }
209 |
210 | func (c realtimeIOCloser) Close() error {
211 | switch c.c.Connection.State() {
212 | case
213 | ably.ConnectionStateInitialized,
214 | ably.ConnectionStateClosed,
215 | ably.ConnectionStateFailed:
216 |
217 | return nil
218 | }
219 |
220 | ctx, cancel := context.WithTimeout(context.Background(), Timeout)
221 | defer cancel()
222 |
223 | errCh := make(chan error, 1)
224 |
225 | off := make(chan func(), 1)
226 | off <- c.c.Connection.OnAll(func(c ably.ConnectionStateChange) {
227 | switch c.Current {
228 | default:
229 | return
230 | case
231 | ably.ConnectionStateClosed,
232 | ably.ConnectionStateFailed:
233 | }
234 |
235 | (<-off)()
236 |
237 | var err error
238 | if c.Reason != nil {
239 | err = *c.Reason
240 | }
241 | errCh <- err
242 | })
243 |
244 | c.c.Close()
245 |
246 | select {
247 | case err := <-errCh:
248 | return err
249 | case <-ctx.Done():
250 | return ctx.Err()
251 | }
252 | }
253 |
254 | func body(p []byte) io.ReadCloser {
255 | return io.NopCloser(bytes.NewReader(p))
256 | }
257 |
--------------------------------------------------------------------------------
/ablytest/resultgroup.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "sync"
8 | "time"
9 |
10 | "github.com/ably/ably-go/ably"
11 | )
12 |
13 | type Result interface {
14 | Wait(context.Context) error
15 | }
16 |
17 | func Wait(res Result, err error) error {
18 | if err != nil {
19 | return err
20 | }
21 | errch := make(chan error)
22 | go func() {
23 | errch <- res.Wait(context.Background())
24 | }()
25 | select {
26 | case err := <-errch:
27 | return err
28 | case <-time.After(Timeout):
29 | return errors.New("waiting on Result timed out after " + Timeout.String())
30 | }
31 | }
32 |
33 | // ResultGroup is like sync.WaitGroup, but for Result values.
34 | //
35 | // ResultGroup blocks till last added Result has completed successfully.
36 | //
37 | // If at least Result value failed, ResultGroup returns first encountered
38 | // error immediately.
39 | type ResultGroup struct {
40 | mu sync.Mutex
41 | wg sync.WaitGroup
42 | err error
43 | errch chan error
44 | }
45 |
46 | func (rg *ResultGroup) check(err error) bool {
47 | rg.mu.Lock()
48 | defer rg.mu.Unlock()
49 | if rg.errch == nil {
50 | rg.errch = make(chan error, 1)
51 | }
52 | rg.err = nonil(rg.err, err)
53 | return rg.err == nil
54 | }
55 |
56 | func (rg *ResultGroup) Add(res Result, err error) {
57 | if !rg.check(err) {
58 | return
59 | }
60 | rg.wg.Add(1)
61 | go func() {
62 | err := res.Wait(context.Background())
63 | if err != nil && err != (*ably.ErrorInfo)(nil) {
64 | select {
65 | case rg.errch <- err:
66 | default:
67 | }
68 | }
69 | rg.wg.Done()
70 | }()
71 | }
72 |
73 | func (rg *ResultGroup) GoAdd(f ResultFunc) {
74 | rg.Add(f.Go(), nil)
75 | }
76 |
77 | func (rg *ResultGroup) Wait() error {
78 | if rg.err != nil {
79 | return rg.err
80 | }
81 | done := make(chan struct{})
82 | go func() {
83 | rg.wg.Wait()
84 | done <- struct{}{}
85 | }()
86 | select {
87 | case <-done:
88 | return nil
89 | case err := <-rg.errch:
90 | return err
91 | }
92 | }
93 |
94 | type resultFunc func(context.Context) error
95 |
96 | func (f resultFunc) Wait(ctx context.Context) error {
97 | return f(ctx)
98 | }
99 |
100 | func AssertionWaiter(assertion func() bool) Result {
101 | return resultFunc(func(ctx context.Context) error {
102 | for {
103 | select {
104 | case <-ctx.Done():
105 | return ctx.Err()
106 | default:
107 | time.Sleep(time.Millisecond * 200)
108 | if assertion() {
109 | return nil
110 | }
111 | }
112 | }
113 | })
114 | }
115 |
116 | func ConnWaiter(client *ably.Realtime, do func(), expectedEvent ...ably.ConnectionEvent) Result {
117 | change := make(chan ably.ConnectionStateChange, 1)
118 | off := client.Connection.OnAll(func(ev ably.ConnectionStateChange) {
119 | change <- ev
120 | })
121 | if do != nil {
122 | do()
123 | }
124 | for _, ev := range expectedEvent {
125 | if ev == ably.ConnectionEvent(client.Connection.State()) {
126 | var err error
127 | if errInfo := client.Connection.ErrorReason(); errInfo != nil {
128 | err = errInfo
129 | }
130 | return ResultFunc(func(context.Context) error { return err })
131 | }
132 | }
133 | return ResultFunc(func(ctx context.Context) error {
134 | defer off()
135 | timer := time.After(Timeout)
136 |
137 | for {
138 | select {
139 | case <-ctx.Done():
140 | return ctx.Err()
141 |
142 | case <-timer:
143 | return fmt.Errorf("timeout waiting for event %v", expectedEvent)
144 |
145 | case change := <-change:
146 | if change.Reason != nil {
147 | return change.Reason
148 | }
149 |
150 | if len(expectedEvent) > 0 {
151 | for _, ev := range expectedEvent {
152 | if ev == change.Event {
153 | return nil
154 | }
155 | }
156 | continue
157 | }
158 |
159 | return nil
160 | }
161 | }
162 | })
163 | }
164 |
165 | type ResultFunc func(context.Context) error
166 |
167 | func (f ResultFunc) Wait(ctx context.Context) error {
168 | return f(ctx)
169 | }
170 |
171 | func (f ResultFunc) Go() Result {
172 | err := make(chan error, 1)
173 | go func() {
174 | err <- f(context.Background())
175 | }()
176 | return ResultFunc(func(ctx context.Context) error {
177 | select {
178 | case <-ctx.Done():
179 | return ctx.Err()
180 | case err := <-err:
181 | return err
182 | }
183 | })
184 | }
185 |
--------------------------------------------------------------------------------
/ablytest/test_utils.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "sort"
5 | "testing"
6 | )
7 |
8 | func GetReconnectionTimersFrom(t *testing.T, afterCalls <-chan AfterCall) (reconnect AfterCall, suspend AfterCall) {
9 | var timers []AfterCall
10 | for i := 0; i < 2; i++ {
11 | var timer AfterCall
12 | Instantly.Recv(t, &timer, afterCalls, t.Fatalf)
13 | timers = append(timers, timer)
14 | }
15 | // Shortest timer is for reconnection.
16 | sort.Slice(timers, func(i, j int) bool {
17 | return timers[i].D < timers[j].D
18 | })
19 | reconnect, suspend = timers[0], timers[1]
20 | return
21 | }
22 |
--------------------------------------------------------------------------------
/ablytest/timeout.go:
--------------------------------------------------------------------------------
1 | package ablytest
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "testing"
7 | "time"
8 | )
9 |
10 | var Instantly = Before(10 * time.Millisecond)
11 |
12 | var Soon = Before(Timeout)
13 |
14 | // Before returns a WithTimeout with the given timeout duration.
15 | func Before(d time.Duration) WithTimeout {
16 | return WithTimeout{before: d}
17 | }
18 |
19 | // WithTimeout configures test helpers with a timeout.
20 | type WithTimeout struct {
21 | before time.Duration
22 | }
23 |
24 | // Recv asserts that a value is received through channel from before the
25 | // timeout. If it isn't, the fail function is called.
26 | //
27 | // If into is non-nil, it must be a pointer to a variable of the same type as
28 | // from's element type and it will be set to the received value, if any.
29 | //
30 | // It returns the second, boolean value returned by the receive operation,
31 | // or false if the operation times out.
32 | func (wt WithTimeout) Recv(t *testing.T, into, from interface{}, fail func(fmt string, args ...interface{}), failExtraArgs ...interface{}) (ok bool) {
33 | t.Helper()
34 | ok, timeout := wt.recv(into, from)
35 | if timeout {
36 | fail("timed out waiting for channel receive" + fmtExtraArgs(failExtraArgs))
37 | }
38 | return ok
39 | }
40 |
41 | // NoRecv is like Recv, except it asserts no value is received.
42 | func (wt WithTimeout) NoRecv(t *testing.T, into, from interface{}, fail func(fmt string, args ...interface{}), failExtraArgs ...interface{}) (ok bool) {
43 | t.Helper()
44 | if into == nil {
45 | into = &into
46 | }
47 | ok, timeout := wt.recv(into, from)
48 | if !timeout {
49 | fail("unexpectedly received in channel: %v"+fmtExtraArgs(failExtraArgs), into)
50 | }
51 | return ok
52 | }
53 |
54 | // Send is like Recv, except it sends.
55 | func (wt WithTimeout) Send(t *testing.T, ch, v interface{}, fail func(fmt string, args ...interface{}), failExtraArgs ...interface{}) (ok bool) {
56 | t.Helper()
57 | if timeout := wt.send(ch, v); timeout {
58 | fail("timed out waiting for channel send" + fmtExtraArgs(failExtraArgs))
59 | }
60 | return ok
61 | }
62 |
63 | func fmtExtraArgs(args []interface{}) string {
64 | if len(args) == 0 {
65 | return ""
66 | }
67 | return fmt.Sprintf(" [%s]", fmt.Sprint(args...))
68 | }
69 |
70 | func (wt WithTimeout) recv(into, from interface{}) (ok, timeout bool) {
71 | chosen, recv, ok := reflect.Select([]reflect.SelectCase{{
72 | Dir: reflect.SelectRecv,
73 | Chan: reflect.ValueOf(from),
74 | }, {
75 | Dir: reflect.SelectRecv,
76 | Chan: reflect.ValueOf(time.After(wt.before)),
77 | }})
78 | if chosen == 0 && ok && into != nil {
79 | reflect.ValueOf(into).Elem().Set(recv)
80 | }
81 | return ok, chosen == 1
82 | }
83 |
84 | func (wt WithTimeout) send(ch, v interface{}) (timeout bool) {
85 | chosen, _, _ := reflect.Select([]reflect.SelectCase{{
86 | Dir: reflect.SelectSend,
87 | Chan: reflect.ValueOf(ch),
88 | Send: reflect.ValueOf(v),
89 | }, {
90 | Dir: reflect.SelectRecv,
91 | Chan: reflect.ValueOf(time.After(wt.before)),
92 | }})
93 | return chosen == 1
94 | }
95 |
96 | func (wt WithTimeout) IsTrue(pred func() bool) bool {
97 | ticker := time.NewTicker(10 * time.Millisecond)
98 | defer ticker.Stop()
99 | timeout := time.NewTimer(wt.before)
100 | defer timeout.Stop()
101 | for {
102 | if pred() {
103 | return true
104 | }
105 | select {
106 | case <-ticker.C:
107 | case <-timeout.C:
108 | return pred()
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | # How to run
2 |
3 | 1. Realtime
4 |
5 | - Go to realtime dir `cd realtime`
6 | - Set the `ABLY_KEY` environment variable to your [Ably API key](https://faqs.ably.com/setting-up-and-managing-api-keys)
7 | - Realtime channel pub-sub
8 |
9 | `go run presence/main.go`
10 |
11 |
12 | - Realtime channel presence
13 |
14 | `go run pub-sub/main.go`
15 |
16 | 2. REST
17 |
18 | - Go to rest dir `cd rest`
19 | - Set the `ABLY_KEY` environment variable to your [Ably API key](https://faqs.ably.com/setting-up-and-managing-api-keys)
20 | - Rest channel publish
21 |
22 | `go run publish/main.go`
23 |
24 | - Rest channel presence
25 |
26 | `go run presence/main.go`
27 |
28 | - Rest channel message history
29 |
30 | `go run history/main.go`
31 |
32 | - Rest application stats
33 |
34 | `go run stats/main.go`
35 |
--------------------------------------------------------------------------------
/examples/constants.go:
--------------------------------------------------------------------------------
1 | package examples
2 |
3 | const EventName = "message"
4 | const UserName = "testUser"
5 | const AblyKey = "ABLY_KEY"
6 | const ChannelName = "chat"
7 |
--------------------------------------------------------------------------------
/examples/realtime/presence/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "time"
8 |
9 | "github.com/ably/ably-go/ably"
10 | "github.com/ably/ably-go/examples"
11 | )
12 |
13 | func main() {
14 | // Connect to Ably using the API key and ClientID specified
15 | client, err := ably.NewRealtime(
16 | ably.WithKey(os.Getenv(examples.AblyKey)),
17 | ably.WithClientID(examples.UserName))
18 | if err != nil {
19 | panic(err)
20 | }
21 |
22 | checkPresenceEnter(client)
23 | checkPresenceLeave(client)
24 | checkPresenceEnterAndLeave(client)
25 | }
26 |
27 | func checkPresenceEnter(client *ably.Realtime) {
28 | channel := client.Channels.Get(examples.ChannelName)
29 | unsubscribe := subscribePresenceEnter(channel)
30 | enterPresence(channel)
31 | time.Sleep(time.Second)
32 | unsubscribe()
33 | }
34 |
35 | func checkPresenceLeave(client *ably.Realtime) {
36 | channel := client.Channels.Get(examples.ChannelName)
37 | unsubscribe := subscribePresenceLeave(channel)
38 | leavePresence(channel)
39 | time.Sleep(time.Second)
40 | unsubscribe()
41 | }
42 |
43 | func checkPresenceEnterAndLeave(client *ably.Realtime) {
44 | channel := client.Channels.Get(examples.ChannelName)
45 | unsubscribe := subscribeAllPresence(channel)
46 | enterPresence(channel)
47 | printAllClientsOnChannel(channel)
48 | leavePresence(channel)
49 | time.Sleep(time.Second)
50 | unsubscribe()
51 | }
52 |
53 | func enterPresence(channel *ably.RealtimeChannel) {
54 | pErr := channel.Presence.Enter(context.Background(), examples.UserName+" entered the channel")
55 | if pErr != nil {
56 | err := fmt.Errorf("error with enter presence on the channel %w", pErr)
57 | fmt.Println(err)
58 | }
59 | }
60 |
61 | func enterOnBehalfOf(clientId string, channel *ably.RealtimeChannel) {
62 | pErr := channel.Presence.EnterClient(context.Background(), clientId, examples.UserName+" entered the channel on behalf of "+clientId)
63 | if pErr != nil {
64 | err := fmt.Errorf("error with enter presence on behalf of other client on the channel %w", pErr)
65 | fmt.Println(err)
66 | }
67 | }
68 |
69 | func updatePresence(channel *ably.RealtimeChannel) {
70 | pErr := channel.Presence.Update(context.Background(), examples.UserName+" entered the channel")
71 | if pErr != nil {
72 | err := fmt.Errorf("error with update presence on the channel %w", pErr)
73 | fmt.Println(err)
74 | }
75 | }
76 |
77 | func leavePresence(channel *ably.RealtimeChannel) {
78 | pErr := channel.Presence.Leave(context.Background(), examples.UserName+" entered the channel")
79 | if pErr != nil {
80 | err := fmt.Errorf("error with leave presence on the channel %w", pErr)
81 | fmt.Println(err)
82 | }
83 | }
84 |
85 | func subscribeAllPresence(channel *ably.RealtimeChannel) func() {
86 | // Subscribe to presence events (people entering and leaving) on the channel
87 | unsubscribeAll, pErr := channel.Presence.SubscribeAll(context.Background(), func(msg *ably.PresenceMessage) {
88 | if msg.Action == ably.PresenceActionEnter {
89 | fmt.Printf("%v has entered the chat\n", msg.ClientID)
90 | } else if msg.Action == ably.PresenceActionLeave {
91 | fmt.Printf("%v has left the chat\n", msg.ClientID)
92 | }
93 | })
94 | if pErr != nil {
95 | err := fmt.Errorf("error subscribing to presence in channel: %w", pErr)
96 | fmt.Println(err)
97 |
98 | }
99 | return unsubscribeAll
100 | }
101 |
102 | func subscribePresenceEnter(channel *ably.RealtimeChannel) func() {
103 | // Subscribe to presence events entering the channel
104 | unsubscribe, pErr := channel.Presence.Subscribe(context.Background(), ably.PresenceActionEnter, func(msg *ably.PresenceMessage) {
105 | if msg.Action == ably.PresenceActionEnter {
106 | fmt.Printf("%v has entered the chat\n", msg.ClientID)
107 | } else {
108 | panic("Not supposed to get presence related to actions other than presence enter")
109 | }
110 | })
111 |
112 | if pErr != nil {
113 | err := fmt.Errorf("error subscribing to enter presence in channel: %w", pErr)
114 | fmt.Println(err)
115 | }
116 | return unsubscribe
117 | }
118 |
119 | func subscribePresenceLeave(channel *ably.RealtimeChannel) func() {
120 | // Subscribe to presence events leaving the channel
121 | unsubscribe, pErr := channel.Presence.Subscribe(context.Background(), ably.PresenceActionLeave, func(msg *ably.PresenceMessage) {
122 | if msg.Action == ably.PresenceActionLeave {
123 | fmt.Printf("%v has left the chat\n", msg.ClientID)
124 | } else {
125 | panic("Not supposed to get presence related actions other than presence leave")
126 | }
127 | })
128 | if pErr != nil {
129 | err := fmt.Errorf("error subscribing to leave presence in channel: %w", pErr)
130 | fmt.Println(err)
131 | }
132 | return unsubscribe
133 | }
134 |
135 | func printAllClientsOnChannel(channel *ably.RealtimeChannel) {
136 | clients, err := channel.Presence.Get(context.Background())
137 | if err != nil {
138 | panic(err)
139 | }
140 | for _, client := range clients {
141 | fmt.Println("Present client:", client)
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/examples/realtime/pub-sub/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "time"
8 |
9 | "github.com/ably/ably-go/ably"
10 | "github.com/ably/ably-go/examples"
11 | )
12 |
13 | func main() {
14 | // Connect to Ably using the API key and ClientID specified
15 | client, err := ably.NewRealtime(
16 | ably.WithKey(os.Getenv(examples.AblyKey)),
17 | ably.WithClientID(examples.UserName))
18 | if err != nil {
19 | panic(err)
20 | }
21 |
22 | checkSubscribeAll(client)
23 | checkSubscribeToEvent(client)
24 | }
25 |
26 | func checkSubscribeAll(client *ably.Realtime) {
27 |
28 | channel := client.Channels.Get(examples.ChannelName)
29 |
30 | unsubscribeAll := subscribeAll(channel)
31 |
32 | publish(channel, "Hey there !!")
33 |
34 | time.Sleep(time.Second)
35 |
36 | unsubscribeAll()
37 | }
38 |
39 | func checkSubscribeToEvent(client *ably.Realtime) {
40 | // Connect to the Ably Channel with name 'chat'
41 | channel := client.Channels.Get(examples.ChannelName)
42 |
43 | unsubscribe := subscribeToEvent(channel)
44 |
45 | // publish message with blocking call
46 | publish(channel, "Hey there !!")
47 |
48 | time.Sleep(time.Second)
49 |
50 | unsubscribe()
51 | }
52 |
53 | func subscribeToEvent(channel *ably.RealtimeChannel) func() {
54 | // Subscribe to messages sent on the channel with given eventName
55 | unsubscribe, err := channel.Subscribe(context.Background(), examples.EventName, func(msg *ably.Message) {
56 | fmt.Printf("Received message from %v: '%v'\n", msg.ClientID, msg.Data)
57 | })
58 | if err != nil {
59 | err := fmt.Errorf("error subscribing to channel: %w", err)
60 | fmt.Println(err)
61 | }
62 | return unsubscribe
63 | }
64 |
65 | func subscribeAll(channel *ably.RealtimeChannel) func() {
66 | // Subscribe to all messages sent on the channel
67 | unsubscribeAll, err := channel.SubscribeAll(context.Background(), func(msg *ably.Message) {
68 | fmt.Printf("Received message from %v: '%v'\n", msg.ClientID, msg.Data)
69 | })
70 | if err != nil {
71 | err := fmt.Errorf("error subscribing to channel: %w", err)
72 | fmt.Println(err)
73 | }
74 | return unsubscribeAll
75 | }
76 |
77 | func publish(channel *ably.RealtimeChannel, message string) {
78 | // Publish the message to Ably Channel
79 | err := channel.Publish(context.Background(), examples.EventName, message)
80 | if err != nil {
81 | err := fmt.Errorf("error publishing to channel: %w", err)
82 | fmt.Println(err)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/examples/rest/history/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "time"
8 |
9 | "github.com/ably/ably-go/ably"
10 | "github.com/ably/ably-go/examples"
11 | )
12 |
13 | func main() {
14 | // Connect to Ably using the API key and ClientID
15 | client, err := ably.NewREST(
16 | ably.WithKey(os.Getenv(examples.AblyKey)),
17 | ably.WithClientID(examples.UserName))
18 |
19 | if err != nil {
20 | panic(err)
21 | }
22 |
23 | checkRestChannelMessageHistory(client)
24 | checkRestChannelPresenceHistory(client)
25 | }
26 |
27 | func checkRestChannelMessageHistory(client *ably.REST) {
28 | channel := client.Channels.Get(examples.ChannelName)
29 | realtimeClient := examples.InitRealtimeClient()
30 | examples.RealtimePublish(realtimeClient, "Hey there!")
31 | examples.RealtimePublish(realtimeClient, "How are you")
32 |
33 | time.Sleep(time.Second)
34 | printChannelMessageHistory(channel)
35 | time.Sleep(time.Second)
36 | realtimeClient.Close()
37 | }
38 |
39 | func checkRestChannelPresenceHistory(client *ably.REST) {
40 | channel := client.Channels.Get(examples.ChannelName)
41 | realtimeClient := examples.InitRealtimeClient()
42 | examples.RealtimeEnterPresence(realtimeClient)
43 |
44 | time.Sleep(time.Second)
45 | printChannelPresenceHistory(channel)
46 | time.Sleep(time.Second)
47 | realtimeClient.Close()
48 | }
49 |
50 | func printChannelMessageHistory(channel *ably.RESTChannel) {
51 | pages, err := channel.History().Pages(context.Background())
52 | if err != nil {
53 | panic(err)
54 | }
55 | for pages.Next(context.Background()) {
56 | for _, message := range pages.Items() {
57 | fmt.Println("--- Channel history ---")
58 | fmt.Println(examples.Jsonify(message))
59 | fmt.Println("--------")
60 | }
61 | }
62 | if err := pages.Err(); err != nil {
63 | panic(err)
64 | }
65 | }
66 |
67 | func printChannelPresenceHistory(channel *ably.RESTChannel) {
68 | pages, err := channel.Presence.History().Pages(context.Background())
69 | if err != nil {
70 | panic(err)
71 | }
72 | for pages.Next(context.Background()) {
73 | for _, presence := range pages.Items() {
74 | fmt.Println("--- Channel presence history ---")
75 | fmt.Println(examples.Jsonify(presence))
76 | fmt.Println("----------")
77 | }
78 | }
79 | if err := pages.Err(); err != nil {
80 | panic(err)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/examples/rest/presence/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "time"
8 |
9 | "github.com/ably/ably-go/ably"
10 | "github.com/ably/ably-go/examples"
11 | )
12 |
13 | func main() {
14 | // Connect to Ably using the API key and ClientID
15 | client, err := ably.NewREST(
16 | ably.WithKey(os.Getenv(examples.AblyKey)),
17 | ably.WithClientID(examples.UserName))
18 |
19 | if err != nil {
20 | panic(err)
21 | }
22 |
23 | checkPresence(client)
24 | }
25 |
26 | func checkPresence(client *ably.REST) {
27 | channel := client.Channels.Get(examples.ChannelName)
28 | realtimeClient := examples.InitRealtimeClient()
29 | examples.RealtimeEnterPresence(realtimeClient)
30 |
31 | printPresenceMessages(channel)
32 |
33 | time.Sleep(time.Second)
34 | examples.RealtimeLeavePresence(realtimeClient)
35 | realtimeClient.Close()
36 | }
37 |
38 | func printPresenceMessages(channel *ably.RESTChannel) {
39 |
40 | pages, err := channel.Presence.Get().Pages(context.Background())
41 | if err != nil {
42 | panic(err)
43 | }
44 | for pages.Next(context.Background()) {
45 | for _, presence := range pages.Items() {
46 | fmt.Println("--- Channel presence ---")
47 | fmt.Println(examples.Jsonify(presence))
48 | fmt.Println("----------")
49 | }
50 | }
51 | if err := pages.Err(); err != nil {
52 | panic(err)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/examples/rest/publish/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "time"
8 |
9 | "github.com/ably/ably-go/ably"
10 | "github.com/ably/ably-go/examples"
11 | )
12 |
13 | func main() {
14 | // Connect to Ably using the API key and ClientID
15 | client, err := ably.NewREST(
16 | ably.WithKey(os.Getenv(examples.AblyKey)),
17 | ably.WithClientID(examples.UserName))
18 | if err != nil {
19 | panic(err)
20 | }
21 |
22 | checkRestPublish(client)
23 | checkRestBulkPublish(client)
24 | }
25 |
26 | func checkRestPublish(client *ably.REST) {
27 | channel := client.Channels.Get(examples.ChannelName)
28 | realtimeClient := examples.InitRealtimeClient()
29 | unsubscribe := examples.RealtimeSubscribeToEvent(realtimeClient)
30 |
31 | restPublish(channel, "Hey there")
32 |
33 | time.Sleep(time.Second)
34 | unsubscribe()
35 | realtimeClient.Close()
36 | }
37 |
38 | func checkRestBulkPublish(client *ably.REST) {
39 | channel := client.Channels.Get(examples.ChannelName)
40 | realtimeClient := examples.InitRealtimeClient()
41 | unsubscribe := examples.RealtimeSubscribeToEvent(realtimeClient)
42 |
43 | restPublishBatch(channel, "Hey there", "How are you?")
44 |
45 | time.Sleep(time.Second)
46 | unsubscribe()
47 | realtimeClient.Close()
48 | }
49 |
50 | func restPublish(channel *ably.RESTChannel, message string) {
51 |
52 | err := channel.Publish(context.Background(), examples.EventName, message)
53 | if err != nil {
54 | err := fmt.Errorf("error publishing to channel: %w", err)
55 | panic(err)
56 | }
57 | }
58 |
59 | func restPublishBatch(channel *ably.RESTChannel, message1 string, message2 string) {
60 |
61 | err := channel.PublishMultiple(context.Background(), []*ably.Message{
62 | {Name: examples.EventName, Data: message1},
63 | {Name: examples.EventName, Data: message2},
64 | })
65 | if err != nil {
66 | err := fmt.Errorf("error batch publishing to channel: %w", err)
67 | panic(err)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/examples/rest/stats/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/ably/ably-go/ably"
9 | "github.com/ably/ably-go/examples"
10 | )
11 |
12 | func main() {
13 | // Connect to Ably using the API key and ClientID
14 | client, err := ably.NewREST(
15 | ably.WithKey(os.Getenv(examples.AblyKey)),
16 | ably.WithClientID(examples.UserName))
17 | if err != nil {
18 | panic(err)
19 | }
20 |
21 | printApplicationStats(client)
22 | }
23 |
24 | func printApplicationStats(client *ably.REST) {
25 | pages, err := client.Stats().Pages(context.Background())
26 | if err != nil {
27 | panic(err)
28 | }
29 |
30 | for pages.Next(context.Background()) {
31 | for _, stat := range pages.Items() {
32 | fmt.Println(examples.Jsonify(stat))
33 | }
34 | }
35 | if err := pages.Err(); err != nil {
36 | panic(err)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/rest/status/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/ably/ably-go/ably"
9 | "github.com/ably/ably-go/examples"
10 | )
11 |
12 | func main() {
13 | // Connect to Ably using the API key and ClientID
14 | client, err := ably.NewREST(
15 | ably.WithKey(os.Getenv(examples.AblyKey)),
16 | ably.WithClientID(examples.UserName))
17 | if err != nil {
18 | panic(err)
19 | }
20 |
21 | // Get channel
22 | channel := client.Channels.Get("channelName")
23 | // Get the channel status
24 | status, err := channel.Status(context.Background())
25 | if err != nil {
26 | panic(err)
27 | }
28 | fmt.Print(status, status.ChannelId)
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/examples/utils.go:
--------------------------------------------------------------------------------
1 | package examples
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "os"
8 |
9 | "github.com/ably/ably-go/ably"
10 | )
11 |
12 | func InitRealtimeClient() *ably.Realtime {
13 | client, err := ably.NewRealtime(
14 | ably.WithKey(os.Getenv(AblyKey)),
15 | // ably.WithEchoMessages(true), // Uncomment to stop messages you send from being sent back
16 | ably.WithClientID(UserName))
17 | if err != nil {
18 | panic(err)
19 | }
20 | return client
21 | }
22 |
23 | func RealtimeSubscribeToEvent(client *ably.Realtime) func() {
24 | channel := client.Channels.Get(ChannelName)
25 |
26 | // Subscribe to messages sent on the channel
27 | unsubscribe, err := channel.Subscribe(context.Background(), EventName, func(msg *ably.Message) {
28 | fmt.Printf("Received message from %v: '%v'\n", msg.ClientID, msg.Data)
29 | })
30 | if err != nil {
31 | err := fmt.Errorf("error subscribing to channel: %w", err)
32 | fmt.Println(err)
33 | }
34 | return unsubscribe
35 | }
36 |
37 | func RealtimeEnterPresence(client *ably.Realtime) {
38 | channel := client.Channels.Get(ChannelName)
39 | pErr := channel.Presence.Enter(context.Background(), UserName+" entered the channel")
40 | if pErr != nil {
41 | err := fmt.Errorf("error with enter presence on the channel %w", pErr)
42 | fmt.Println(err)
43 | }
44 | }
45 |
46 | func RealtimeLeavePresence(client *ably.Realtime) {
47 | channel := client.Channels.Get(ChannelName)
48 | pErr := channel.Presence.Leave(context.Background(), UserName+" entered the channel")
49 | if pErr != nil {
50 | err := fmt.Errorf("error with leave presence on the channel %w", pErr)
51 | fmt.Println(err)
52 | }
53 | }
54 |
55 | func RealtimePublish(client *ably.Realtime, message string) {
56 | channel := client.Channels.Get(ChannelName)
57 | // Publish the message typed in to the Ably Channel
58 | err := channel.Publish(context.Background(), EventName, message)
59 | if err != nil {
60 | err := fmt.Errorf("error publishing to channel: %w", err)
61 | fmt.Println(err)
62 | }
63 | }
64 |
65 | func Jsonify(i interface{}) string {
66 | s, _ := json.MarshalIndent(i, "", "\t")
67 | return string(s)
68 | }
69 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ably/ably-go
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/stretchr/testify v1.7.1
7 | github.com/ugorji/go/codec v1.1.9
8 | golang.org/x/sys v0.2.0
9 | nhooyr.io/websocket v1.8.7
10 | )
11 |
12 | require (
13 | github.com/davecgh/go-spew v1.1.1 // indirect
14 | github.com/klauspost/compress v1.10.3 // indirect
15 | github.com/pmezard/go-difflib v1.0.0 // indirect
16 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
17 | )
18 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
5 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
6 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
7 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
8 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
9 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
10 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
11 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
12 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
13 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
14 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
15 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
16 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
17 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
18 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
19 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
20 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
21 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
22 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
23 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
24 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
25 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
26 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
27 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
28 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
29 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
30 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
31 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
32 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
33 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
34 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
35 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
36 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
37 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
38 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
39 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
40 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
43 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
44 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
45 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
46 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
47 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
48 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
49 | github.com/ugorji/go v1.1.9/go.mod h1:chLrngdsg43geAaeId+nXO57YsDdl5OZqd/QtBiD19g=
50 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
51 | github.com/ugorji/go/codec v1.1.9 h1:J/7hhpkQwgypRNvaeh/T5gzJ2gEI/l8S3qyRrdEa1fA=
52 | github.com/ugorji/go/codec v1.1.9/go.mod h1:+SWgpdqOgdW5sBaiDfkHilQ1SxQ1hBkq/R+kHfL7Suo=
53 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
54 | golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
55 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
56 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
57 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
58 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
59 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
60 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
61 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
63 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
64 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
65 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
66 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
67 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
68 | nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
69 | nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
70 |
--------------------------------------------------------------------------------
/scripts/ci-generate.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # A script to be run with a clean working tree.
4 | # Runs the code generator and then ensures the working tree is still clean.
5 |
6 | set -e
7 |
8 | if [[ $(git diff --stat) != '' ]]; then
9 | echo 'FAILURE: Dirty working tree BEFORE code generation!'
10 | git diff
11 | exit 1
12 | fi
13 |
14 | go generate ./...
15 |
16 | if [[ $(git diff --stat) != '' ]]; then
17 | echo 'FAILURE: Dirty working tree after code generation.'
18 | git diff
19 | exit 1
20 | fi
21 |
--------------------------------------------------------------------------------
/scripts/errors/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "flag"
8 | "go/format"
9 | "go/scanner"
10 | "log"
11 | "os"
12 | "strings"
13 | "text/template"
14 | )
15 |
16 | type options struct {
17 | jsonSource string
18 | template string
19 | output string
20 | }
21 |
22 | const version = "0.3.0"
23 |
24 | func main() {
25 | log.SetFlags(log.Llongfile)
26 | opts := &options{
27 | template: errorsTemplate,
28 | }
29 | flag.StringVar(&opts.jsonSource, "json", "", `path to the ably-common/protocol/errors.json`)
30 | flag.StringVar(&opts.output, "o", "", "file to write output")
31 | flag.Parse()
32 | if opts.jsonSource == "" {
33 | flag.PrintDefaults()
34 | return
35 | }
36 | sourceFile, err := os.ReadFile(opts.jsonSource)
37 | if err != nil {
38 | log.Fatal(err)
39 | }
40 |
41 | codeToDesc := make(map[int]string)
42 | err = json.Unmarshal(sourceFile, &codeToDesc)
43 | if err != nil {
44 | log.Fatal(err)
45 | }
46 | tpl, err := template.New("errors").Parse(opts.template)
47 | if err != nil {
48 | log.Fatal(err)
49 | }
50 | var buf bytes.Buffer
51 | err = tpl.Execute(&buf, map[string]interface{}{
52 | "codes": codeToDesc,
53 | "version": version,
54 | })
55 | if err != nil {
56 | log.Fatal(err)
57 | }
58 | goFile, err := format.Source(buf.Bytes())
59 | if err != nil {
60 | var sErr scanner.ErrorList
61 | if errors.As(err, &sErr) {
62 | // Try to print out the line that caused the error.
63 | lines := strings.Split(buf.String(), "\n")
64 | pos := (sErr)[0].Pos
65 | log.Println("Error in line", pos.Line, lines[pos.Line])
66 | }
67 | log.Fatal(err)
68 | }
69 | if opts.output != "" {
70 | err = os.WriteFile(opts.output, goFile, 0600)
71 | if err != nil {
72 | log.Fatal(err)
73 | }
74 | }
75 | }
76 |
77 | const errorsTemplate = `// Code generated by errors-const v{{.version}} tool DO NOT EDIT.
78 |
79 | package ably
80 |
81 | // ErrorCode is the type for predefined Ably error codes.
82 | type ErrorCode int
83 |
84 | func (c ErrorCode) String() string {
85 | switch c {
86 | case 0:
87 | return "(error code not set)"
88 | {{- range $code, $desc := .codes}}
89 | case {{ $code}}: return "{{ $desc}}"
90 | {{- end}}
91 | }
92 | return ""
93 | }
94 | `
95 |
--------------------------------------------------------------------------------