cloudflare-ruby 0.0.1 → 0.2.0

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/LICENSE +21 -0
  4. data/README.md +127 -3
  5. data/lib/cloudflare/configuration.rb +11 -0
  6. data/lib/cloudflare/connection.rb +52 -0
  7. data/lib/cloudflare/errors.rb +25 -0
  8. data/lib/cloudflare/realtime_kit/README.md +209 -0
  9. data/lib/cloudflare/realtime_kit/active_livestream_session.rb +46 -0
  10. data/lib/cloudflare/realtime_kit/active_session.rb +68 -0
  11. data/lib/cloudflare/realtime_kit/analytics.rb +62 -0
  12. data/lib/cloudflare/realtime_kit/app.rb +56 -0
  13. data/lib/cloudflare/realtime_kit/chat.rb +14 -0
  14. data/lib/cloudflare/realtime_kit/livestream.rb +27 -0
  15. data/lib/cloudflare/realtime_kit/livestream_session.rb +26 -0
  16. data/lib/cloudflare/realtime_kit/meeting.rb +82 -0
  17. data/lib/cloudflare/realtime_kit/participant.rb +40 -0
  18. data/lib/cloudflare/realtime_kit/preset.rb +31 -0
  19. data/lib/cloudflare/realtime_kit/recording.rb +85 -0
  20. data/lib/cloudflare/realtime_kit/session.rb +69 -0
  21. data/lib/cloudflare/realtime_kit/session_participant.rb +30 -0
  22. data/lib/cloudflare/realtime_kit/summary.rb +28 -0
  23. data/lib/cloudflare/realtime_kit/transcript.rb +17 -0
  24. data/lib/cloudflare/realtime_kit/webhook.rb +36 -0
  25. data/lib/cloudflare/realtime_kit.rb +40 -0
  26. data/lib/cloudflare/relation.rb +22 -0
  27. data/lib/cloudflare/resource.rb +333 -0
  28. data/lib/cloudflare/version.rb +3 -0
  29. data/lib/cloudflare-ruby.rb +1 -1
  30. data/lib/cloudflare.rb +34 -0
  31. data/sig/cloudflare/configuration.rbs +11 -0
  32. data/sig/cloudflare/connection.rbs +8 -0
  33. data/sig/cloudflare/errors.rbs +25 -0
  34. data/sig/cloudflare/realtime_kit.rbs +4 -0
  35. data/sig/cloudflare/relation.rbs +9 -0
  36. data/sig/cloudflare/resource.rbs +40 -0
  37. data/sig/cloudflare.rbs +11 -0
  38. data/spec/slices/realtime_kit.json +9627 -0
  39. metadata +70 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79f116a0ab2331ea5587107448e486c7659867ca8e40b1519159819371d080a4
4
- data.tar.gz: 2e02047872c19bb021fda6c841ee7ac783ed8892274cb6e20a3a361f08dd5674
3
+ metadata.gz: e840df4dc6ff74e2cb0fddaba160fc041c80472c52f7a583f9c23185d24b8765
4
+ data.tar.gz: 9a42b0db13ed3d842fb1bc1b9df86011de55223a6667a666e3431bd455d4e4fd
5
5
  SHA512:
6
- metadata.gz: d8bd76dddd2e2051ca5b2ab81189889683877d8238acf9c9e33c5f40120cd5a8fc9e9e14193322e33b2be013d27b1e2f7c71d83a2d250f36dc77447a79067e70
7
- data.tar.gz: 6d259474067c3354e00f0dee5cd1e158f342cce6c13abea5c1d299162b61be3502f1c395a3db1300f63ad73965702fe171b1e567f1ead349f9afcb26ae46ac3c
6
+ metadata.gz: 71d24e6e2fbb63aee5c96e23e581f668207df71b7cc281cc02bfdb8c07e2e377e3a30e5a5888c90a5dd1acb0b0b4a4d734faf779bc9140b51a2400cad6029769
7
+ data.tar.gz: 9a73874dadf0b7847b5058a4ab40dec0acedb93765438d0c2b96632c376bddf1a9b23f04ee167206c7ee35a138ca8cfabca8c2e6eba66a20bd646050e13f5c7a
data/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - **Default `app_id` per process.** Set `Cloudflare::RealtimeKit.app_id`
6
+ once at boot and every RealtimeKit call defaults to it; pass `app_id:`
7
+ per-call only to override. Mirrors the existing `Cloudflare.account_id`
8
+ pattern. Eliminates `app_id:` repetition in apps that talk to a single
9
+ RealtimeKit app per process (the common case).
10
+ - `Resource.build_scope` and `extract_scope!` now consult a generic
11
+ `global_default_for(key)` helper that resolves `:account_id` against
12
+ `Cloudflare.account_id` and any other key against a same-named
13
+ accessor on the resource's product namespace
14
+ (e.g., `:app_id` → `Cloudflare::RealtimeKit.app_id`). Generalizes for
15
+ future product surfaces.
16
+ - `Recording.start_track`, `Recording.active_for`,
17
+ `Session.find_participant_by_peer_id`, `Analytics.daywise`, and
18
+ `Analytics.livestreams_overall` now accept `app_id:` as optional with
19
+ the default falling through.
20
+
21
+ ## 0.1.2
22
+
23
+ - Add `require "bundler/gem_tasks"` to the Rakefile so `rake release`
24
+ resolves. The `rubygems/release-gem@v1` action invokes that task on
25
+ release; without it the workflow failed at the publish step.
26
+
27
+ ## 0.1.1
28
+
29
+ - Re-publish 0.1.0 with the RubyGems trusted publisher configured. No
30
+ code changes.
31
+
32
+ ## 0.1.0
33
+
34
+ - Initial release. Hand-written Ruby client for the Cloudflare API.
35
+ - Covers the **RealtimeKit** product surface: `App`, `Meeting`, `Participant`,
36
+ `ActiveSession`, `Session`, `SessionParticipant`, `Chat`, `Summary`,
37
+ `Transcript`, `Recording`, `Preset`, `Webhook`, `Livestream`,
38
+ `LivestreamSession`, `ActiveLivestreamSession`, and `Analytics`.
39
+ Every endpoint in the upstream OpenAPI slice is reachable through one
40
+ Ruby method (MECE-verified).
41
+ - AR-flavored foundation: `Cloudflare::Resource` with `attribute`,
42
+ `has_many`, `has_one`, `read_only`, plus `Cloudflare::Relation` for
43
+ nested-collection access.
44
+ - Stripe-style global config (`Cloudflare.configure`).
45
+ - Status-keyed error hierarchy: `AuthenticationError`, `NotFoundError`,
46
+ `ValidationError`, `RateLimitError`, `ServerError`.
47
+ - Drift-detection CLI: `bundle exec exe/cloudflare-ruby check-drift PRODUCT`
48
+ fetches upstream `openapi.json`, runs the slicer, and reports a
49
+ structured diff against the saved `spec/slices/<product>.json`. Caches
50
+ 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: placeholder.** This release reserves the gem name. The real SDK is in active development. Track progress at https://github.com/tokimonki/cloudflare-ruby.
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,209 @@
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
+ # Default app_id for every RealtimeKit call this process makes.
16
+ # Pass `app_id:` per-call only to override (e.g., admin tools that hop
17
+ # between apps).
18
+ Cloudflare::RealtimeKit.app_id = ENV.fetch("REALTIMEKIT_APP_ID")
19
+
20
+ RK = Cloudflare::RealtimeKit # alias to taste
21
+ ```
22
+
23
+ After that, both `account_id:` and `app_id:` are implicit on every call — pass either per-call only to override.
24
+
25
+ ## Apps
26
+
27
+ Top-level tenants. Meetings, sessions, recordings, presets, webhooks, livestreams, and analytics all live under an app.
28
+
29
+ ```ruby
30
+ RK::App.create(name: "Production") # POST /apps
31
+ RK::App.all # GET /apps
32
+ ```
33
+
34
+ ## Meetings
35
+
36
+ Most calls below show `app_id:` for clarity. With `Cloudflare::RealtimeKit.app_id` set globally (see Setup), you can omit it.
37
+
38
+ ```ruby
39
+ meeting = RK::Meeting.create(app_id: app.id, title: "Standup", record_on_start: true)
40
+ meeting.id # => "mtg-…"
41
+ meeting.title # => "Standup"
42
+ meeting.persist_chat? # => false
43
+ meeting.created_at # => Time
44
+
45
+ meeting.update(title: "Daily standup") # PATCH
46
+ meeting.replace(title: "Daily", persist_chat: false) # PUT — full replacement
47
+
48
+ RK::Meeting.find(meeting.id, app_id: app.id)
49
+ RK::Meeting.all(app_id: app.id, page_no: 1, per_page: 50, search: "team")
50
+ ```
51
+
52
+ Per-meeting livestream control:
53
+
54
+ ```ruby
55
+ meeting.start_livestreaming(name: "Stream") # POST /meetings/{id}/livestreams
56
+ meeting.active_livestream # → Livestream
57
+ meeting.stop_livestream # POST /meetings/{id}/active-livestream/stop
58
+ meeting.livestream_details(page_no: 1, per_page: 10) # legacy bundled view
59
+ ```
60
+
61
+ ## Participants
62
+
63
+ ```ruby
64
+ host = meeting.participants.create(
65
+ custom_participant_id: "u-1",
66
+ preset_name: "host_preset",
67
+ name: "Alex"
68
+ )
69
+
70
+ host.token # → join token
71
+ host.regenerate_token # mints fresh; mutates in place
72
+ host.update(name: "Alexandra")
73
+ host.destroy
74
+
75
+ meeting.participants.all
76
+ meeting.participants.find("p-1")
77
+ ```
78
+
79
+ ## Active session — moderation surface
80
+
81
+ `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.
82
+
83
+ ```ruby
84
+ active = meeting.active_session
85
+ active.live_participants # 1 GET
86
+ active.kick(participant_ids: %w[p-1 p-2], custom_participant_ids: [])
87
+ active.kick_all
88
+ active.mute(participant_ids: %w[p-1], custom_participant_ids: [])
89
+ active.mute_all(allow_unmute: false)
90
+ active.create_poll(question: "Lunch?", options: %w[Pizza Salad], anonymous: true)
91
+ ```
92
+
93
+ ## Recording
94
+
95
+ ```ruby
96
+ rec = RK::Recording.create(app_id: app.id, meeting_id: meeting.id)
97
+
98
+ # Transitions: one canonical method, three sugar verbs.
99
+ rec.transition(action: :pause) # canonical — sole wire mapping for PUT /recordings/{id}
100
+ rec.pause # sugar — calls transition(action: :pause)
101
+ rec.resume
102
+ rec.stop
103
+
104
+ RK::Recording::TRANSITIONS # => [:pause, :resume, :stop] — spec-enumerated
105
+ RK::Recording.active_for(meeting_id: meeting.id, app_id: app.id)
106
+ RK::Recording.start_track(app_id: app.id, meeting_id: meeting.id, layers: [{ type: "audio" }])
107
+ ```
108
+
109
+ ## Sessions — historical records
110
+
111
+ Sessions are read-only from the SDK; they're created automatically by the platform when a meeting goes live.
112
+
113
+ ```ruby
114
+ session = RK::Session.find("sess-1", app_id: app.id)
115
+ session.participants.all # → Array<SessionParticipant>
116
+ session.chat.messages # auto-loads /sessions/{id}/chat
117
+ session.summary.text # auto-loads /sessions/{id}/summary
118
+ session.summary.generate # POST to (re)trigger summary
119
+ session.transcript.segments # auto-loads /sessions/{id}/transcript
120
+ session.livestream_sessions(page_no: 1, per_page: 20) # → Array<LivestreamSession>
121
+
122
+ # Reverse lookup: peer-id from a webhook → participant data
123
+ RK::Session.find_participant_by_peer_id("peer-99", app_id: app.id)
124
+ ```
125
+
126
+ `SessionParticipant` is read-only — the upstream spec exposes only GET endpoints. Calling `session.participants.create(...)` raises `NoMethodError` immediately, before any HTTP call.
127
+
128
+ ## Livestreams
129
+
130
+ ```ruby
131
+ ls = RK::Livestream.create(app_id: app.id, name: "Conference")
132
+ ls.ingest_server # => "rtmps://live.cloudflare.com:443/live/"
133
+ ls.stream_key
134
+ ls.playback_url # HLS
135
+
136
+ # Active session — bundles config + runtime stats as typed sub-objects
137
+ active = ls.active_session
138
+ active.livestream.stream_key # → Livestream instance
139
+ active.session.viewer_seconds # → LivestreamSession instance
140
+
141
+ # Direct lookup
142
+ RK::LivestreamSession.find("lsx-1", app_id: app.id)
143
+ ```
144
+
145
+ ## Webhooks
146
+
147
+ ```ruby
148
+ hook = RK::Webhook.create(
149
+ app_id: app.id,
150
+ name: "Recording finished",
151
+ url: "https://example.com/hooks/realtimekit",
152
+ events: %w[recording.finished session.ended]
153
+ )
154
+ hook.enabled?
155
+ hook.update(enabled: false)
156
+ hook.replace(name: "...", url: "...", events: [...]) # PUT
157
+ hook.destroy
158
+ ```
159
+
160
+ ## Presets
161
+
162
+ ```ruby
163
+ preset = RK::Preset.create(
164
+ app_id: app.id,
165
+ name: "host_preset",
166
+ config: { ... },
167
+ ui: { ... },
168
+ permissions: { ... }
169
+ )
170
+ preset.update(permissions: { ... })
171
+ preset.destroy
172
+ ```
173
+
174
+ ## Analytics
175
+
176
+ ```ruby
177
+ RK::Analytics.daywise(app_id: app.id, start_date: "2025-01-01", end_date: "2025-01-31")
178
+ RK::Analytics.livestreams_overall(app_id: app.id, start_time: "...", end_time: "...")
179
+ ```
180
+
181
+ Returns the raw report Hash — no per-row modeling.
182
+
183
+ ## Conventions
184
+
185
+ - **Envelopes** (`{data: ...}` or `{result: ...}`) are unwrapped automatically. You read attributes as if they were flat.
186
+ - **Time coercion**: ISO-8601 strings → `Time` on read for any attribute typed `Time`.
187
+ - **Booleans**: typed attributes get a `?` predicate (`webhook.enabled?`).
188
+ - **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.
189
+ - **Singletons** (`active_session`, `chat`, `summary`, `transcript`): lazy-load on first attribute read. Action methods skip the fetch.
190
+ - **Read-only resources** (`SessionParticipant`): `create` / `update` / `destroy` raise `NoMethodError` before any HTTP call, citing the missing endpoint.
191
+
192
+ ## Class index
193
+
194
+ | Class | Endpoints under |
195
+ |---|---|
196
+ | `App` | `/accounts/{id}/realtime/kit/apps` |
197
+ | `Meeting` | `/.../{app_id}/meetings`, `/meetings/{id}` |
198
+ | `Participant` | `/meetings/{id}/participants` |
199
+ | `ActiveSession` | `/meetings/{id}/active-session` (singleton) |
200
+ | `Session` | `/.../{app_id}/sessions`, `/sessions/{id}` |
201
+ | `SessionParticipant` | `/sessions/{id}/participants` (read-only) |
202
+ | `Chat`, `Summary`, `Transcript` | session sub-singletons |
203
+ | `Recording` | `/.../{app_id}/recordings`, `/recordings/{id}` |
204
+ | `Preset` | `/.../{app_id}/presets`, `/presets/{id}` |
205
+ | `Webhook` | `/.../{app_id}/webhooks`, `/webhooks/{id}` |
206
+ | `Livestream` | `/.../{app_id}/livestreams`, `/livestreams/{id}` |
207
+ | `LivestreamSession` | `/livestreams/sessions/{id}` |
208
+ | `ActiveLivestreamSession` | `/livestreams/{id}/active-livestream-session` (singleton) |
209
+ | `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