webinarjam 0.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +115 -0
- data/lib/webinarjam/client.rb +149 -0
- data/lib/webinarjam/errors.rb +26 -0
- data/lib/webinarjam/version.rb +5 -0
- data/lib/webinarjam.rb +26 -0
- metadata +56 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 007f29eaa5a4eeefb046bcfa9b02f3e25e39406acad46b0a395ae969533b91a3
|
|
4
|
+
data.tar.gz: df1a31482c8dd326010dc2e13ad6d60657a9a57b3502e3a363ff54b511adbd1b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f223c1847c4e590be08cb8882e08806e04cac0d59f4b341507a06dd4a527a97074a117e658d6c1ffbac94062bfd77d8c228536c0cb4a20c63f0f46d819a26f30
|
|
7
|
+
data.tar.gz: dcd6b918f3131a4aab6f49f5215787343f8250a8fdc92bc509f4fa6712efa06ad6d8afb5a8d2e1629c895ace00cb2a9d5362ad24226cc1c77170011afe46adf8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.0.0] - 2026-08-13
|
|
4
|
+
|
|
5
|
+
- Initial release claiming the gem name.
|
|
6
|
+
- `WebinarJam::Client` covering all documented WebinarJam API endpoints:
|
|
7
|
+
`#webinars`, `#webinar`, `#register`, `#registrants`, `#unsubscribe`.
|
|
8
|
+
- EverWebinar support via `product: :everwebinar`.
|
|
9
|
+
- Typed errors (`AuthenticationError`, `RateLimitError`, `APIError`) and
|
|
10
|
+
opt-in automatic retries on HTTP 429.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yi-Ru Lin
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# WebinarJam
|
|
2
|
+
|
|
3
|
+
A dependency-free Ruby client for the [WebinarJam / EverWebinar API](https://support.webinarjam.com/en/articles/15370144-connect-to-webinarjam-or-everwebinar-api).
|
|
4
|
+
|
|
5
|
+
Covers every documented endpoint:
|
|
6
|
+
|
|
7
|
+
| Method | Endpoint | Purpose |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `#webinars` | `POST /webinars` | List all published webinars |
|
|
10
|
+
| `#webinar(id)` | `POST /webinar` | Details for one webinar (schedules, URLs, presenters) |
|
|
11
|
+
| `#register(...)` | `POST /register` | Register a user to a webinar |
|
|
12
|
+
| `#registrants(...)` | `POST /registrants` | List registrants and attendees |
|
|
13
|
+
| `#unsubscribe(...)` | `POST /unsubscribe` | Unsubscribe a lead from notifications |
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# Gemfile
|
|
19
|
+
gem "webinarjam"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
require "webinarjam"
|
|
26
|
+
|
|
27
|
+
client = WebinarJam::Client.new(api_key: ENV["WEBINARJAM_API_KEY"])
|
|
28
|
+
# api_key: defaults to ENV["WEBINARJAM_API_KEY"], so this is equivalent:
|
|
29
|
+
client = WebinarJam::Client.new
|
|
30
|
+
|
|
31
|
+
# Or configure once, module-wide:
|
|
32
|
+
WebinarJam.configure { |config| config.api_key = "your-key" }
|
|
33
|
+
client = WebinarJam.client
|
|
34
|
+
|
|
35
|
+
# List all webinars
|
|
36
|
+
client.webinars
|
|
37
|
+
# => { status: "success", webinars: [{ webinar_id: 1, webinar_hash: "abcd1234", ... }] }
|
|
38
|
+
|
|
39
|
+
# Details for one webinar (schedule ids live here)
|
|
40
|
+
details = client.webinar(1)
|
|
41
|
+
schedule = details[:webinar][:schedules].first[:schedule]
|
|
42
|
+
|
|
43
|
+
# Register a user
|
|
44
|
+
client.register(
|
|
45
|
+
webinar_id: 1,
|
|
46
|
+
schedule: schedule,
|
|
47
|
+
first_name: "Jane",
|
|
48
|
+
email: "jane@example.com",
|
|
49
|
+
# optional: last_name, country, state, timezone_id, ip_address,
|
|
50
|
+
# phone_country_code, phone, twilio_consent
|
|
51
|
+
)
|
|
52
|
+
# => { status: "success", user: { user_id: ..., live_room_url: ..., ... } }
|
|
53
|
+
|
|
54
|
+
# Registrants / attendees for one session (paginated)
|
|
55
|
+
listing = client.registrants(webinar_id: 1, schedule_id: schedule, search: "jane@example.com")
|
|
56
|
+
# => { status: "success", registrants: { current_page: 1, data: [{ lead_id: ..., email: ..., ... }] } }
|
|
57
|
+
# optional filters: attended_live, attended_replay, purchased, page,
|
|
58
|
+
# attended_live_timestamp, attended_replay_timestamp, date_range
|
|
59
|
+
|
|
60
|
+
# Unsubscribe a lead (lead id comes from #registrants)
|
|
61
|
+
client.unsubscribe(webinar_id: 1, lead_id: 818)
|
|
62
|
+
# => true (the API responds 204 No Content)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
All responses are parsed JSON with symbolized keys.
|
|
66
|
+
|
|
67
|
+
### EverWebinar
|
|
68
|
+
|
|
69
|
+
The EverWebinar API shares the same shape under a different path prefix:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
client = WebinarJam::Client.new(product: :everwebinar)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Errors and rate limiting
|
|
76
|
+
|
|
77
|
+
Every API failure raises a subclass of `WebinarJam::Error`:
|
|
78
|
+
|
|
79
|
+
- `WebinarJam::ConfigurationError` — no API key provided.
|
|
80
|
+
- `WebinarJam::AuthenticationError` — HTTP 401/403 (invalid API key).
|
|
81
|
+
- `WebinarJam::RateLimitError` — HTTP 429. The API allows at most
|
|
82
|
+
**20 calls per second per user**.
|
|
83
|
+
- `WebinarJam::APIError` — any other error, including 2xx responses whose
|
|
84
|
+
body carries `"status": "error"`. Exposes `#http_status` and `#body`.
|
|
85
|
+
|
|
86
|
+
Opt into automatic retries on 429 (exponential backoff: 0.5s, 1s, 2s, ...):
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
client = WebinarJam::Client.new(max_retries: 3)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Development
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
bundle install
|
|
96
|
+
bundle exec rspec # unit specs + integration specs (VCR replay)
|
|
97
|
+
bundle exec rspec spec/webinarjam # unit specs only
|
|
98
|
+
bin/console # IRB with the gem and .env loaded
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Integration specs replay VCR cassettes committed under
|
|
102
|
+
`spec/fixtures/vcr_cassettes/` — no credentials needed. To re-record against
|
|
103
|
+
the live API, put a real key in `.env` (`WEBINARJAM_API_KEY=...`) and run:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
VCR_RECORD=all bundle exec rspec spec/integration
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The API key is filtered out of cassettes (`<API_KEY>`), but recorded bodies
|
|
110
|
+
still contain your account's webinar names and room URLs — review cassettes
|
|
111
|
+
before publishing them anywhere.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module WebinarJam
|
|
8
|
+
# HTTP client for the WebinarJam / EverWebinar API.
|
|
9
|
+
#
|
|
10
|
+
# Every endpoint is a POST with a form-encoded body and returns JSON,
|
|
11
|
+
# except +unsubscribe+ which returns 204 No Content.
|
|
12
|
+
class Client
|
|
13
|
+
BASE_URL = "https://api.webinarjam.com"
|
|
14
|
+
PRODUCTS = %i[webinarjam everwebinar].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :product, :max_retries
|
|
17
|
+
|
|
18
|
+
# @param api_key [String] account-wide API key (defaults to ENV["WEBINARJAM_API_KEY"])
|
|
19
|
+
# @param product [Symbol] :webinarjam or :everwebinar
|
|
20
|
+
# @param max_retries [Integer] automatic retries (with backoff) on HTTP 429
|
|
21
|
+
def initialize(api_key: ENV["WEBINARJAM_API_KEY"], product: :webinarjam,
|
|
22
|
+
base_url: BASE_URL, open_timeout: 10, read_timeout: 30,
|
|
23
|
+
max_retries: 0)
|
|
24
|
+
if api_key.nil? || api_key.to_s.empty?
|
|
25
|
+
raise ConfigurationError,
|
|
26
|
+
"api_key is required (pass api_key: or set ENV[\"WEBINARJAM_API_KEY\"])"
|
|
27
|
+
end
|
|
28
|
+
product = product.to_sym
|
|
29
|
+
raise ArgumentError, "product must be one of #{PRODUCTS.inspect}" unless PRODUCTS.include?(product)
|
|
30
|
+
|
|
31
|
+
@api_key = api_key
|
|
32
|
+
@product = product
|
|
33
|
+
@base_url = base_url
|
|
34
|
+
@open_timeout = open_timeout
|
|
35
|
+
@read_timeout = read_timeout
|
|
36
|
+
@max_retries = max_retries
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Retrieve the full list of webinars published in the account.
|
|
40
|
+
# @return [Hash] { status: "success", webinars: [...] }
|
|
41
|
+
def webinars
|
|
42
|
+
post("webinars")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get details about one particular webinar.
|
|
46
|
+
# @return [Hash] { status: "success", webinar: {...} }
|
|
47
|
+
def webinar(webinar_id)
|
|
48
|
+
post("webinar", webinar_id: webinar_id)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Register a user to a webinar.
|
|
52
|
+
#
|
|
53
|
+
# Optional params include: last_name, country, state, timezone_id,
|
|
54
|
+
# ip_address, phone_country_code, phone, twilio_consent.
|
|
55
|
+
# @return [Hash] { status: "success", user: {...} }
|
|
56
|
+
def register(webinar_id:, first_name:, email:, schedule:, **params)
|
|
57
|
+
post("register", params.merge(
|
|
58
|
+
webinar_id: webinar_id, first_name: first_name, email: email, schedule: schedule
|
|
59
|
+
))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Get a list of registrants and attendees for one webinar session.
|
|
63
|
+
#
|
|
64
|
+
# Optional params include: attended_live, attended_replay, purchased,
|
|
65
|
+
# page, attended_live_timestamp, attended_replay_timestamp, date_range,
|
|
66
|
+
# search.
|
|
67
|
+
# @return [Hash]
|
|
68
|
+
def registrants(webinar_id:, schedule_id:, **params)
|
|
69
|
+
post("registrants", params.merge(webinar_id: webinar_id, schedule_id: schedule_id))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Unsubscribe a lead from a webinar's notification queue.
|
|
73
|
+
# @return [true] the API responds with 204 No Content on success
|
|
74
|
+
def unsubscribe(webinar_id:, lead_id:)
|
|
75
|
+
post("unsubscribe", webinar_id: webinar_id, lead_id: lead_id)
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def post(path, params = {})
|
|
82
|
+
attempts = 0
|
|
83
|
+
begin
|
|
84
|
+
handle_response(perform_post(path, params))
|
|
85
|
+
rescue RateLimitError
|
|
86
|
+
raise if attempts >= @max_retries
|
|
87
|
+
|
|
88
|
+
attempts += 1
|
|
89
|
+
sleep(retry_backoff(attempts))
|
|
90
|
+
retry
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def perform_post(path, params)
|
|
95
|
+
uri = URI("#{@base_url}/#{@product}/#{path}")
|
|
96
|
+
request = Net::HTTP::Post.new(uri)
|
|
97
|
+
request.set_form_data(params.compact.merge(api_key: @api_key))
|
|
98
|
+
|
|
99
|
+
Net::HTTP.start(uri.host, uri.port,
|
|
100
|
+
use_ssl: uri.scheme == "https",
|
|
101
|
+
open_timeout: @open_timeout,
|
|
102
|
+
read_timeout: @read_timeout) do |http|
|
|
103
|
+
http.request(request)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def handle_response(response)
|
|
108
|
+
status = response.code.to_i
|
|
109
|
+
return nil if status == 204
|
|
110
|
+
|
|
111
|
+
body = parse_json(response.body)
|
|
112
|
+
|
|
113
|
+
if (200..299).cover?(status)
|
|
114
|
+
return body unless body.is_a?(Hash) && body[:status] == "error"
|
|
115
|
+
|
|
116
|
+
raise APIError.new(error_message(body, status), http_status: status, body: body)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
error_class = case status
|
|
120
|
+
when 401, 403 then AuthenticationError
|
|
121
|
+
when 429 then RateLimitError
|
|
122
|
+
else APIError
|
|
123
|
+
end
|
|
124
|
+
raise error_class.new(error_message(body, status), http_status: status, body: body)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def parse_json(raw)
|
|
128
|
+
JSON.parse(raw, symbolize_names: true)
|
|
129
|
+
rescue JSON::ParserError, TypeError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def error_message(body, status)
|
|
134
|
+
return "HTTP #{status}" unless body.is_a?(Hash)
|
|
135
|
+
|
|
136
|
+
errors = body[:errors]
|
|
137
|
+
detail = case errors
|
|
138
|
+
when Hash then errors.map { |field, msg| "#{field}: #{msg}" }.join("; ")
|
|
139
|
+
when Array then errors.join("; ")
|
|
140
|
+
else body[:message] || body[:error]
|
|
141
|
+
end
|
|
142
|
+
detail && !detail.empty? ? detail : "HTTP #{status}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def retry_backoff(attempt)
|
|
146
|
+
0.5 * (2**(attempt - 1))
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WebinarJam
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised when the client is instantiated without an API key.
|
|
7
|
+
class ConfigurationError < Error; end
|
|
8
|
+
|
|
9
|
+
# Raised when the API returns an error, either as a non-2xx HTTP status or
|
|
10
|
+
# as a 2xx response whose JSON body carries `"status": "error"`.
|
|
11
|
+
class APIError < Error
|
|
12
|
+
attr_reader :http_status, :body
|
|
13
|
+
|
|
14
|
+
def initialize(message, http_status: nil, body: nil)
|
|
15
|
+
@http_status = http_status
|
|
16
|
+
@body = body
|
|
17
|
+
super(message)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# HTTP 401/403 — invalid or missing API key.
|
|
22
|
+
class AuthenticationError < APIError; end
|
|
23
|
+
|
|
24
|
+
# HTTP 429 — the API allows at most 20 calls per second per user.
|
|
25
|
+
class RateLimitError < APIError; end
|
|
26
|
+
end
|
data/lib/webinarjam.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "webinarjam/version"
|
|
4
|
+
require_relative "webinarjam/errors"
|
|
5
|
+
require_relative "webinarjam/client"
|
|
6
|
+
|
|
7
|
+
module WebinarJam
|
|
8
|
+
class << self
|
|
9
|
+
attr_writer :api_key
|
|
10
|
+
|
|
11
|
+
def api_key
|
|
12
|
+
@api_key || ENV["WEBINARJAM_API_KEY"]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# WebinarJam.configure { |config| config.api_key = "..." }
|
|
16
|
+
def configure
|
|
17
|
+
yield self
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Convenience constructor honoring the module-level configuration.
|
|
22
|
+
def client(**options)
|
|
23
|
+
Client.new(api_key: api_key, **options)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: webinarjam
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yi-Ru Lin
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'A dependency-free Ruby client for the WebinarJam and EverWebinar HTTP
|
|
14
|
+
API: list webinars, fetch webinar details, register users, list registrants and
|
|
15
|
+
attendees, and unsubscribe leads.'
|
|
16
|
+
email:
|
|
17
|
+
- lawrence@kaik.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE.txt
|
|
24
|
+
- README.md
|
|
25
|
+
- lib/webinarjam.rb
|
|
26
|
+
- lib/webinarjam/client.rb
|
|
27
|
+
- lib/webinarjam/errors.rb
|
|
28
|
+
- lib/webinarjam/version.rb
|
|
29
|
+
homepage: https://github.com/linyiru/webinarjam
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
metadata:
|
|
33
|
+
homepage_uri: https://github.com/linyiru/webinarjam
|
|
34
|
+
source_code_uri: https://github.com/linyiru/webinarjam
|
|
35
|
+
changelog_uri: https://github.com/linyiru/webinarjam/blob/main/CHANGELOG.md
|
|
36
|
+
rubygems_mfa_required: 'true'
|
|
37
|
+
post_install_message:
|
|
38
|
+
rdoc_options: []
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '3.0'
|
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
51
|
+
requirements: []
|
|
52
|
+
rubygems_version: 3.5.22
|
|
53
|
+
signing_key:
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: Ruby client for the WebinarJam / EverWebinar API
|
|
56
|
+
test_files: []
|