cloudflare-ruby 0.0.1 → 0.1.2
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 +4 -4
- data/CHANGELOG.md +32 -0
- data/LICENSE +21 -0
- data/README.md +127 -3
- data/lib/cloudflare/configuration.rb +11 -0
- data/lib/cloudflare/connection.rb +52 -0
- data/lib/cloudflare/errors.rb +25 -0
- data/lib/cloudflare/realtime_kit/README.md +202 -0
- data/lib/cloudflare/realtime_kit/active_livestream_session.rb +46 -0
- data/lib/cloudflare/realtime_kit/active_session.rb +68 -0
- data/lib/cloudflare/realtime_kit/analytics.rb +57 -0
- data/lib/cloudflare/realtime_kit/app.rb +56 -0
- data/lib/cloudflare/realtime_kit/chat.rb +14 -0
- data/lib/cloudflare/realtime_kit/livestream.rb +27 -0
- data/lib/cloudflare/realtime_kit/livestream_session.rb +26 -0
- data/lib/cloudflare/realtime_kit/meeting.rb +82 -0
- data/lib/cloudflare/realtime_kit/participant.rb +40 -0
- data/lib/cloudflare/realtime_kit/preset.rb +31 -0
- data/lib/cloudflare/realtime_kit/recording.rb +85 -0
- data/lib/cloudflare/realtime_kit/session.rb +69 -0
- data/lib/cloudflare/realtime_kit/session_participant.rb +30 -0
- data/lib/cloudflare/realtime_kit/summary.rb +28 -0
- data/lib/cloudflare/realtime_kit/transcript.rb +17 -0
- data/lib/cloudflare/realtime_kit/webhook.rb +36 -0
- data/lib/cloudflare/realtime_kit.rb +23 -0
- data/lib/cloudflare/relation.rb +22 -0
- data/lib/cloudflare/resource.rb +309 -0
- data/lib/cloudflare/version.rb +3 -0
- data/lib/cloudflare-ruby.rb +1 -1
- data/lib/cloudflare.rb +34 -0
- data/sig/cloudflare/configuration.rbs +11 -0
- data/sig/cloudflare/connection.rbs +8 -0
- data/sig/cloudflare/errors.rbs +25 -0
- data/sig/cloudflare/realtime_kit.rbs +4 -0
- data/sig/cloudflare/relation.rbs +9 -0
- data/sig/cloudflare/resource.rbs +40 -0
- data/sig/cloudflare.rbs +11 -0
- data/spec/slices/realtime_kit.json +9627 -0
- metadata +70 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e55c381f1697329fa6cb8bd393493b4b63b31d0f3b4b672d43e61c2799c0c40
|
|
4
|
+
data.tar.gz: dba84759f533e9203bb0f70404b4d719ece8509a883bb83d2aa5accd607d0375
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e90e2f05d9b52b24195ff9d95c9849ff04290ac8d460ec0e59917869125e03cdc176a4717b2c1895891b463076d04ddbf30b17f4a8c52693d0f365bd6bef025f
|
|
7
|
+
data.tar.gz: 364098f7920cebdb13e6751be1bd28c32ba74b144e5a6f06e947a333a1b49b27efd5da9ae6befa3361253171df1755aa51816494f2b2821ee0190830733337e5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
- Add `require "bundler/gem_tasks"` to the Rakefile so `rake release`
|
|
6
|
+
resolves. The `rubygems/release-gem@v1` action invokes that task on
|
|
7
|
+
release; without it the workflow failed at the publish step.
|
|
8
|
+
|
|
9
|
+
## 0.1.1
|
|
10
|
+
|
|
11
|
+
- Re-publish 0.1.0 with the RubyGems trusted publisher configured. No
|
|
12
|
+
code changes.
|
|
13
|
+
|
|
14
|
+
## 0.1.0
|
|
15
|
+
|
|
16
|
+
- Initial release. Hand-written Ruby client for the Cloudflare API.
|
|
17
|
+
- Covers the **RealtimeKit** product surface: `App`, `Meeting`, `Participant`,
|
|
18
|
+
`ActiveSession`, `Session`, `SessionParticipant`, `Chat`, `Summary`,
|
|
19
|
+
`Transcript`, `Recording`, `Preset`, `Webhook`, `Livestream`,
|
|
20
|
+
`LivestreamSession`, `ActiveLivestreamSession`, and `Analytics`.
|
|
21
|
+
Every endpoint in the upstream OpenAPI slice is reachable through one
|
|
22
|
+
Ruby method (MECE-verified).
|
|
23
|
+
- AR-flavored foundation: `Cloudflare::Resource` with `attribute`,
|
|
24
|
+
`has_many`, `has_one`, `read_only`, plus `Cloudflare::Relation` for
|
|
25
|
+
nested-collection access.
|
|
26
|
+
- Stripe-style global config (`Cloudflare.configure`).
|
|
27
|
+
- Status-keyed error hierarchy: `AuthenticationError`, `NotFoundError`,
|
|
28
|
+
`ValidationError`, `RateLimitError`, `ServerError`.
|
|
29
|
+
- Drift-detection CLI: `bundle exec exe/cloudflare-ruby check-drift PRODUCT`
|
|
30
|
+
fetches upstream `openapi.json`, runs the slicer, and reports a
|
|
31
|
+
structured diff against the saved `spec/slices/<product>.json`. Caches
|
|
32
|
+
upstream at `tmp/upstream-openapi.json` for offline re-runs.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tokimonki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,9 +1,133 @@
|
|
|
1
1
|
# cloudflare-ruby
|
|
2
2
|
|
|
3
|
-
Ruby SDK for the Cloudflare API.
|
|
3
|
+
Ruby SDK for the Cloudflare API. Hand-written, narrow on purpose — currently covers the **RealtimeKit** product surface; other surfaces (R2, Workers, DNS) added on demand.
|
|
4
4
|
|
|
5
|
-
> **Status:
|
|
5
|
+
> **Status: prototyping.** API not yet stable. The 0.0.x line is exploratory; the first stable release will be `0.1.0`. Repo is private until then.
|
|
6
|
+
|
|
7
|
+
## Product surfaces
|
|
8
|
+
|
|
9
|
+
Each Cloudflare product is its own namespace under `Cloudflare::`, with its own README living next to the code:
|
|
10
|
+
|
|
11
|
+
| Product | Namespace | README |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| RealtimeKit | `Cloudflare::RealtimeKit` | [`lib/cloudflare/realtime_kit/README.md`](lib/cloudflare/realtime_kit/README.md) |
|
|
14
|
+
|
|
15
|
+
New product surfaces are added by writing the resource classes by hand (no codegen) and adding a slice entry under `spec/slices/<product>.json` for drift detection.
|
|
16
|
+
|
|
17
|
+
## Why hand-rolled and not openapi-generator / Stainless
|
|
18
|
+
|
|
19
|
+
- We only need a thin slice of Cloudflare's 2,000+ endpoint API.
|
|
20
|
+
- We want code that reads like the rest of our Ruby — generated SDKs in any language carry a generic style we'd rather not adopt.
|
|
21
|
+
- A dedicated drift checker, scoped to *what we use*, gives us automation without 17 KLOC of generated noise.
|
|
22
|
+
|
|
23
|
+
See [the design doc](https://github.com/am1006/tokimonki-exchange/issues/215) for the full reasoning.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Gemfile
|
|
29
|
+
gem "cloudflare-ruby", git: "git@github.com:tokimonki/cloudflare-ruby.git"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Configuration
|
|
33
|
+
|
|
34
|
+
Stripe-style global config; every resource picks it up implicitly.
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
Cloudflare.configure do |c|
|
|
38
|
+
c.api_token = ENV.fetch("CLOUDFLARE_API_TOKEN")
|
|
39
|
+
c.account_id = ENV.fetch("CLOUDFLARE_ACCOUNT_ID") # default for every call
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Per-call `account_id:` overrides the global. `api_token` has no per-call override — set it once.
|
|
44
|
+
|
|
45
|
+
## Errors
|
|
46
|
+
|
|
47
|
+
Every product surface raises a member of the same hierarchy:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
begin
|
|
51
|
+
Cloudflare::RealtimeKit::Meeting.find("missing", app_id: "app-1")
|
|
52
|
+
rescue Cloudflare::NotFoundError => e
|
|
53
|
+
e.status # => 404
|
|
54
|
+
e.body # => parsed response body
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Hierarchy:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Cloudflare::Error
|
|
62
|
+
├── AuthenticationError (401, 403)
|
|
63
|
+
├── NotFoundError (404)
|
|
64
|
+
├── ValidationError (422)
|
|
65
|
+
├── RateLimitError (429)
|
|
66
|
+
└── ServerError (5xx)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`Connection` retries 429/502/503/504 up to 2× with backoff before raising.
|
|
70
|
+
|
|
71
|
+
## Drift detection
|
|
72
|
+
|
|
73
|
+
The SDK is hand-written against a saved slice of Cloudflare's OpenAPI spec (`spec/slices/<product>.json`). When upstream changes, you re-extract the slice and diff to surface what moved:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bundle exec exe/cloudflare-ruby check-drift realtime_kit
|
|
77
|
+
# → "No drift detected for realtime_kit." (exit 0)
|
|
78
|
+
# → or a structured report of added/removed/changed paths and components (exit 1)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The fetched upstream spec is cached at `tmp/upstream-openapi.json` (gitignored) for inspection or offline re-runs (`--spec PATH`).
|
|
82
|
+
|
|
83
|
+
To re-baseline after an intentional update:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
bundle exec exe/cloudflare-ruby refresh-slice realtime_kit
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The diff covers `paths` plus every `components.*` sub-section discovered in either slice's union — new sections upstream starts using (e.g., `requestBodies`, `securitySchemes`) get diffed without code changes.
|
|
90
|
+
|
|
91
|
+
## Architecture
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
lib/
|
|
95
|
+
├── cloudflare.rb # top-level module, autoload registry
|
|
96
|
+
└── cloudflare/
|
|
97
|
+
├── version.rb
|
|
98
|
+
├── configuration.rb # global config (api_token, account_id, base_url)
|
|
99
|
+
├── connection.rb # Faraday singleton — auth, retries, JSON
|
|
100
|
+
├── errors.rb # status-keyed error classes
|
|
101
|
+
├── resource.rb # AR-flavored base (attribute, has_many, has_one, ...)
|
|
102
|
+
├── relation.rb # nested-collection proxy
|
|
103
|
+
└── <product>/ # one folder per product surface
|
|
104
|
+
├── README.md # product-specific DX guide
|
|
105
|
+
└── *.rb # resource classes
|
|
106
|
+
|
|
107
|
+
codegen/ # drift detection, not codegen
|
|
108
|
+
├── cli.rb # Thor: refresh-slice + check-drift
|
|
109
|
+
├── slicer.rb # extract paths-by-prefix from upstream
|
|
110
|
+
├── drift_detector.rb # structured diff
|
|
111
|
+
└── spec_version.rb # OpenAPI 3.0.3 pin
|
|
112
|
+
|
|
113
|
+
spec/slices/<product>.json # the saved slice — drift baseline
|
|
114
|
+
test/ # foundation + per-resource tests
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
bundle install
|
|
121
|
+
bundle exec rake test
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Adding a new product surface:
|
|
125
|
+
|
|
126
|
+
1. `bundle exec exe/cloudflare-ruby refresh-slice <product>` (after registering it in `codegen/cli.rb` `PRODUCTS`).
|
|
127
|
+
2. Write the resource classes under `lib/cloudflare/<product>/`.
|
|
128
|
+
3. Write a `lib/cloudflare/<product>/README.md` documenting the DX.
|
|
129
|
+
4. Add a row to the product surfaces table above.
|
|
6
130
|
|
|
7
131
|
## License
|
|
8
132
|
|
|
9
|
-
MIT
|
|
133
|
+
MIT.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Cloudflare
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :api_token, :account_id, :base_url, :timeout, :user_agent
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@base_url = "https://api.cloudflare.com/client/v4"
|
|
7
|
+
@timeout = 30
|
|
8
|
+
@user_agent = "cloudflare-ruby/#{VERSION}"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Cloudflare
|
|
2
|
+
# Internal Faraday wrapper. Singleton; shared by every resource across every
|
|
3
|
+
# product. Auth, retries, and JSON encoding live here so the Resource layer
|
|
4
|
+
# can stay focused on REST semantics.
|
|
5
|
+
class Connection
|
|
6
|
+
class << self
|
|
7
|
+
def instance = @instance ||= new
|
|
8
|
+
def reset! = @instance = nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def request(method, path, body: nil, params: nil)
|
|
12
|
+
raise Error, "Cloudflare.api_token not configured" if Cloudflare.api_token.nil?
|
|
13
|
+
|
|
14
|
+
faraday.public_send(method, path.delete_prefix("/")) do |req|
|
|
15
|
+
req.headers["Authorization"] = "Bearer #{Cloudflare.api_token}"
|
|
16
|
+
req.headers["User-Agent"] = Cloudflare.configuration.user_agent
|
|
17
|
+
req.params = compact(params) if params
|
|
18
|
+
req.body = compact(body) if body
|
|
19
|
+
end.body
|
|
20
|
+
rescue Faraday::Error => e
|
|
21
|
+
raise translate_error(e)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
def faraday
|
|
26
|
+
Faraday.new(url: Cloudflare.base_url, request: { timeout: Cloudflare.configuration.timeout }) do |f|
|
|
27
|
+
f.request :json
|
|
28
|
+
f.request :retry, max: 2, interval: 0.5, backoff_factor: 2,
|
|
29
|
+
retry_statuses: [ 429, 502, 503, 504 ]
|
|
30
|
+
f.response :json, content_type: /\bjson$/
|
|
31
|
+
f.response :raise_error
|
|
32
|
+
f.adapter Faraday.default_adapter
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Strip nil values recursively so unset kwargs don't become explicit nulls.
|
|
37
|
+
def compact(value)
|
|
38
|
+
case value
|
|
39
|
+
when Hash then value.each_with_object({}) { |(k, v), h| h[k] = compact(v) unless v.nil? }
|
|
40
|
+
when Array then value.map { compact(_1) }
|
|
41
|
+
else value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def translate_error(faraday_error)
|
|
46
|
+
status = faraday_error.response_status
|
|
47
|
+
body = faraday_error.response_body
|
|
48
|
+
klass = ERROR_BY_STATUS[status] || (status && status >= 500 ? ServerError : Error)
|
|
49
|
+
klass.new(faraday_error.message, status: status, body: body)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Cloudflare
|
|
2
|
+
class Error < StandardError
|
|
3
|
+
attr_reader :status, :body
|
|
4
|
+
|
|
5
|
+
def initialize(message = nil, status: nil, body: nil)
|
|
6
|
+
super(message)
|
|
7
|
+
@status = status
|
|
8
|
+
@body = body
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class AuthenticationError < Error; end
|
|
13
|
+
class NotFoundError < Error; end
|
|
14
|
+
class ValidationError < Error; end
|
|
15
|
+
class RateLimitError < Error; end
|
|
16
|
+
class ServerError < Error; end
|
|
17
|
+
|
|
18
|
+
ERROR_BY_STATUS = {
|
|
19
|
+
401 => AuthenticationError,
|
|
20
|
+
403 => AuthenticationError,
|
|
21
|
+
404 => NotFoundError,
|
|
22
|
+
422 => ValidationError,
|
|
23
|
+
429 => RateLimitError
|
|
24
|
+
}.freeze
|
|
25
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# RealtimeKit
|
|
2
|
+
|
|
3
|
+
Ruby bindings for [Cloudflare RealtimeKit](https://developers.cloudflare.com/realtime/realtimekit/) — managed video/audio infrastructure for meetings, livestreams, recordings, sessions, and analytics. Every endpoint in the upstream OpenAPI spec is reachable through one method on a hand-written class under `Cloudflare::RealtimeKit::*`.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require "cloudflare"
|
|
9
|
+
|
|
10
|
+
Cloudflare.configure do |c|
|
|
11
|
+
c.api_token = ENV.fetch("CLOUDFLARE_API_TOKEN")
|
|
12
|
+
c.account_id = ENV.fetch("CLOUDFLARE_ACCOUNT_ID")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
RK = Cloudflare::RealtimeKit # alias to taste
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
After that, `account_id:` is implicit on every call — pass it per-call only to override.
|
|
19
|
+
|
|
20
|
+
## Apps
|
|
21
|
+
|
|
22
|
+
Top-level tenants. Meetings, sessions, recordings, presets, webhooks, livestreams, and analytics all live under an app.
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
RK::App.create(name: "Production") # POST /apps
|
|
26
|
+
RK::App.all # GET /apps
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Meetings
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
meeting = RK::Meeting.create(app_id: app.id, title: "Standup", record_on_start: true)
|
|
33
|
+
meeting.id # => "mtg-…"
|
|
34
|
+
meeting.title # => "Standup"
|
|
35
|
+
meeting.persist_chat? # => false
|
|
36
|
+
meeting.created_at # => Time
|
|
37
|
+
|
|
38
|
+
meeting.update(title: "Daily standup") # PATCH
|
|
39
|
+
meeting.replace(title: "Daily", persist_chat: false) # PUT — full replacement
|
|
40
|
+
|
|
41
|
+
RK::Meeting.find(meeting.id, app_id: app.id)
|
|
42
|
+
RK::Meeting.all(app_id: app.id, page_no: 1, per_page: 50, search: "team")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Per-meeting livestream control:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
meeting.start_livestreaming(name: "Stream") # POST /meetings/{id}/livestreams
|
|
49
|
+
meeting.active_livestream # → Livestream
|
|
50
|
+
meeting.stop_livestream # POST /meetings/{id}/active-livestream/stop
|
|
51
|
+
meeting.livestream_details(page_no: 1, per_page: 10) # legacy bundled view
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Participants
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
host = meeting.participants.create(
|
|
58
|
+
custom_participant_id: "u-1",
|
|
59
|
+
preset_name: "host_preset",
|
|
60
|
+
name: "Alex"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
host.token # → join token
|
|
64
|
+
host.regenerate_token # mints fresh; mutates in place
|
|
65
|
+
host.update(name: "Alexandra")
|
|
66
|
+
host.destroy
|
|
67
|
+
|
|
68
|
+
meeting.participants.all
|
|
69
|
+
meeting.participants.find("p-1")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Active session — moderation surface
|
|
73
|
+
|
|
74
|
+
`meeting.active_session` returns a stub that auto-fetches its attrs only on the first attribute read. Action methods (`kick_all`, `mute_all`, etc.) skip the fetch — pure-action flows pay no GET.
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
active = meeting.active_session
|
|
78
|
+
active.live_participants # 1 GET
|
|
79
|
+
active.kick(participant_ids: %w[p-1 p-2], custom_participant_ids: [])
|
|
80
|
+
active.kick_all
|
|
81
|
+
active.mute(participant_ids: %w[p-1], custom_participant_ids: [])
|
|
82
|
+
active.mute_all(allow_unmute: false)
|
|
83
|
+
active.create_poll(question: "Lunch?", options: %w[Pizza Salad], anonymous: true)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Recording
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
rec = RK::Recording.create(app_id: app.id, meeting_id: meeting.id)
|
|
90
|
+
|
|
91
|
+
# Transitions: one canonical method, three sugar verbs.
|
|
92
|
+
rec.transition(action: :pause) # canonical — sole wire mapping for PUT /recordings/{id}
|
|
93
|
+
rec.pause # sugar — calls transition(action: :pause)
|
|
94
|
+
rec.resume
|
|
95
|
+
rec.stop
|
|
96
|
+
|
|
97
|
+
RK::Recording::TRANSITIONS # => [:pause, :resume, :stop] — spec-enumerated
|
|
98
|
+
RK::Recording.active_for(meeting_id: meeting.id, app_id: app.id)
|
|
99
|
+
RK::Recording.start_track(app_id: app.id, meeting_id: meeting.id, layers: [{ type: "audio" }])
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Sessions — historical records
|
|
103
|
+
|
|
104
|
+
Sessions are read-only from the SDK; they're created automatically by the platform when a meeting goes live.
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
session = RK::Session.find("sess-1", app_id: app.id)
|
|
108
|
+
session.participants.all # → Array<SessionParticipant>
|
|
109
|
+
session.chat.messages # auto-loads /sessions/{id}/chat
|
|
110
|
+
session.summary.text # auto-loads /sessions/{id}/summary
|
|
111
|
+
session.summary.generate # POST to (re)trigger summary
|
|
112
|
+
session.transcript.segments # auto-loads /sessions/{id}/transcript
|
|
113
|
+
session.livestream_sessions(page_no: 1, per_page: 20) # → Array<LivestreamSession>
|
|
114
|
+
|
|
115
|
+
# Reverse lookup: peer-id from a webhook → participant data
|
|
116
|
+
RK::Session.find_participant_by_peer_id("peer-99", app_id: app.id)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`SessionParticipant` is read-only — the upstream spec exposes only GET endpoints. Calling `session.participants.create(...)` raises `NoMethodError` immediately, before any HTTP call.
|
|
120
|
+
|
|
121
|
+
## Livestreams
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
ls = RK::Livestream.create(app_id: app.id, name: "Conference")
|
|
125
|
+
ls.ingest_server # => "rtmps://live.cloudflare.com:443/live/"
|
|
126
|
+
ls.stream_key
|
|
127
|
+
ls.playback_url # HLS
|
|
128
|
+
|
|
129
|
+
# Active session — bundles config + runtime stats as typed sub-objects
|
|
130
|
+
active = ls.active_session
|
|
131
|
+
active.livestream.stream_key # → Livestream instance
|
|
132
|
+
active.session.viewer_seconds # → LivestreamSession instance
|
|
133
|
+
|
|
134
|
+
# Direct lookup
|
|
135
|
+
RK::LivestreamSession.find("lsx-1", app_id: app.id)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Webhooks
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
hook = RK::Webhook.create(
|
|
142
|
+
app_id: app.id,
|
|
143
|
+
name: "Recording finished",
|
|
144
|
+
url: "https://example.com/hooks/realtimekit",
|
|
145
|
+
events: %w[recording.finished session.ended]
|
|
146
|
+
)
|
|
147
|
+
hook.enabled?
|
|
148
|
+
hook.update(enabled: false)
|
|
149
|
+
hook.replace(name: "...", url: "...", events: [...]) # PUT
|
|
150
|
+
hook.destroy
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Presets
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
preset = RK::Preset.create(
|
|
157
|
+
app_id: app.id,
|
|
158
|
+
name: "host_preset",
|
|
159
|
+
config: { ... },
|
|
160
|
+
ui: { ... },
|
|
161
|
+
permissions: { ... }
|
|
162
|
+
)
|
|
163
|
+
preset.update(permissions: { ... })
|
|
164
|
+
preset.destroy
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Analytics
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
RK::Analytics.daywise(app_id: app.id, start_date: "2025-01-01", end_date: "2025-01-31")
|
|
171
|
+
RK::Analytics.livestreams_overall(app_id: app.id, start_time: "...", end_time: "...")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Returns the raw report Hash — no per-row modeling.
|
|
175
|
+
|
|
176
|
+
## Conventions
|
|
177
|
+
|
|
178
|
+
- **Envelopes** (`{data: ...}` or `{result: ...}`) are unwrapped automatically. You read attributes as if they were flat.
|
|
179
|
+
- **Time coercion**: ISO-8601 strings → `Time` on read for any attribute typed `Time`.
|
|
180
|
+
- **Booleans**: typed attributes get a `?` predicate (`webhook.enabled?`).
|
|
181
|
+
- **Scope plumbing**: once you have a `meeting`, every nested call (`meeting.participants.find(id)`, `meeting.active_session.kick_all`) carries `account_id` + `app_id` + `meeting_id` automatically.
|
|
182
|
+
- **Singletons** (`active_session`, `chat`, `summary`, `transcript`): lazy-load on first attribute read. Action methods skip the fetch.
|
|
183
|
+
- **Read-only resources** (`SessionParticipant`): `create` / `update` / `destroy` raise `NoMethodError` before any HTTP call, citing the missing endpoint.
|
|
184
|
+
|
|
185
|
+
## Class index
|
|
186
|
+
|
|
187
|
+
| Class | Endpoints under |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `App` | `/accounts/{id}/realtime/kit/apps` |
|
|
190
|
+
| `Meeting` | `/.../{app_id}/meetings`, `/meetings/{id}` |
|
|
191
|
+
| `Participant` | `/meetings/{id}/participants` |
|
|
192
|
+
| `ActiveSession` | `/meetings/{id}/active-session` (singleton) |
|
|
193
|
+
| `Session` | `/.../{app_id}/sessions`, `/sessions/{id}` |
|
|
194
|
+
| `SessionParticipant` | `/sessions/{id}/participants` (read-only) |
|
|
195
|
+
| `Chat`, `Summary`, `Transcript` | session sub-singletons |
|
|
196
|
+
| `Recording` | `/.../{app_id}/recordings`, `/recordings/{id}` |
|
|
197
|
+
| `Preset` | `/.../{app_id}/presets`, `/presets/{id}` |
|
|
198
|
+
| `Webhook` | `/.../{app_id}/webhooks`, `/webhooks/{id}` |
|
|
199
|
+
| `Livestream` | `/.../{app_id}/livestreams`, `/livestreams/{id}` |
|
|
200
|
+
| `LivestreamSession` | `/livestreams/sessions/{id}` |
|
|
201
|
+
| `ActiveLivestreamSession` | `/livestreams/{id}/active-livestream-session` (singleton) |
|
|
202
|
+
| `Analytics` | `/.../{app_id}/analytics/*` (function namespace, not a Resource) |
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Cloudflare
|
|
2
|
+
module RealtimeKit
|
|
3
|
+
# A livestream's currently-active session. Singleton — there's at most one
|
|
4
|
+
# active session per livestream. Reached via +livestream.active_session+,
|
|
5
|
+
# which returns a stub that auto-fetches on first attribute read.
|
|
6
|
+
#
|
|
7
|
+
# The response bundles two sub-objects: +livestream+ (the broadcast
|
|
8
|
+
# configuration: ingest server, stream key, playback URL) and +session+
|
|
9
|
+
# (the runtime: started_time, viewer_seconds, status). Both are returned
|
|
10
|
+
# as typed instances of +Livestream+ and +LivestreamSession+ — partial
|
|
11
|
+
# copies of those schemas, but typed access reads better than Hash
|
|
12
|
+
# bracketing, and the returned instances carry enough scope to call
|
|
13
|
+
# +#reload+ against their canonical endpoints if you want a full refresh.
|
|
14
|
+
#
|
|
15
|
+
# active = livestream.active_session
|
|
16
|
+
# active.livestream.stream_key # → String
|
|
17
|
+
# active.session.viewer_seconds # → Integer
|
|
18
|
+
# active.livestream.reload # → fetches /livestreams/{id} for the full record
|
|
19
|
+
class ActiveLivestreamSession < Resource
|
|
20
|
+
member_path "/accounts/{account_id}/realtime/kit/{app_id}/livestreams/{livestream_id}/active-livestream-session"
|
|
21
|
+
scope_required :app_id, :livestream_id
|
|
22
|
+
|
|
23
|
+
# Override the default attribute readers: instead of returning the raw
|
|
24
|
+
# Hash sub-object, hand back a typed Livestream / LivestreamSession
|
|
25
|
+
# scoped to the same app so the caller can keep operating on it.
|
|
26
|
+
def livestream
|
|
27
|
+
ensure_loaded!
|
|
28
|
+
raw = @attrs["livestream"]
|
|
29
|
+
raw && Livestream.new(raw, scope: parent_scope)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def session
|
|
33
|
+
ensure_loaded!
|
|
34
|
+
raw = @attrs["session"]
|
|
35
|
+
raw && LivestreamSession.new(raw, scope: parent_scope)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
# The sub-objects share the app-level scope but not the
|
|
40
|
+
# +livestream_id+ that's specific to *this* active-session lookup.
|
|
41
|
+
def parent_scope
|
|
42
|
+
{ account_id: @scope[:account_id], app_id: @scope[:app_id] }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Cloudflare
|
|
2
|
+
module RealtimeKit
|
|
3
|
+
# A meeting's currently-running session. Modeled as a singleton: every
|
|
4
|
+
# meeting has at most one active session at a time. Reached via
|
|
5
|
+
# +meeting.active_session+, which auto-loads the session attributes on
|
|
6
|
+
# first access and caches them.
|
|
7
|
+
#
|
|
8
|
+
# active = meeting.active_session
|
|
9
|
+
# active.live_participants
|
|
10
|
+
# active.kick(participant_ids: [ "p-1" ])
|
|
11
|
+
# active.mute_all(allow_unmute: false)
|
|
12
|
+
# active.create_poll(question: "Lunch?", options: [ "Pizza", "Salad" ])
|
|
13
|
+
#
|
|
14
|
+
# +kick_all+ and +mute_all+ act on every participant; the others target
|
|
15
|
+
# specific ids (or +custom_participant_ids+ if you assigned them at
|
|
16
|
+
# invite time).
|
|
17
|
+
class ActiveSession < Resource
|
|
18
|
+
# Singleton path — no +{id}+ placeholder. The base +reload+ uses
|
|
19
|
+
# +member_path+ regardless of whether the path interpolates an id.
|
|
20
|
+
member_path "/accounts/{account_id}/realtime/kit/{app_id}/meetings/{meeting_id}/active-session"
|
|
21
|
+
scope_required :app_id, :meeting_id
|
|
22
|
+
|
|
23
|
+
attribute :id, String
|
|
24
|
+
attribute :associated_id, String
|
|
25
|
+
attribute :type, String
|
|
26
|
+
attribute :status, String
|
|
27
|
+
attribute :meeting_display_name, String
|
|
28
|
+
attribute :organization_id, String
|
|
29
|
+
attribute :live_participants, Integer
|
|
30
|
+
attribute :max_concurrent_participants, Integer
|
|
31
|
+
attribute :minutes_consumed, Integer
|
|
32
|
+
attribute :started_at, Time
|
|
33
|
+
attribute :ended_at, Time
|
|
34
|
+
attribute :created_at, Time
|
|
35
|
+
attribute :updated_at, Time
|
|
36
|
+
|
|
37
|
+
# POST .../active-session/kick — remove specified participants.
|
|
38
|
+
def kick(participant_ids:, custom_participant_ids:)
|
|
39
|
+
Connection.instance.request(:post, "#{member_path}/kick",
|
|
40
|
+
body: { participant_ids: participant_ids, custom_participant_ids: custom_participant_ids })
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# POST .../active-session/kick-all — remove every participant.
|
|
44
|
+
def kick_all
|
|
45
|
+
Connection.instance.request(:post, "#{member_path}/kick-all")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# POST .../active-session/mute — mute specified participants.
|
|
49
|
+
def mute(participant_ids:, custom_participant_ids:)
|
|
50
|
+
Connection.instance.request(:post, "#{member_path}/mute",
|
|
51
|
+
body: { participant_ids: participant_ids, custom_participant_ids: custom_participant_ids })
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# POST .../active-session/mute-all — mute every participant.
|
|
55
|
+
# +allow_unmute+ controls whether participants can re-enable their mic.
|
|
56
|
+
def mute_all(allow_unmute:)
|
|
57
|
+
Connection.instance.request(:post, "#{member_path}/mute-all",
|
|
58
|
+
body: { allow_unmute: allow_unmute })
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# POST .../active-session/poll — broadcast a poll to participants.
|
|
62
|
+
def create_poll(question:, options:, anonymous: nil, hide_votes: nil)
|
|
63
|
+
Connection.instance.request(:post, "#{member_path}/poll",
|
|
64
|
+
body: { question: question, options: options, anonymous: anonymous, hide_votes: hide_votes })
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module Cloudflare
|
|
2
|
+
module RealtimeKit
|
|
3
|
+
# Analytics endpoints. Not a CRUD resource — a thin namespace for the
|
|
4
|
+
# two reporting endpoints upstream exposes. Both return raw report data
|
|
5
|
+
# as a Hash; no per-resource modeling.
|
|
6
|
+
#
|
|
7
|
+
# Cloudflare::RealtimeKit::Analytics.daywise(
|
|
8
|
+
# app_id: "app-1",
|
|
9
|
+
# start_date: "2025-01-01",
|
|
10
|
+
# end_date: "2025-01-31"
|
|
11
|
+
# )
|
|
12
|
+
#
|
|
13
|
+
# Cloudflare::RealtimeKit::Analytics.livestreams_overall(
|
|
14
|
+
# app_id: "app-1",
|
|
15
|
+
# start_time: "2025-01-01T00:00:00Z",
|
|
16
|
+
# end_time: "2025-01-31T23:59:59Z"
|
|
17
|
+
# )
|
|
18
|
+
module Analytics
|
|
19
|
+
class << self
|
|
20
|
+
# GET /analytics/daywise — daily breakdown of meeting activity.
|
|
21
|
+
def daywise(app_id:, start_date:, end_date:, account_id: nil)
|
|
22
|
+
fetch("/accounts/{account_id}/realtime/kit/{app_id}/analytics/daywise",
|
|
23
|
+
app_id: app_id, account_id: account_id,
|
|
24
|
+
params: { start_date: start_date, end_date: end_date })
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# GET /analytics/livestreams/overall — overall livestream metrics.
|
|
28
|
+
def livestreams_overall(app_id:, start_time:, end_time:, account_id: nil)
|
|
29
|
+
fetch("/accounts/{account_id}/realtime/kit/{app_id}/analytics/livestreams/overall",
|
|
30
|
+
app_id: app_id, account_id: account_id,
|
|
31
|
+
params: { start_time: start_time, end_time: end_time })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
def fetch(path_template, app_id:, account_id:, params:)
|
|
36
|
+
scope = build_scope(account_id: account_id, app_id: app_id)
|
|
37
|
+
path = interpolate(path_template, scope)
|
|
38
|
+
response = Connection.instance.request(:get, path, params: params)
|
|
39
|
+
Resource.unwrap_envelope(response)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Local copies of the two private helpers from Resource — Analytics
|
|
43
|
+
# isn't a Resource subclass (no instances, just a function namespace),
|
|
44
|
+
# so it can't inherit the private class methods.
|
|
45
|
+
def build_scope(account_id:, app_id:)
|
|
46
|
+
account = account_id || Cloudflare.account_id
|
|
47
|
+
raise ArgumentError, "missing required scope param: account_id" unless account
|
|
48
|
+
{ account_id: account, app_id: app_id }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def interpolate(path, values)
|
|
52
|
+
path.gsub(/\{(\w+)\}/) { values[$1.to_sym] || raise(ArgumentError, "missing path param: #{$1}") }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|