websitescreenshotworld 0.1.3

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 18f107d8d939617295e29e83fdebb5fcbeb03f641b49df6017769b16169b8fec
4
+ data.tar.gz: 9b1ca9e8c2b2fa3f7312672e1b7628e97615f5601b87ce1fcc0a65d295da0a41
5
+ SHA512:
6
+ metadata.gz: 6bdeeeecca966e596712851b050965e7c07c379cbfbd266959e2d1c69874884f7cd85ca7662fce47b94da259dfc760c46cef51f9b00f4c1b75578f3632eb1fc3
7
+ data.tar.gz: de2bcee7717050479ecfcce4b600abf981758c522db692857991ef55fd873aecabf4ba5d9c33c4f435ac0af8b622627bd6661b80deab60fcc1eded5b5d9c110e
data/CHANGELOG.md ADDED
@@ -0,0 +1,59 @@
1
+ # Changelog
2
+
3
+ All notable changes to this gem are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the gem follows
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 0.1.3 - 2026-11-13
8
+
9
+ The first version on RubyGems. The library is the same as 0.1.0; 0.1.0 to 0.1.2 were tagged but never published.
10
+
11
+ ### Fixed
12
+
13
+ - Publishing: the release job names the owning RubyGems.org account when it exchanges its OIDC token, which
14
+ RubyGems.org requires before a trusted publisher may create a new gem.
15
+
16
+ ## 0.1.2 - 2026-11-13
17
+
18
+ Tagged but not published (see 0.1.3).
19
+
20
+ ### Fixed
21
+
22
+ - A test compared the User-Agent with a hard-coded 0.1.0, so the 0.1.1 release stopped at the test step. It now
23
+ reads the version from `WebsiteScreenshotWorld::VERSION`.
24
+
25
+ ## 0.1.1 - 2026-11-13
26
+
27
+ Tagged but not published (see 0.1.2).
28
+
29
+ ### Fixed
30
+
31
+ - Publishing: the release job builds and pushes the gem itself instead of running `rake release`, which tried to
32
+ create and push a `ruby-v` tag left over from the old multi-library repository.
33
+
34
+ ## 0.1.0 - 2026-11-09
35
+
36
+ The first release.
37
+
38
+ ### Added
39
+
40
+ - `WebsiteScreenshotWorld::Client`, one client for both products, with keys read from
41
+ `WSW_INSTANT_SNAP_API_KEY` and `WSW_AUTO_SCHEDULE_API_KEY` by default.
42
+ - Instant Snap: `capture`, `capture_file` (streamed, or buffered to a `Tempfile`), `capture_to_bucket`,
43
+ `get`, `download`, `defaults` and `catalog`, taking every capture option of the API as keyword arguments.
44
+ - Auto Schedule: `trigger_now`.
45
+ - The `press_escape` capture option, and the `sitemap_schedule_not_triggerable` error code (an
46
+ `InvalidRequestError`: a sitemap schedule runs only on its schedule), from API version 1.1.0.
47
+ - `WebsiteScreenshotWorld::Webhook.verify`, which checks the `X-WSW-Signature` header and returns a typed
48
+ event for each webhook type, or `UnknownEvent` for types added later.
49
+ - Immutable `Data` results with `Time` fields and the original JSON in `#raw`.
50
+ - One error class per kind of refusal under `WebsiteScreenshotWorld::Error`, plus `ConnectionError`.
51
+ - Automatic retries of rate limits, busy or unavailable services and connection failures, with a random
52
+ `Idempotency-Key` per capture so a retry never captures twice, and timeouts that cover the whole call, retries
53
+ included.
54
+ - Redirects are never followed, so the API key is never sent to another host; a 3xx answer is raised as an error.
55
+ - Async Instant Snap captures: `submit` and `submit_to_bucket` (answered at once with the capture
56
+ `queued`) and `wait`, which polls `get` until the capture has finished or its `timeout_seconds` run out;
57
+ the `timeout:` option of an async capture (30 to 300 seconds of browser time); the `CaptureStatus::QUEUED`
58
+ and `CaptureStatus::RUNNING` statuses; and the `queue_full` (`RateLimitError`, not retried) and
59
+ `capture_expired` (`CaptureError`) error codes.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Silhouettec Solutions Private Limited
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 ADDED
@@ -0,0 +1,371 @@
1
+ # Website Screenshot World for Ruby
2
+
3
+ The Ruby client for [Website Screenshot World](https://websitescreenshotworld.com): capture web pages as
4
+ images, videos and PDFs with **Instant Snap**, start scheduled captures with **Auto Schedule**, and verify
5
+ the webhooks both send.
6
+
7
+ It has no runtime dependencies beyond the Ruby standard library.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ bundle add websitescreenshotworld
13
+ ```
14
+
15
+ or `gem install websitescreenshotworld`.
16
+
17
+ ## Quick start
18
+
19
+ ### Instant Snap
20
+
21
+ ```ruby
22
+ require "websitescreenshotworld"
23
+
24
+ # Reads WSW_INSTANT_SNAP_API_KEY and WSW_AUTO_SCHEDULE_API_KEY from the environment.
25
+ client = WebsiteScreenshotWorld::Client.new
26
+
27
+ capture = client.instant_snap.capture(
28
+ url: "https://example.com",
29
+ format: "webp",
30
+ viewport: { width: 1440, height: 900 },
31
+ scroll_mode: "scroll",
32
+ block_ads: false
33
+ )
34
+
35
+ capture.id # => "3f6c2a1e-..."
36
+ capture.status # => "succeeded"
37
+ capture.captured_at # => 2026-09-26 10:30:04.512 UTC (a Time)
38
+ capture.download_url # valid for 60 minutes
39
+ ```
40
+
41
+ Options are keyword arguments with the API's own names; nested blocks are hashes. An option you leave out
42
+ is not sent and takes the server's default, while `false`, `0` and `""` are sent as given. The keys of
43
+ `headers:` and `cookies:` are sent exactly as you write them. Every option is listed in
44
+ `WebsiteScreenshotWorld::InstantSnap::CAPTURE_OPTIONS`, and a misspelt one raises `ArgumentError` before
45
+ anything is sent.
46
+
47
+ Constants name the known values of each token, for example `WebsiteScreenshotWorld::Format::WEBP` or
48
+ `WebsiteScreenshotWorld::CaptureStatus::SUCCEEDED`. Tokens are plain strings, so a value added to the API
49
+ later still comes through.
50
+
51
+ ### Auto Schedule
52
+
53
+ ```ruby
54
+ result = client.auto_schedule.trigger_now("7d4e2b1a-6c5f-4a3e-9d8c-2b1a0f9e8d7c")
55
+
56
+ result.status # => "triggered", or "already_pending" if a capture was already queued
57
+ result.triggered_at # => a Time
58
+ ```
59
+
60
+ The web page is set up in the dashboard. Its capture arrives later by [webhook](#webhooks), where the
61
+ same id is called `schedule_id`.
62
+
63
+ ## Saving the file with `capture_file`
64
+
65
+ `capture_file` answers with the file itself (up to 64 MB) and its metadata. With a block, the body streams
66
+ straight from the connection and is never held in memory:
67
+
68
+ ```ruby
69
+ client.instant_snap.capture_file(url: "https://example.com", format: "png") do |file|
70
+ file.filename # => "3f6c2a1e-....png"
71
+ file.width # => 1920
72
+ file.sha256 # => lowercase hex digest of the body
73
+ file.save("page.png")
74
+ end
75
+ ```
76
+
77
+ `save` also takes an open IO, such as a socket or an upload stream. `read` returns the bytes as a String
78
+ and `each_chunk` yields them as they arrive. The body can be read once.
79
+
80
+ Without a block, the body is buffered to a `Tempfile` first, so the file outlives the connection. Close it
81
+ when you are done to delete the temporary copy:
82
+
83
+ ```ruby
84
+ file = client.instant_snap.capture_file(url: "https://example.com")
85
+ begin
86
+ file.path # the Tempfile's path
87
+ file.save("page.png")
88
+ ensure
89
+ file.close
90
+ end
91
+ ```
92
+
93
+ ## Uploading to a bucket
94
+
95
+ `capture_to_bucket` uploads the file to your S3-compatible bucket instead of keeping it:
96
+
97
+ ```ruby
98
+ capture = client.instant_snap.capture_to_bucket(
99
+ url: "https://example.com",
100
+ s3_delivery: {
101
+ access_key_id: ENV.fetch("S3_ACCESS_KEY_ID"),
102
+ secret_access_key: ENV.fetch("S3_SECRET_ACCESS_KEY"),
103
+ bucket: "captures",
104
+ region: "eu-west-2",
105
+ key_prefix: "site/",
106
+ wait_for_sync: true
107
+ }
108
+ )
109
+
110
+ capture.s3_delivery.status # => "completed", "in_progress", ...
111
+ capture.s3_delivery.key # => the object key, once known
112
+ ```
113
+
114
+ Use `endpoint_url:` (and `force_path_style: true` where needed) for providers other than AWS.
115
+
116
+ ## Async captures
117
+
118
+ `submit` answers at once, before the capture runs, with the capture in status `queued`; it then goes
119
+ `running`, and ends `succeeded` or `failed`. `wait` reads it with `get` every `poll_interval_seconds`
120
+ (default 3) until it has finished, and returns it:
121
+
122
+ ```ruby
123
+ queued = client.instant_snap.submit(url: "https://example.com", format: "webp", timeout: 240)
124
+ queued.status # => "queued"
125
+
126
+ capture = client.instant_snap.wait(queued) # or wait(queued.id), with timeout_seconds: 360 by default
127
+ if capture.status == WebsiteScreenshotWorld::CaptureStatus::SUCCEEDED
128
+ client.instant_snap.download(capture, "page.webp")
129
+ end
130
+ ```
131
+
132
+ If `timeout_seconds` runs out first, `wait` returns the capture as last read, still `queued` or `running`,
133
+ rather than raising, so you can wait again. The `capture.succeeded` and `capture.failed` [webhooks](#webhooks)
134
+ tell you without polling.
135
+
136
+ `submit_to_bucket` is the async `capture_to_bucket`:
137
+
138
+ ```ruby
139
+ queued = client.instant_snap.submit_to_bucket(
140
+ url: "https://example.com",
141
+ s3_delivery: { access_key_id: "...", secret_access_key: "...", bucket: "captures", region: "eu-west-2" }
142
+ )
143
+ capture = client.instant_snap.wait(queued.id, poll_interval_seconds: 5)
144
+ capture.s3_delivery.status # => "completed", ...
145
+ ```
146
+
147
+ - On `submit` and `submit_to_bucket`, `timeout:` is the browser time the capture may use, 30 to 300 seconds
148
+ (the server uses 120 when you leave it out). The request itself is quick and uses the client's ordinary
149
+ `timeout` (30 seconds by default). On the other capture methods `timeout:` is the call's own timeout, as in
150
+ [Timeouts](#timeouts).
151
+ - A capture still queued 15 minutes after it was accepted ends `failed` with `capture_expired`, and is not
152
+ charged.
153
+ - When your organization already has as many async captures waiting or running as it may, `submit` raises
154
+ `RateLimitError` with `error_code` `queue_full` and `retry_after_seconds`. It is not retried, since a retry
155
+ would meet the same queue.
156
+
157
+ ## Reading a capture back
158
+
159
+ ```ruby
160
+ capture = client.instant_snap.get("3f6c2a1e-9b8d-4c7e-a5f4-1d2e3c4b5a69")
161
+
162
+ # get reissues download_url; download streams it to a path or an IO.
163
+ client.instant_snap.download(capture, "page.png")
164
+ client.instant_snap.download(capture.page_source, "page.html") if capture.page_source
165
+ ```
166
+
167
+ Files are kept for 24 hours (`capture.expires_at`). The API key is not sent to the download host.
168
+
169
+ `client.instant_snap.defaults` returns the values a capture takes for options it does not send (no key
170
+ needed), and `client.instant_snap.catalog` lists the accepted `emulated_device` and `proxy: { country: }`
171
+ values.
172
+
173
+ Every result is an immutable `Data` object. Dates are `Time` objects in UTC, and `raw` holds the JSON
174
+ object as the API sent it, including fields added after this version of the gem.
175
+
176
+ ## Errors and retries
177
+
178
+ Every error inherits from `WebsiteScreenshotWorld::Error`, which carries `error_code`, `message`,
179
+ `http_status`, `retry_after_seconds` and the raw response `body`. Switch on `error_code`, never on the
180
+ status or the message.
181
+
182
+ | Class | Raised for |
183
+ |---|---|
184
+ | `AuthenticationError` | `api_key_invalid`, or a key that was never given |
185
+ | `InvalidRequestError` | `validation_failed`, `s3_destination_invalid`, `web_page_id_invalid`, `idempotency_key_reused`, `sitemap_schedule_not_triggerable` |
186
+ | `PlanLimitError` | `quota_exhausted`, `feature_not_available`, `no_trigger_allowance`, `forbidden` |
187
+ | `NotFoundError` | `capture_not_found`, `web_page_not_found`, `not_found` |
188
+ | `RateLimitError` | `rate_limited`, `queue_full` |
189
+ | `CaptureError` | `capture_failed`, `capture_timeout`, `payload_too_large`, `capture_expired` |
190
+ | `ServiceError` | `capture_busy`, `service_unavailable`, `internal_error`, `request_cancelled`, `idempotency_key_in_use` |
191
+ | `ConnectionError` | no answer: the connection failed or broke, or the timeout ran out (see `cause`) |
192
+ | `SignatureVerificationError` | a webhook signature that does not verify |
193
+ | `Error` | a code this version does not know yet |
194
+
195
+ ```ruby
196
+ begin
197
+ client.instant_snap.capture(url: "https://example.com")
198
+ rescue WebsiteScreenshotWorld::CaptureError => e
199
+ # The website refused or was too slow. Nothing was charged.
200
+ warn "#{e.error_code}: #{e.message}"
201
+ rescue WebsiteScreenshotWorld::PlanLimitError => e
202
+ warn "Plan limit: #{e.error_code}"
203
+ rescue WebsiteScreenshotWorld::Error => e
204
+ warn "#{e.class}: #{e.message} (HTTP #{e.http_status})"
205
+ end
206
+ ```
207
+
208
+ `rate_limited`, `capture_busy`, `service_unavailable`, `idempotency_key_in_use` and connection failures
209
+ are retried up to twice before the error is raised. The wait is the `retry_after_seconds` the API sends
210
+ (or its `Retry-After` header), capped at 60 seconds, or else 0.5 and then 1 second with some random jitter.
211
+ Nothing else is retried, including `capture_failed`, `capture_timeout` and `queue_full`.
212
+
213
+ ## Idempotency keys
214
+
215
+ Every capture sends an `Idempotency-Key`, and its retries reuse it, so a retry never captures twice. The
216
+ gem makes a random one per call. Pass your own to make a call safe to repeat across processes, for
217
+ example from a background job:
218
+
219
+ ```ruby
220
+ capture = client.instant_snap.capture(url: "https://example.com", idempotency_key: "order-1234-hero")
221
+ capture.idempotent_replayed? # => true when this answers an earlier request with the same key
222
+ ```
223
+
224
+ A key is remembered for 24 hours. Reusing it with a different body, or after a `capture_file` call (whose
225
+ file is not kept), raises `InvalidRequestError` with `idempotency_key_reused`.
226
+
227
+ ## Timeouts
228
+
229
+ A capture may run for up to 120 seconds on the server, so capture calls and downloads wait up to 180
230
+ seconds by default and every other call, `submit` and `submit_to_bucket` included, 30 seconds. A timeout covers the whole call, retries included: a
231
+ wait that would run past it is not started, and the last error is raised at once.
232
+
233
+ ```ruby
234
+ client = WebsiteScreenshotWorld::Client.new(capture_timeout: 240, timeout: 10)
235
+
236
+ # Per call, on capture, capture_file, capture_to_bucket and download:
237
+ client.instant_snap.capture(url: "https://example.com", timeout: 60)
238
+ ```
239
+
240
+ Redirects are never followed, so the API key is never sent to another host. Neither the API nor a download link
241
+ redirects, so a `3xx` answer is raised as an error.
242
+
243
+ ## Webhooks
244
+
245
+ Deliveries are signed with the `X-WSW-Signature` header. `Webhook.verify` checks the signature and the
246
+ timestamp (within 300 seconds by default) and returns a typed event:
247
+
248
+ | `type` | Class | `data` |
249
+ |---|---|---|
250
+ | `capture.succeeded` | `CaptureSucceededEvent` | `Capture` (Instant Snap) or `ScheduledCapture` (Auto Schedule) |
251
+ | `capture.failed` | `CaptureFailedEvent` | `Capture` or `ScheduledCapture` |
252
+ | `delivery.completed` | `DeliveryCompletedEvent` | `ScheduledCapture` with `destinations` |
253
+ | `capture.changed` | `CaptureChangedEvent` | `ScheduledCapture` with `change_percent` |
254
+ | `schedule.paused` | `SchedulePausedEvent` | `SchedulePause` |
255
+ | `sitemap_run.completed` | `SitemapRunCompletedEvent` | `SitemapRun` |
256
+ | `ping` | `PingEvent` | `Ping` |
257
+ | anything newer | `UnknownEvent` | the raw `Hash` |
258
+
259
+ All of them inherit from `WebsiteScreenshotWorld::Event`, which has `id`, `type`, `created_at`, `product`
260
+ and `data`.
261
+
262
+ **Pass the body exactly as it arrived.** Parsing the JSON and serialising it again changes the bytes and the
263
+ signature no longer matches. Each framework below reads the raw body.
264
+
265
+ ### Rails
266
+
267
+ ```ruby
268
+ class WebsiteScreenshotWorldWebhooksController < ActionController::API
269
+ def create
270
+ event = WebsiteScreenshotWorld::Webhook.verify(
271
+ request.raw_post,
272
+ request.headers["X-WSW-Signature"],
273
+ ENV.fetch("WSW_WEBHOOK_SECRET")
274
+ )
275
+
276
+ case event
277
+ when WebsiteScreenshotWorld::CaptureSucceededEvent
278
+ Rails.logger.info("Capture #{event.data.id} succeeded")
279
+ when WebsiteScreenshotWorld::SchedulePausedEvent
280
+ Rails.logger.warn("Schedule #{event.data.schedule_name} paused: #{event.data.reason}")
281
+ end
282
+
283
+ head :ok
284
+ rescue WebsiteScreenshotWorld::SignatureVerificationError
285
+ head :bad_request
286
+ end
287
+ end
288
+ ```
289
+
290
+ ### Sinatra
291
+
292
+ ```ruby
293
+ post "/webhooks/websitescreenshotworld" do
294
+ request.body.rewind
295
+ event = WebsiteScreenshotWorld::Webhook.verify(
296
+ request.body.read,
297
+ request.env["HTTP_X_WSW_SIGNATURE"],
298
+ ENV.fetch("WSW_WEBHOOK_SECRET")
299
+ )
300
+ logger.info("Received #{event.type} #{event.id}")
301
+ 200
302
+ rescue WebsiteScreenshotWorld::SignatureVerificationError
303
+ 400
304
+ end
305
+ ```
306
+
307
+ ### Rack
308
+
309
+ ```ruby
310
+ class WebsiteScreenshotWorldWebhook
311
+ def call(env)
312
+ request = Rack::Request.new(env)
313
+ event = WebsiteScreenshotWorld::Webhook.verify(
314
+ request.body.read,
315
+ env["HTTP_X_WSW_SIGNATURE"],
316
+ ENV.fetch("WSW_WEBHOOK_SECRET")
317
+ )
318
+ warn "Received #{event.type} #{event.id}"
319
+ [200, {}, []]
320
+ rescue WebsiteScreenshotWorld::SignatureVerificationError
321
+ [400, {}, []]
322
+ end
323
+ end
324
+ ```
325
+
326
+ Events also work with pattern matching:
327
+
328
+ ```ruby
329
+ case event
330
+ in { type: "capture.changed", data: { change_percent:, url: } } if change_percent > 10
331
+ puts "#{url} changed by #{change_percent}%"
332
+ in WebsiteScreenshotWorld::UnknownEvent
333
+ # A type added after this version of the gem.
334
+ else
335
+ end
336
+ ```
337
+
338
+ ## Configuration
339
+
340
+ | Keyword for `Client.new` | Default |
341
+ |---|---|
342
+ | `instant_snap_api_key` | `ENV["WSW_INSTANT_SNAP_API_KEY"]` |
343
+ | `auto_schedule_api_key` | `ENV["WSW_AUTO_SCHEDULE_API_KEY"]` |
344
+ | `instant_snap_base_url` | `https://api.websitescreenshotworld.com` |
345
+ | `auto_schedule_base_url` | `https://autoapi.websitescreenshotworld.com` |
346
+ | `capture_timeout` | `180` seconds, for captures and downloads |
347
+ | `timeout` | `30` seconds, for everything else, including `submit` and `submit_to_bucket` |
348
+ | `max_retries` | `2` (so at most 3 attempts); `0` turns retrying off |
349
+
350
+ Either key may be left out. Calling a product whose key is missing raises `AuthenticationError` before
351
+ anything is sent. A client holds no open connection and is safe to share between threads.
352
+
353
+ Requests go through `Net::HTTP`, which honours the `https_proxy` and `no_proxy` environment variables.
354
+
355
+ ## Supported Ruby versions
356
+
357
+ Ruby 3.2 and later. CI runs on 3.2 and 3.4.
358
+
359
+ ## Development
360
+
361
+ The test suite plays the shared cases in `spec/` back through `spec/conformance/server.js`, so it needs
362
+ Node.js 22 or later. `spec/` is the [websitescreenshotworld-spec](https://github.com/websitescreenshotworld/websitescreenshotworld-spec) submodule: clone with `git clone --recurse-submodules`, or run `git submodule update --init` in an existing clone.
363
+
364
+ ```sh
365
+ bundle install
366
+ bundle exec rake # tests, then RuboCop
367
+ ```
368
+
369
+ ## License
370
+
371
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebsiteScreenshotWorld
4
+ # The Auto Schedule API: start a scheduled web page's capture now. Reach it through
5
+ # {Client#auto_schedule}. The capture's result arrives by webhook.
6
+ class AutoSchedule < Service
7
+ MISSING_KEY = "No Auto Schedule API key was given: pass auto_schedule_api_key: to " \
8
+ "WebsiteScreenshotWorld::Client.new or set WSW_AUTO_SCHEDULE_API_KEY."
9
+
10
+ # Queues a capture of a web page set up in the dashboard. Safe to repeat: while a capture is pending,
11
+ # another call answers +already_pending+ and charges nothing.
12
+ #
13
+ # @param web_page_id [String] the web page's id, the same id webhooks call +schedule_id+
14
+ # @return [TriggerResult]
15
+ # @raise [NotFoundError] when there is no such web page for this account
16
+ # @raise [Error] another subclass matching the refusal
17
+ def trigger_now(web_page_id)
18
+ hash, = request_json(:post, "/auto-schedule/v1/web-pages/#{path_segment(web_page_id)}/trigger-now")
19
+ TriggerResult.from_api(hash)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WebsiteScreenshotWorld
4
+ # The page source kept alongside a capture when +page_source:+ was requested.
5
+ #
6
+ # @!attribute [r] format
7
+ # @return [String] one of {PageSourceFormat}
8
+ # @!attribute [r] content_type
9
+ # @return [String, nil]
10
+ # @!attribute [r] size_bytes
11
+ # @return [Integer, nil]
12
+ # @!attribute [r] sha256
13
+ # @return [String, nil] lowercase hex digest of the source
14
+ # @!attribute [r] download_url
15
+ # @return [String, nil] short-lived link, reissued on every read; pass the object to
16
+ # {InstantSnap#download} to fetch it
17
+ PageSource = Data.define(:format, :content_type, :size_bytes, :sha256, :download_url) do
18
+ include Model::Raw
19
+
20
+ # @api private
21
+ def self.from_api(hash)
22
+ Model.build(self, hash)
23
+ end
24
+ end
25
+
26
+ # Where a +cloud_storage+ capture was uploaded.
27
+ #
28
+ # @!attribute [r] status
29
+ # @return [String] one of {S3DeliveryStatus}
30
+ # @!attribute [r] bucket
31
+ # @return [String]
32
+ # @!attribute [r] key
33
+ # @return [String, nil] the object key, once known
34
+ # @!attribute [r] access_key_id
35
+ # @return [String, nil] masked to its last four characters
36
+ # @!attribute [r] error
37
+ # @return [String, nil] why the upload failed
38
+ S3Delivery = Data.define(:status, :bucket, :key, :access_key_id, :error) do
39
+ include Model::Raw
40
+
41
+ # @api private
42
+ def self.from_api(hash)
43
+ Model.build(self, hash)
44
+ end
45
+ end
46
+
47
+ # An Instant Snap capture, as returned by {InstantSnap#capture}, {InstantSnap#capture_to_bucket},
48
+ # {InstantSnap#submit}, {InstantSnap#submit_to_bucket}, {InstantSnap#wait}, {InstantSnap#get} and the
49
+ # Instant Snap webhooks.
50
+ #
51
+ # @!attribute [r] id
52
+ # @return [String]
53
+ # @!attribute [r] status
54
+ # @return [String] one of {CaptureStatus}; an async capture is +queued+, then +running+, then
55
+ # +succeeded+ or +failed+, and its file fields are nil until it has finished
56
+ # @!attribute [r] response_type
57
+ # @return [String] one of {ResponseType}
58
+ # @!attribute [r] media_category
59
+ # @return [String, nil] one of {MediaCategory}; nil while an async capture is unfinished
60
+ # @!attribute [r] format
61
+ # @return [String, nil] the format written, one of {Format}
62
+ # @!attribute [r] content_type
63
+ # @return [String, nil]
64
+ # @!attribute [r] size_bytes
65
+ # @return [Integer, nil]
66
+ # @!attribute [r] width
67
+ # @return [Integer, nil]
68
+ # @!attribute [r] height
69
+ # @return [Integer, nil]
70
+ # @!attribute [r] duration_ms
71
+ # @return [Integer, nil] how long the capture took
72
+ # @!attribute [r] page_http_status
73
+ # @return [Integer, nil] the status the website answered with
74
+ # @!attribute [r] captured_at
75
+ # @return [Time, nil]
76
+ # @!attribute [r] final_url
77
+ # @return [String, nil] where the browser ended up, when the page redirected
78
+ # @!attribute [r] media_sha256
79
+ # @return [String, nil] lowercase hex digest of the file
80
+ # @!attribute [r] download_url
81
+ # @return [String, nil] valid for 60 minutes and reissued by {InstantSnap#get}; nil for +by_format+,
82
+ # +cloud_storage+ and webhooks
83
+ # @!attribute [r] expires_at
84
+ # @return [Time, nil] when the file is deleted
85
+ # @!attribute [r] page_source
86
+ # @return [PageSource, nil]
87
+ # @!attribute [r] s3_delivery
88
+ # @return [S3Delivery, nil] present for +cloud_storage+ only
89
+ # @!attribute [r] callback_url
90
+ # @return [String, nil]
91
+ # @!attribute [r] error_code
92
+ # @return [String, nil] why a +failed+ or +timed_out+ capture ended
93
+ # @!attribute [r] spec_version
94
+ # @return [String] the default set the capture was taken under
95
+ Capture = Data.define(
96
+ :id, :status, :response_type, :media_category, :format, :content_type, :size_bytes, :width, :height,
97
+ :duration_ms, :page_http_status, :captured_at, :final_url, :media_sha256, :download_url, :expires_at,
98
+ :page_source, :s3_delivery, :callback_url, :error_code, :spec_version
99
+ ) do
100
+ include Model::Raw
101
+
102
+ def initialize(idempotent_replayed: false, **fields)
103
+ @idempotent_replayed = idempotent_replayed
104
+ super(**fields)
105
+ end
106
+
107
+ # @return [Boolean] whether this answer replays an earlier request made with the same idempotency key
108
+ def idempotent_replayed?
109
+ @idempotent_replayed
110
+ end
111
+
112
+ # @api private
113
+ def self.from_api(hash, idempotent_replayed: false)
114
+ Model.build(
115
+ self, hash,
116
+ idempotent_replayed: idempotent_replayed,
117
+ captured_at: Model.time(hash["captured_at"]),
118
+ expires_at: Model.time(hash["expires_at"]),
119
+ page_source: hash["page_source"]&.then { PageSource.from_api(_1) },
120
+ s3_delivery: hash["s3_delivery"]&.then { S3Delivery.from_api(_1) }
121
+ )
122
+ end
123
+ end
124
+ end