relaygrid 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +87 -0
- data/QUICKSTART.md +112 -0
- data/README.md +249 -0
- data/examples/rails_initializer.rb +37 -0
- data/examples/usage_examples.rb +115 -0
- data/lib/relaygrid/client.rb +171 -0
- data/lib/relaygrid/configuration.rb +96 -0
- data/lib/relaygrid/delivery.rb +129 -0
- data/lib/relaygrid/errors.rb +105 -0
- data/lib/relaygrid/resources/channel_tokens.rb +35 -0
- data/lib/relaygrid/resources/deliveries.rb +101 -0
- data/lib/relaygrid/resources/message_templates.rb +33 -0
- data/lib/relaygrid/resources/messages.rb +63 -0
- data/lib/relaygrid/resources/notifications.rb +108 -0
- data/lib/relaygrid/resources/users.rb +117 -0
- data/lib/relaygrid/send_result.rb +78 -0
- data/lib/relaygrid/version.rb +5 -0
- data/lib/relaygrid/websocket_client.rb +214 -0
- data/lib/relaygrid.rb +74 -0
- data/sig/relaygrid.rbs +4 -0
- metadata +178 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0d0cc0707a44f80cc25e92aad237601e2fc3ec3f8ea2bd455facb68180843196
|
|
4
|
+
data.tar.gz: 237cd1b6377a7b4a8fc3ba57287da93990620efc3c6971af67277defcd50b886
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 289569fcadc3c5d298ea4450a0ee00c33e21db743725ef9116aff25206b5c5fcf10ae3564434fb6a8a08af8b31c138e67a9cf79e64debcbef9d5616ba51e3672
|
|
7
|
+
data.tar.gz: 30de0ca4b93b88a2740787d1ce97eba72bf17807fe9af59eb2df99041909a6d86916a76f64debc45a14af950c38dfb70627b9048898d06c410ec4580d7e4c37a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.1] - 2026-07-30
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
|
|
14
|
+
- `users.sms_opted_out?`, and `phone` is no longer forwarded from the `user:`
|
|
15
|
+
payload `notify` accepts. Contact management covers email; passing `phone:`
|
|
16
|
+
is now silently ignored rather than sent. Technically breaking for anyone who
|
|
17
|
+
reached for either in the hours 0.1.0 was current.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Contact documentation and examples cover email throughout.
|
|
22
|
+
|
|
23
|
+
## [0.1.0] - 2026-07-30
|
|
24
|
+
|
|
25
|
+
First public release.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `RelayGrid.client.notify(user:, template:, attributes:)` — the one-call send.
|
|
30
|
+
Creates or updates the recipient server-side, and returns a `SendResult` with
|
|
31
|
+
the message id, delivery ids, and a push token when the template has a live
|
|
32
|
+
push channel.
|
|
33
|
+
- `SendResult` and `Delivery` value objects. Both immutable; `Delivery` carries
|
|
34
|
+
status predicates (`queued?`, `sent?`, `delivered?`, `failed?`, `bounced?`,
|
|
35
|
+
`opened?`, `success?`) and `#refresh`.
|
|
36
|
+
- `deliveries.get(id)` and `deliveries.get_all(ids)` for delivery status, backed
|
|
37
|
+
by the new `GET /api/v1/deliveries` endpoints. `get_all` batches in chunks of
|
|
38
|
+
100 to match the server's cap.
|
|
39
|
+
- `result.wait_for_deliveries(timeout:, interval:)` — blocking poll until every
|
|
40
|
+
delivery settles. `failed` is not treated as terminal, since delivery jobs
|
|
41
|
+
retry.
|
|
42
|
+
- `messages.new_for(user_id:)`, `mark_as_seen`, `mark_as_delivered` for the
|
|
43
|
+
receive side, addressed by your own user id rather than an internal one.
|
|
44
|
+
- `RelayGrid.websocket(user_id:)` — Action Cable subscriber for `MessagesChannel`
|
|
45
|
+
that mints a fresh channel token on every connect and reconnect.
|
|
46
|
+
- `channel_tokens.create(user_id:)` to refresh a push token after its hour.
|
|
47
|
+
- `users` resource (`list`, `get`, `upsert`, `delete`) for managing recipients
|
|
48
|
+
outside a send, plus contact management — `contacts`, `set_contact` and
|
|
49
|
+
`delete_contact`.
|
|
50
|
+
- `notify` forwards `user[:email]`, which the server upserts as the contact
|
|
51
|
+
email deliveries resolve their recipient through. Omitting the key leaves that
|
|
52
|
+
contact untouched; an empty string removes it.
|
|
53
|
+
- Full error hierarchy under `RelayGrid::Error`, including `LimitExceededError`
|
|
54
|
+
(402) and `TemplateNotFoundError` / `UserNotFoundError` discriminated from a
|
|
55
|
+
404's message. Faraday exceptions no longer escape the gem.
|
|
56
|
+
- Automatic retries with jittered backoff for idempotent (GET) requests.
|
|
57
|
+
- `ca_file` configuration for trusting a private CA.
|
|
58
|
+
- RuboCop (Omakase, matching the backend) and a GitHub Actions matrix on Ruby
|
|
59
|
+
3.1/3.2/3.3.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **Renamed from `notify_hub` to `relaygrid`.** The module is now `RelayGrid`,
|
|
64
|
+
the require path is `relaygrid`, and environment variables are prefixed
|
|
65
|
+
`RELAYGRID_`. This settles the naming question ahead of the first release.
|
|
66
|
+
- `Client` instances are frozen after construction and safe to share across
|
|
67
|
+
threads.
|
|
68
|
+
- `Client.new` accepts per-client overrides (`Client.new(api_key: "...")`) for
|
|
69
|
+
apps serving multiple accounts.
|
|
70
|
+
- Configuration validates eagerly: a blank `api_key` or malformed `base_url`
|
|
71
|
+
raises `ConfigurationError` when the client is built.
|
|
72
|
+
- Minimum Ruby is now 3.1.
|
|
73
|
+
|
|
74
|
+
### Removed
|
|
75
|
+
|
|
76
|
+
- `config.account_id`. The account is resolved from the API key server-side, so
|
|
77
|
+
it was never needed on the wire.
|
|
78
|
+
- `config.verify_ssl`. TLS certificates are always verified; use `ca_file` to
|
|
79
|
+
trust a private CA. A switch to disable verification is not something this
|
|
80
|
+
client should offer, given it carries an API key and relays channel tokens.
|
|
81
|
+
- `actor_email` on sends. The API does not accept the parameter, so passing it
|
|
82
|
+
was silently a no-op.
|
|
83
|
+
|
|
84
|
+
### Dependencies
|
|
85
|
+
|
|
86
|
+
- Added `faraday-retry`.
|
|
87
|
+
- Dropped `faraday-multipart` and `faraday-follow_redirects` (unused).
|
data/QUICKSTART.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# RelayGrid Gem — Quick Start
|
|
2
|
+
|
|
3
|
+
From zero to a delivered notification. The [README](README.md) has the full
|
|
4
|
+
reference.
|
|
5
|
+
|
|
6
|
+
## 1. Install
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
# Gemfile
|
|
10
|
+
gem "relaygrid"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 2. Get an API key
|
|
18
|
+
|
|
19
|
+
Dashboard → **API Keys** → *New API Key*. Copy it — it is shown once. The key
|
|
20
|
+
identifies your account, so there is nothing else to configure.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# .env
|
|
24
|
+
RELAYGRID_API_KEY=sk_your_key_here
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 3. Configure
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# config/initializers/relaygrid.rb
|
|
31
|
+
RelayGrid.configure do |config|
|
|
32
|
+
config.api_key = ENV["RELAYGRID_API_KEY"]
|
|
33
|
+
config.base_url = ENV.fetch("RELAYGRID_BASE_URL", "https://relaygrid.dev")
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 4. Create a template
|
|
38
|
+
|
|
39
|
+
Dashboard → **Templates** → *New Template*. Give it a name you'll pass to
|
|
40
|
+
`notify` (say `order_shipped`), write the subject and body with `{{placeholders}}`,
|
|
41
|
+
attach at least one channel, and set it **active**.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Name: order_shipped
|
|
45
|
+
Subject: Hi {{first_name}}, order {{order_number}} shipped
|
|
46
|
+
Body: It should arrive {{eta}}.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 5. Send
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
result = RelayGrid.client.notify(
|
|
53
|
+
user: { id: current_user.id, first_name: current_user.first_name, last_name: current_user.last_name },
|
|
54
|
+
template: "order_shipped",
|
|
55
|
+
attributes: { first_name: current_user.first_name, order_number: order.number, eta: "tomorrow" }
|
|
56
|
+
)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`user[:id]` is **your** identifier. The recipient is created on first send, so
|
|
60
|
+
there's no registration step.
|
|
61
|
+
|
|
62
|
+
## 6. Check what happened
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
result.delivery_ids # => [201, 202] — keep these
|
|
66
|
+
|
|
67
|
+
delivery = RelayGrid.client.deliveries.get(201)
|
|
68
|
+
delivery.status # => "delivered"
|
|
69
|
+
delivery.delivered? # => true
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Store `delivery_ids` alongside whatever triggered the notification, and poll
|
|
73
|
+
them when you want status. Don't block a web request waiting — see
|
|
74
|
+
`wait_for_deliveries` in the README for when that helper is and isn't
|
|
75
|
+
appropriate.
|
|
76
|
+
|
|
77
|
+
## 7. Receive (optional)
|
|
78
|
+
|
|
79
|
+
If your template has a push channel, the send hands you a token for the
|
|
80
|
+
recipient's device:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
result.push_token # => "eyJf..." — pass to your frontend; expires in 1 hour
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Server-side, poll or subscribe:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
RelayGrid.client.messages.new_for(user_id: current_user.id)
|
|
90
|
+
|
|
91
|
+
ws = RelayGrid.websocket(user_id: current_user.id)
|
|
92
|
+
ws.on_message { |message| puts message["rendered_subject"] }
|
|
93
|
+
ws.connect
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Troubleshooting
|
|
97
|
+
|
|
98
|
+
| Problem | Cause |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `ConfigurationError: api_key is required` | `RELAYGRID_API_KEY` isn't set in this environment. |
|
|
101
|
+
| `AuthenticationError` | Key is wrong, deactivated, expired, or the account is suspended. |
|
|
102
|
+
| `TemplateNotFoundError` | Template name doesn't match, or the template is soft-deleted. |
|
|
103
|
+
| `LimitExceededError` | Monthly notification limit spent — check Usage in the dashboard. |
|
|
104
|
+
| `result.push_token` is `nil` | The template has no live push channel attached. |
|
|
105
|
+
| Delivery stuck at `queued` | The delivery job hasn't run yet; poll again shortly. |
|
|
106
|
+
| Delivery `failed` | Read `delivery.friendly_error_message` — usually channel credentials. |
|
|
107
|
+
|
|
108
|
+
## Next steps
|
|
109
|
+
|
|
110
|
+
- [README](README.md) — full configuration, error hierarchy, retry semantics
|
|
111
|
+
- [examples/usage_examples.rb](examples/usage_examples.rb) — runnable end-to-end script
|
|
112
|
+
- [examples/rails_initializer.rb](examples/rails_initializer.rb) — annotated initializer
|
data/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# RelayGrid Ruby Gem
|
|
2
|
+
|
|
3
|
+
Send and receive notifications through [RelayGrid](https://relaygrid.com) in one call.
|
|
4
|
+
|
|
5
|
+
Give it a user, a template name, and the attributes the template needs. Get back
|
|
6
|
+
a push token for the recipient's device and the delivery ids you can check later.
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
result = RelayGrid.client.notify(
|
|
10
|
+
user: { id: "user-123", first_name: "Ada", last_name: "Lovelace" },
|
|
11
|
+
template: "order_shipped",
|
|
12
|
+
attributes: { order_number: "1042", eta: "tomorrow" }
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
result.message_id # => 87
|
|
16
|
+
result.delivery_ids # => [201, 202]
|
|
17
|
+
result.push_token # => "eyJf..." (nil when the template has no push channel)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
gem "relaygrid"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then `bundle install`. Requires Ruby >= 3.1.
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# config/initializers/relaygrid.rb
|
|
32
|
+
RelayGrid.configure do |config|
|
|
33
|
+
config.api_key = ENV["RELAYGRID_API_KEY"]
|
|
34
|
+
config.base_url = ENV.fetch("RELAYGRID_BASE_URL", "https://relaygrid.dev")
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
| Option | Default | Purpose |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| `api_key` | — | Account API key (`sk_...`). Required. |
|
|
41
|
+
| `base_url` | `https://relaygrid.dev` | API origin. |
|
|
42
|
+
| `timeout` | `30` | Seconds to wait for a response. |
|
|
43
|
+
| `open_timeout` | `10` | Seconds to wait for the connection to open. |
|
|
44
|
+
| `max_retries` | `3` | Retry attempts for idempotent (GET) requests. |
|
|
45
|
+
| `logger` | `nil` | Logs requests when set. Headers and bodies are not logged. |
|
|
46
|
+
| `ca_file` | `nil` | PEM bundle for a private CA. |
|
|
47
|
+
| `ws_url` | derived from `base_url` | Overrides the Action Cable URL. |
|
|
48
|
+
|
|
49
|
+
Bad configuration raises `RelayGrid::ConfigurationError` when the client is
|
|
50
|
+
built, not on the first request.
|
|
51
|
+
|
|
52
|
+
**TLS.** Certificates are always verified. There is no option to turn
|
|
53
|
+
verification off: the client carries your API key and relays channel tokens, so
|
|
54
|
+
an unverified connection is never the right default. To trust a self-signed or
|
|
55
|
+
internal CA, point `ca_file` at its PEM bundle.
|
|
56
|
+
|
|
57
|
+
### Multiple accounts
|
|
58
|
+
|
|
59
|
+
`RelayGrid.client` is the shared default. For a host app talking to several
|
|
60
|
+
accounts, build clients with per-client overrides:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
client = RelayGrid::Client.new(api_key: account.relaygrid_key)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Clients are frozen once built and safe to share across threads.
|
|
67
|
+
|
|
68
|
+
## Sending
|
|
69
|
+
|
|
70
|
+
`user[:id]` is **your** identifier for the recipient. RelayGrid stores it as
|
|
71
|
+
`external_user_id` and never exposes its own internal ids to you.
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
result = RelayGrid.client.notify(
|
|
75
|
+
user: { id: "user-123", first_name: "Ada", last_name: "Lovelace" },
|
|
76
|
+
template: "order_shipped",
|
|
77
|
+
attributes: { order_number: "1042" }
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The recipient is created on first send and their names refreshed when they
|
|
82
|
+
change, so there is no separate registration step. If the recipient already
|
|
83
|
+
exists and you don't want to send names, pass the id on its own:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
RelayGrid.client.notify(user: "user-123", template: "order_shipped")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Email recipients
|
|
90
|
+
|
|
91
|
+
Push needs nothing but the id. Email deliveries resolve **who** to send to from
|
|
92
|
+
the address stored for the recipient, so include it in the send:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
RelayGrid.client.notify(
|
|
96
|
+
user: { id: "user-123", first_name: "Ada", last_name: "Lovelace",
|
|
97
|
+
email: "ada@example.com" },
|
|
98
|
+
template: "order_shipped"
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Without it, a send over an email channel produces a delivery that fails with
|
|
103
|
+
"no contact on file". A value RelayGrid can't use raises `ValidationError`
|
|
104
|
+
rather than failing quietly at delivery time.
|
|
105
|
+
|
|
106
|
+
Omitting the key leaves the existing address alone; passing an empty string
|
|
107
|
+
removes it. Contacts can also be managed outside a send:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
RelayGrid.client.users.contacts("user-123")
|
|
111
|
+
RelayGrid.client.users.set_contact("user-123", channel_type: "email", value: "ada@example.com")
|
|
112
|
+
RelayGrid.client.users.delete_contact("user-123", channel_type: "email")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### The result
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
result.message_id # => 87
|
|
119
|
+
result.delivery_ids # => [201, 202]
|
|
120
|
+
result.deliveries # => [#<RelayGrid::Delivery id: 201, channel_type: "push", status: "queued">, ...]
|
|
121
|
+
result.delivery_for("email") # => #<RelayGrid::Delivery ...>
|
|
122
|
+
result.rendered_subject # => "Hi Ada"
|
|
123
|
+
result.push_token # => "eyJf..." or nil
|
|
124
|
+
result.push_token_expires_at # => 2026-07-28 11:00:00 +0000
|
|
125
|
+
result.push? # => true when a push channel was dispatched
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### The push token
|
|
129
|
+
|
|
130
|
+
When the template has a live push channel, the send response carries a token
|
|
131
|
+
that authenticates **the recipient's device** to the realtime channel. Your
|
|
132
|
+
server receives it and hands it down to your client. It expires after an hour;
|
|
133
|
+
mint a fresh one with:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
RelayGrid.client.channel_tokens.create(user_id: "user-123")
|
|
137
|
+
# => { token: "eyJf...", expires_in: 3600, expires_at: 2026-07-28 11:00:00 +0000 }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Checking delivery status
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
delivery = RelayGrid.client.deliveries.get(201)
|
|
144
|
+
delivery.status # => "delivered"
|
|
145
|
+
delivery.delivered? # => true
|
|
146
|
+
delivery.channel_type # => "push"
|
|
147
|
+
delivery.friendly_error_message # => nil, or human-readable guidance when failed
|
|
148
|
+
|
|
149
|
+
RelayGrid.client.deliveries.get_all([201, 202]) # one batched request
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Predicates: `queued?`, `sent?`, `delivered?`, `failed?`, `bounced?`, `opened?`,
|
|
153
|
+
plus `success?` (delivered or opened). `#refresh` re-fetches and returns a new
|
|
154
|
+
`Delivery` — the objects are immutable.
|
|
155
|
+
|
|
156
|
+
### Waiting for deliveries to settle
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
settled = result.wait_for_deliveries(timeout: 30, interval: 2)
|
|
160
|
+
settled.all?(&:success?) # => true
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
> **This blocks the calling thread.** Use it in a background job, a rake task, or
|
|
164
|
+
> a script — never inside a web request, where it would pin a request thread for
|
|
165
|
+
> up to `timeout` seconds. In a request, return `delivery_ids` and let the
|
|
166
|
+
> browser poll, or subscribe over the websocket.
|
|
167
|
+
|
|
168
|
+
`failed` is deliberately **not** treated as final inside the window: delivery
|
|
169
|
+
jobs retry, so a failed delivery can still flip to `sent` and then `delivered`.
|
|
170
|
+
The helper waits rather than reporting a transient failure as permanent.
|
|
171
|
+
|
|
172
|
+
## Receiving
|
|
173
|
+
|
|
174
|
+
### Polling
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
RelayGrid.client.messages.new_for(user_id: "user-123") # unseen messages
|
|
178
|
+
RelayGrid.client.messages.mark_as_seen(message_id)
|
|
179
|
+
RelayGrid.client.messages.mark_as_delivered(message_id)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Realtime
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
ws = RelayGrid.websocket(user_id: "user-123")
|
|
186
|
+
ws.on_message { |message| puts message["rendered_subject"] }
|
|
187
|
+
ws.on_error { |error| Rails.logger.error(error.message) }
|
|
188
|
+
ws.connect
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A fresh channel token is minted on every connect and reconnect, so the hourly
|
|
192
|
+
expiry is handled for you. `connect(blocking: true)` blocks the thread — for a
|
|
193
|
+
standalone consumer process, not a web request.
|
|
194
|
+
|
|
195
|
+
## Errors
|
|
196
|
+
|
|
197
|
+
Everything descends from `RelayGrid::Error`, so `rescue RelayGrid::Error` always
|
|
198
|
+
suffices. Faraday exceptions never escape the gem.
|
|
199
|
+
|
|
200
|
+
| Class | Raised when |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `ConfigurationError` | Bad configuration, at client construction |
|
|
203
|
+
| `ConnectionError` | The request never produced a response |
|
|
204
|
+
| `TimeoutError` | A timeout (subclass of `ConnectionError`) |
|
|
205
|
+
| `AuthenticationError` | 401/403 — key invalid, inactive, expired, or account suspended |
|
|
206
|
+
| `LimitExceededError` | 402 — monthly notification limit spent |
|
|
207
|
+
| `NotFoundError` | 404 |
|
|
208
|
+
| `TemplateNotFoundError` | 404 naming a template (subclass of `NotFoundError`) |
|
|
209
|
+
| `UserNotFoundError` | 404 naming a recipient (subclass of `NotFoundError`) |
|
|
210
|
+
| `ValidationError` | 422 |
|
|
211
|
+
| `ServerError` | 5xx |
|
|
212
|
+
| `TimeoutWaitingForDeliveries` | `wait_for_deliveries` gave up (only with `raise_on_timeout`) |
|
|
213
|
+
|
|
214
|
+
`APIError` subclasses carry `#status`, `#body`, and `#error_message` (the
|
|
215
|
+
server's own message).
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
begin
|
|
219
|
+
RelayGrid.client.notify(user: user, template: "order_shipped")
|
|
220
|
+
rescue RelayGrid::LimitExceededError
|
|
221
|
+
# out of notifications this month
|
|
222
|
+
rescue RelayGrid::TemplateNotFoundError => e
|
|
223
|
+
Rails.logger.error(e.error_message)
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Retries and idempotency
|
|
228
|
+
|
|
229
|
+
GETs retry automatically (3 attempts by default, jittered exponential backoff)
|
|
230
|
+
on 429, 5xx, and transport failures.
|
|
231
|
+
|
|
232
|
+
**`notify` is never retried.** A blind retry would send the recipient a second
|
|
233
|
+
real notification. If a send fails, deciding whether to re-send is yours to
|
|
234
|
+
make. Server-side `Idempotency-Key` support is planned so retries can be made
|
|
235
|
+
safe.
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
bin/setup
|
|
241
|
+
bundle exec rspec
|
|
242
|
+
bundle exec rubocop
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Specs use WebMock; no example makes a real HTTP call or sleeps for real.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# config/initializers/relaygrid.rb
|
|
4
|
+
#
|
|
5
|
+
# Example Rails initializer for the RelayGrid gem. Copy it into your app at
|
|
6
|
+
# config/initializers/relaygrid.rb.
|
|
7
|
+
|
|
8
|
+
RelayGrid.configure do |config|
|
|
9
|
+
# Account API key ("sk_..."), from the API Keys page of your dashboard.
|
|
10
|
+
# Required -- the account is resolved from it server-side.
|
|
11
|
+
config.api_key = ENV.fetch("RELAYGRID_API_KEY", nil)
|
|
12
|
+
|
|
13
|
+
# API origin. Point this at your local stack in development.
|
|
14
|
+
config.base_url = ENV.fetch("RELAYGRID_BASE_URL", "https://relaygrid.dev")
|
|
15
|
+
|
|
16
|
+
# Seconds to wait for a response, and for the connection to open.
|
|
17
|
+
config.timeout = ENV.fetch("RELAYGRID_TIMEOUT", 30).to_i
|
|
18
|
+
config.open_timeout = ENV.fetch("RELAYGRID_OPEN_TIMEOUT", 10).to_i
|
|
19
|
+
|
|
20
|
+
# Optional: trust a private CA (self-signed local dev, internal deployments).
|
|
21
|
+
# TLS certificates are always verified -- there is no way to turn that off.
|
|
22
|
+
config.ca_file = ENV.fetch("RELAYGRID_CA_FILE", nil)
|
|
23
|
+
|
|
24
|
+
# Optional: log requests (method, URL, status). Headers and bodies are not
|
|
25
|
+
# logged, so your API key never reaches the log.
|
|
26
|
+
config.logger = Rails.logger if Rails.env.development?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Fail fast on a misconfigured deploy rather than on the first send: building
|
|
30
|
+
# the client validates the configuration.
|
|
31
|
+
begin
|
|
32
|
+
RelayGrid.client
|
|
33
|
+
Rails.logger.info("RelayGrid configured: #{RelayGrid.configuration.base_url}")
|
|
34
|
+
rescue RelayGrid::ConfigurationError => e
|
|
35
|
+
Rails.logger.warn("RelayGrid is not configured: #{e.message}")
|
|
36
|
+
Rails.logger.warn("Notifications will fail until this is corrected.")
|
|
37
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example usage of the RelayGrid gem.
|
|
4
|
+
#
|
|
5
|
+
# Run with:
|
|
6
|
+
# RELAYGRID_API_KEY=sk_... ruby examples/usage_examples.rb
|
|
7
|
+
|
|
8
|
+
require "relaygrid"
|
|
9
|
+
|
|
10
|
+
RelayGrid.configure do |config|
|
|
11
|
+
config.api_key = ENV.fetch("RELAYGRID_API_KEY")
|
|
12
|
+
config.base_url = ENV.fetch("RELAYGRID_BASE_URL", "https://relaygrid.dev")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# 1. Send a notification
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# `user[:id]` is your own identifier for the recipient. They are created on the
|
|
19
|
+
# first send and their names refreshed when they change, so there is no separate
|
|
20
|
+
# registration step.
|
|
21
|
+
|
|
22
|
+
result = RelayGrid.client.notify(
|
|
23
|
+
user: { id: "user-123", first_name: "Ada", last_name: "Lovelace" },
|
|
24
|
+
template: "order_shipped",
|
|
25
|
+
attributes: { order_number: "1042", eta: "tomorrow" }
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
puts "message: #{result.message_id}"
|
|
29
|
+
puts "deliveries: #{result.delivery_ids.inspect}"
|
|
30
|
+
puts "subject: #{result.rendered_subject}"
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# 2. Hand the push token to the recipient's device
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Present only when the template has a live push channel. It authenticates the
|
|
36
|
+
# *device*, not your server, and expires after an hour.
|
|
37
|
+
|
|
38
|
+
if result.push?
|
|
39
|
+
puts "push token: #{result.push_token[0, 12]}... (expires #{result.push_token_expires_at})"
|
|
40
|
+
else
|
|
41
|
+
puts "push token: none (template has no push channel)"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# 3. Check delivery status later
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
result.delivery_ids.each do |id|
|
|
49
|
+
delivery = RelayGrid.client.deliveries.get(id)
|
|
50
|
+
puts "delivery #{delivery.id} (#{delivery.channel_type}): #{delivery.status}"
|
|
51
|
+
puts " #{delivery.friendly_error_message}" if delivery.failed?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# One request instead of one per id:
|
|
55
|
+
RelayGrid.client.deliveries.get_all(result.delivery_ids).each do |delivery|
|
|
56
|
+
puts "#{delivery.id} => #{delivery.status}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
# 4. Wait for the deliveries to settle
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# BLOCKING -- fine in a script or background job, never in a web request.
|
|
63
|
+
|
|
64
|
+
settled = result.wait_for_deliveries(timeout: 30, interval: 2)
|
|
65
|
+
puts "all delivered: #{settled.all?(&:success?)}"
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# 5. Read a recipient's inbox
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
unseen = RelayGrid.client.messages.new_for(user_id: "user-123")
|
|
72
|
+
puts "unseen: #{unseen.size}"
|
|
73
|
+
|
|
74
|
+
unseen.each do |message|
|
|
75
|
+
puts " #{message['rendered_subject']}"
|
|
76
|
+
RelayGrid.client.messages.mark_as_seen(message["id"])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# 6. Handle errors
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Everything descends from RelayGrid::Error; no Faraday exception escapes.
|
|
83
|
+
|
|
84
|
+
begin
|
|
85
|
+
RelayGrid.client.notify(user: { id: "user-123", first_name: "Ada", last_name: "Lovelace" },
|
|
86
|
+
template: "no_such_template")
|
|
87
|
+
rescue RelayGrid::TemplateNotFoundError => e
|
|
88
|
+
puts "no such template: #{e.error_message}"
|
|
89
|
+
rescue RelayGrid::LimitExceededError
|
|
90
|
+
puts "monthly notification limit reached"
|
|
91
|
+
rescue RelayGrid::AuthenticationError
|
|
92
|
+
puts "API key rejected"
|
|
93
|
+
rescue RelayGrid::ConnectionError => e
|
|
94
|
+
puts "could not reach RelayGrid: #{e.message}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# 7. Receive in realtime
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# A fresh channel token is minted on every connect and reconnect, so the hourly
|
|
101
|
+
# expiry is handled for you.
|
|
102
|
+
|
|
103
|
+
ws = RelayGrid.websocket(user_id: "user-123")
|
|
104
|
+
ws.on_message { |message| puts "realtime: #{message['rendered_subject']}" }
|
|
105
|
+
ws.on_error { |error| warn "websocket error: #{error.message}" }
|
|
106
|
+
|
|
107
|
+
# ws.connect(blocking: true) # uncomment to run as a standalone consumer
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# 8. Serve several accounts from one process
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# Clients are frozen once built and safe to share across threads.
|
|
113
|
+
|
|
114
|
+
other_account = RelayGrid::Client.new(api_key: ENV.fetch("OTHER_RELAYGRID_API_KEY", "sk_other"))
|
|
115
|
+
puts "second client ready: #{other_account.configuration.base_url}"
|