actionwebpush 0.1.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/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +256 -0
- data/LICENSE.txt +21 -0
- data/README.md +569 -0
- data/Rakefile +17 -0
- data/app/controllers/actionwebpush/subscriptions_controller.rb +60 -0
- data/app/models/actionwebpush/subscription.rb +164 -0
- data/config/routes.rb +5 -0
- data/db/migrate/001_create_action_web_push_subscriptions.rb +17 -0
- data/lib/actionwebpush/analytics.rb +197 -0
- data/lib/actionwebpush/authorization.rb +236 -0
- data/lib/actionwebpush/base.rb +107 -0
- data/lib/actionwebpush/batch_delivery.rb +92 -0
- data/lib/actionwebpush/configuration.rb +91 -0
- data/lib/actionwebpush/delivery_job.rb +52 -0
- data/lib/actionwebpush/delivery_methods/base.rb +19 -0
- data/lib/actionwebpush/delivery_methods/test.rb +36 -0
- data/lib/actionwebpush/delivery_methods/web_push.rb +74 -0
- data/lib/actionwebpush/engine.rb +20 -0
- data/lib/actionwebpush/error_handler.rb +99 -0
- data/lib/actionwebpush/generators/campfire_migration_generator.rb +69 -0
- data/lib/actionwebpush/generators/install_generator.rb +47 -0
- data/lib/actionwebpush/generators/templates/campfire_compatibility.rb +173 -0
- data/lib/actionwebpush/generators/templates/campfire_data_migration.rb +98 -0
- data/lib/actionwebpush/generators/templates/create_action_web_push_subscriptions.rb +17 -0
- data/lib/actionwebpush/generators/templates/initializer.rb +16 -0
- data/lib/actionwebpush/generators/vapid_keys_generator.rb +38 -0
- data/lib/actionwebpush/instrumentation.rb +31 -0
- data/lib/actionwebpush/logging.rb +38 -0
- data/lib/actionwebpush/metrics.rb +67 -0
- data/lib/actionwebpush/notification.rb +97 -0
- data/lib/actionwebpush/pool.rb +167 -0
- data/lib/actionwebpush/railtie.rb +48 -0
- data/lib/actionwebpush/rate_limiter.rb +167 -0
- data/lib/actionwebpush/sentry_integration.rb +104 -0
- data/lib/actionwebpush/status_broadcaster.rb +62 -0
- data/lib/actionwebpush/status_channel.rb +21 -0
- data/lib/actionwebpush/tenant_configuration.rb +106 -0
- data/lib/actionwebpush/test_helper.rb +68 -0
- data/lib/actionwebpush/version.rb +5 -0
- data/lib/actionwebpush.rb +78 -0
- data/sig/actionwebpush.rbs +4 -0
- metadata +212 -0
data/README.md
ADDED
@@ -0,0 +1,569 @@
|
|
1
|
+
# ActionWebPush
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/actionwebpush)
|
4
|
+
[](https://github.com/your-org/actionwebpush/actions)
|
5
|
+
|
6
|
+
Rails integration for Web Push notifications with ActionMailer-like interface.
|
7
|
+
|
8
|
+
ActionWebPush provides a comprehensive solution for sending Web Push notifications in Rails applications. It offers an ActionMailer-inspired API, background job integration, rate limiting, and sophisticated error handling.
|
9
|
+
|
10
|
+
Extracted from the [Campfire](https://github.com/your-org/once-campfire) project and designed for production use.
|
11
|
+
|
12
|
+
## Features
|
13
|
+
|
14
|
+
- ๐ **ActionMailer-like Interface** - Familiar Rails patterns for sending notifications
|
15
|
+
- ๐ง **Easy Configuration** - Simple setup with VAPID keys
|
16
|
+
- ๐ฏ **Background Jobs** - ActiveJob integration for async delivery
|
17
|
+
- ๐ **Rate Limiting** - Built-in protection against abuse
|
18
|
+
- ๐ **Thread Pool Management** - Efficient concurrent delivery
|
19
|
+
- ๐ **Instrumentation** - ActiveSupport::Notifications integration
|
20
|
+
- ๐ก๏ธ **Error Handling** - Comprehensive error management with cleanup
|
21
|
+
- ๐๏ธ **ActiveRecord Integration** - Models and migrations included
|
22
|
+
- ๐งช **Test Helpers** - Testing utilities for development
|
23
|
+
- ๐ **Detailed Logging** - Structured logging for debugging
|
24
|
+
|
25
|
+
## Table of Contents
|
26
|
+
|
27
|
+
- [Installation](#installation)
|
28
|
+
- [Configuration](#configuration)
|
29
|
+
- [Quick Start](#quick-start)
|
30
|
+
- [Usage](#usage)
|
31
|
+
- [Basic Notifications](#basic-notifications)
|
32
|
+
- [ActionMailer-like Senders](#actionmailer-like-senders)
|
33
|
+
- [Background Delivery](#background-delivery)
|
34
|
+
- [Batch Operations](#batch-operations)
|
35
|
+
- [Subscription Management](#subscription-management)
|
36
|
+
- [Rate Limiting](#rate-limiting)
|
37
|
+
- [Error Handling](#error-handling)
|
38
|
+
- [Monitoring](#monitoring)
|
39
|
+
- [Testing](#testing)
|
40
|
+
- [Configuration Reference](#configuration-reference)
|
41
|
+
- [API Documentation](#api-documentation)
|
42
|
+
- [Contributing](#contributing)
|
43
|
+
- [License](#license)
|
44
|
+
|
45
|
+
## Installation
|
46
|
+
|
47
|
+
Add this line to your application's Gemfile:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
gem 'actionwebpush'
|
51
|
+
```
|
52
|
+
|
53
|
+
And then execute:
|
54
|
+
|
55
|
+
```bash
|
56
|
+
bundle install
|
57
|
+
```
|
58
|
+
|
59
|
+
Run the installation generator:
|
60
|
+
|
61
|
+
```bash
|
62
|
+
rails generate action_web_push:install
|
63
|
+
```
|
64
|
+
|
65
|
+
This will:
|
66
|
+
- Create configuration file `config/initializers/action_web_push.rb`
|
67
|
+
- Generate database migrations for subscription management
|
68
|
+
- Create VAPID keys if not present
|
69
|
+
|
70
|
+
Run the migrations:
|
71
|
+
|
72
|
+
```bash
|
73
|
+
rails db:migrate
|
74
|
+
```
|
75
|
+
|
76
|
+
## Configuration
|
77
|
+
|
78
|
+
Configure ActionWebPush in `config/initializers/action_web_push.rb`:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
ActionWebPush.configure do |config|
|
82
|
+
# Required: VAPID keys for push service authentication
|
83
|
+
config.vapid_public_key = ENV['VAPID_PUBLIC_KEY']
|
84
|
+
config.vapid_private_key = ENV['VAPID_PRIVATE_KEY']
|
85
|
+
config.vapid_subject = 'mailto:support@yourapp.com'
|
86
|
+
|
87
|
+
# Optional: Performance tuning
|
88
|
+
config.pool_size = 10 # Thread pool size
|
89
|
+
config.queue_size = 100 # Queue size
|
90
|
+
config.connection_pool_size = 5 # HTTP connection pool
|
91
|
+
config.batch_size = 100 # Default batch size
|
92
|
+
|
93
|
+
# Optional: Delivery method (default: :web_push)
|
94
|
+
config.delivery_method = :web_push
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
### Generate VAPID Keys
|
99
|
+
|
100
|
+
If you don't have VAPID keys, generate them:
|
101
|
+
|
102
|
+
```bash
|
103
|
+
rails generate action_web_push:vapid_keys
|
104
|
+
```
|
105
|
+
|
106
|
+
This creates a `.env` file with your keys. Add them to your production environment:
|
107
|
+
|
108
|
+
```bash
|
109
|
+
# .env
|
110
|
+
VAPID_PUBLIC_KEY=your_generated_public_key
|
111
|
+
VAPID_PRIVATE_KEY=your_generated_private_key
|
112
|
+
```
|
113
|
+
|
114
|
+
## Quick Start
|
115
|
+
|
116
|
+
### 1. Set up User Associations
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
class User < ApplicationRecord
|
120
|
+
has_many :push_subscriptions,
|
121
|
+
class_name: "ActionWebPush::Subscription",
|
122
|
+
foreign_key: :user_id,
|
123
|
+
dependent: :destroy
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
### 2. Create Notification Senders
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
class UserNotifications < ActionWebPush::Base
|
131
|
+
def welcome(user)
|
132
|
+
web_push(
|
133
|
+
title: "Welcome to MyApp!",
|
134
|
+
body: "Thanks for joining us",
|
135
|
+
to: user.push_subscriptions,
|
136
|
+
data: { type: 'welcome', url: '/dashboard' }
|
137
|
+
)
|
138
|
+
end
|
139
|
+
|
140
|
+
def new_message(user, message)
|
141
|
+
web_push(
|
142
|
+
title: "New Message",
|
143
|
+
body: message.preview,
|
144
|
+
to: user.push_subscriptions,
|
145
|
+
data: {
|
146
|
+
type: 'message',
|
147
|
+
message_id: message.id,
|
148
|
+
url: message_path(message)
|
149
|
+
}
|
150
|
+
)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
```
|
154
|
+
|
155
|
+
### 3. Send Notifications
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
# Deliver immediately
|
159
|
+
UserNotifications.welcome(user).deliver_now
|
160
|
+
|
161
|
+
# Deliver via background job (recommended)
|
162
|
+
UserNotifications.new_message(user, message).deliver_later
|
163
|
+
|
164
|
+
# Batch delivery to multiple users
|
165
|
+
users = User.active.includes(:push_subscriptions)
|
166
|
+
users.each do |user|
|
167
|
+
UserNotifications.welcome(user).deliver_later
|
168
|
+
end
|
169
|
+
```
|
170
|
+
|
171
|
+
## Usage
|
172
|
+
|
173
|
+
### Basic Notifications
|
174
|
+
|
175
|
+
Create and send notifications directly:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
notification = ActionWebPush::Notification.new(
|
179
|
+
title: "System Alert",
|
180
|
+
body: "Maintenance scheduled for tonight",
|
181
|
+
endpoint: subscription.endpoint,
|
182
|
+
p256dh_key: subscription.p256dh_key,
|
183
|
+
auth_key: subscription.auth_key,
|
184
|
+
data: { type: 'maintenance', scheduled_at: '2024-01-01T02:00:00Z' },
|
185
|
+
icon: '/system-icon.png',
|
186
|
+
badge: '/badge.png',
|
187
|
+
urgency: 'high'
|
188
|
+
)
|
189
|
+
|
190
|
+
# Synchronous delivery
|
191
|
+
notification.deliver_now
|
192
|
+
|
193
|
+
# Asynchronous delivery
|
194
|
+
notification.deliver_later
|
195
|
+
```
|
196
|
+
|
197
|
+
### ActionMailer-like Senders
|
198
|
+
|
199
|
+
Create notification classes that inherit from `ActionWebPush::Base`:
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
class SystemNotifications < ActionWebPush::Base
|
203
|
+
default data: { app: 'MyApp' }
|
204
|
+
|
205
|
+
def maintenance_notice(users, maintenance)
|
206
|
+
web_push(
|
207
|
+
title: "Scheduled Maintenance",
|
208
|
+
body: "Service will be unavailable #{maintenance.start_time}",
|
209
|
+
to: users.flat_map(&:push_subscriptions),
|
210
|
+
data: {
|
211
|
+
type: 'maintenance',
|
212
|
+
start_time: maintenance.start_time.iso8601,
|
213
|
+
duration: maintenance.duration_minutes,
|
214
|
+
url: maintenance_path(maintenance)
|
215
|
+
},
|
216
|
+
urgency: 'high'
|
217
|
+
)
|
218
|
+
end
|
219
|
+
|
220
|
+
def feature_announcement(user, feature)
|
221
|
+
web_push(
|
222
|
+
title: "New Feature Available!",
|
223
|
+
body: feature.description,
|
224
|
+
to: user.push_subscriptions,
|
225
|
+
data: {
|
226
|
+
type: 'feature',
|
227
|
+
feature_id: feature.id,
|
228
|
+
url: feature_path(feature)
|
229
|
+
},
|
230
|
+
icon: feature.icon_url
|
231
|
+
)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
### Background Delivery
|
237
|
+
|
238
|
+
Leverage ActiveJob for asynchronous delivery:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
# Basic background delivery
|
242
|
+
notification.deliver_later
|
243
|
+
|
244
|
+
# With scheduling
|
245
|
+
notification.deliver_later(wait: 1.hour)
|
246
|
+
notification.deliver_later(wait_until: Date.tomorrow.noon)
|
247
|
+
|
248
|
+
# Custom queue and priority
|
249
|
+
notification.deliver_later(
|
250
|
+
queue: :critical_notifications,
|
251
|
+
priority: 10
|
252
|
+
)
|
253
|
+
|
254
|
+
# ActionMailer-like senders
|
255
|
+
UserNotifications.welcome(user).deliver_later(wait: 5.minutes)
|
256
|
+
```
|
257
|
+
|
258
|
+
### Batch Operations
|
259
|
+
|
260
|
+
Efficiently send to multiple recipients:
|
261
|
+
|
262
|
+
```ruby
|
263
|
+
# Using BatchDelivery for performance
|
264
|
+
notifications = users.map do |user|
|
265
|
+
user.push_subscriptions.map do |subscription|
|
266
|
+
subscription.build_notification(
|
267
|
+
title: "Weekly Update",
|
268
|
+
body: "Check out this week's highlights",
|
269
|
+
data: { type: 'weekly_update' }
|
270
|
+
)
|
271
|
+
end
|
272
|
+
end.flatten
|
273
|
+
|
274
|
+
ActionWebPush::BatchDelivery.deliver(notifications)
|
275
|
+
|
276
|
+
# With custom batch size
|
277
|
+
ActionWebPush::BatchDelivery.new(notifications, batch_size: 50).deliver_all
|
278
|
+
|
279
|
+
# Using ActionMailer-like pattern for batches
|
280
|
+
users.find_each do |user|
|
281
|
+
WeeklyNotifications.digest(user).deliver_later
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
## Subscription Management
|
286
|
+
|
287
|
+
ActionWebPush includes ActiveRecord models for managing subscriptions:
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
# Create subscription from frontend
|
291
|
+
subscription = ActionWebPush::Subscription.create!(
|
292
|
+
endpoint: params[:endpoint],
|
293
|
+
p256dh_key: params[:keys][:p256dh],
|
294
|
+
auth_key: params[:keys][:auth],
|
295
|
+
user: current_user,
|
296
|
+
user_agent: request.user_agent
|
297
|
+
)
|
298
|
+
|
299
|
+
# Find subscriptions
|
300
|
+
user_subscriptions = ActionWebPush::Subscription.for_user(current_user)
|
301
|
+
active_subscriptions = ActionWebPush::Subscription.active
|
302
|
+
mobile_subscriptions = ActionWebPush::Subscription.by_user_agent('Mobile')
|
303
|
+
|
304
|
+
# Build notifications from subscriptions
|
305
|
+
notification = subscription.build_notification(
|
306
|
+
title: "Hello",
|
307
|
+
body: "World",
|
308
|
+
data: { url: '/dashboard' }
|
309
|
+
)
|
310
|
+
|
311
|
+
# Cleanup expired subscriptions
|
312
|
+
ActionWebPush::Subscription.expired.destroy_all
|
313
|
+
```
|
314
|
+
|
315
|
+
### Frontend Integration
|
316
|
+
|
317
|
+
Example JavaScript for subscription management:
|
318
|
+
|
319
|
+
```javascript
|
320
|
+
// Register service worker and get subscription
|
321
|
+
navigator.serviceWorker.register('/sw.js').then(registration => {
|
322
|
+
return registration.pushManager.subscribe({
|
323
|
+
userVisibleOnly: true,
|
324
|
+
applicationServerKey: '<%= ActionWebPush.config.vapid_public_key %>'
|
325
|
+
});
|
326
|
+
}).then(subscription => {
|
327
|
+
// Send subscription to your Rails app
|
328
|
+
fetch('/push_subscriptions', {
|
329
|
+
method: 'POST',
|
330
|
+
headers: {
|
331
|
+
'Content-Type': 'application/json',
|
332
|
+
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
333
|
+
},
|
334
|
+
body: JSON.stringify({
|
335
|
+
subscription: {
|
336
|
+
endpoint: subscription.endpoint,
|
337
|
+
keys: {
|
338
|
+
p256dh: arrayBufferToBase64(subscription.getKey('p256dh')),
|
339
|
+
auth: arrayBufferToBase64(subscription.getKey('auth'))
|
340
|
+
}
|
341
|
+
}
|
342
|
+
})
|
343
|
+
});
|
344
|
+
});
|
345
|
+
```
|
346
|
+
|
347
|
+
## Rate Limiting
|
348
|
+
|
349
|
+
Protect your application from abuse with built-in rate limiting:
|
350
|
+
|
351
|
+
```ruby
|
352
|
+
# Configure rate limits
|
353
|
+
rate_limiter = ActionWebPush::RateLimiter.new(
|
354
|
+
limits: {
|
355
|
+
endpoint: { max_requests: 100, window: 3600 }, # 100/hour per endpoint
|
356
|
+
user: { max_requests: 1000, window: 3600 }, # 1000/hour per user
|
357
|
+
subscription: { max_requests: 50, window: 3600 } # 50/hour per subscription
|
358
|
+
}
|
359
|
+
)
|
360
|
+
|
361
|
+
# Check before sending
|
362
|
+
begin
|
363
|
+
rate_limiter.check_rate_limit!(:user, current_user.id)
|
364
|
+
notification.deliver_now
|
365
|
+
rescue ActionWebPush::RateLimitExceeded => e
|
366
|
+
render json: { error: "Rate limit exceeded" }, status: 429
|
367
|
+
end
|
368
|
+
|
369
|
+
# Get rate limit information
|
370
|
+
info = rate_limiter.rate_limit_info(:user, current_user.id)
|
371
|
+
# => { limit: 1000, remaining: 950, window: 3600, reset_at: Time }
|
372
|
+
```
|
373
|
+
|
374
|
+
## Error Handling
|
375
|
+
|
376
|
+
ActionWebPush provides comprehensive error handling:
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
begin
|
380
|
+
notification.deliver_now
|
381
|
+
rescue ActionWebPush::ExpiredSubscriptionError => e
|
382
|
+
# Subscription is no longer valid - cleanup
|
383
|
+
subscription.destroy
|
384
|
+
rescue ActionWebPush::RateLimitExceeded => e
|
385
|
+
# Rate limit hit - retry later
|
386
|
+
notification.deliver_later(wait: 1.hour)
|
387
|
+
rescue ActionWebPush::DeliveryError => e
|
388
|
+
# Delivery failed - log and handle
|
389
|
+
Rails.logger.error "Push notification failed: #{e.message}"
|
390
|
+
rescue ActionWebPush::ConfigurationError => e
|
391
|
+
# Configuration issue - check VAPID keys
|
392
|
+
Rails.logger.error "Push configuration error: #{e.message}"
|
393
|
+
end
|
394
|
+
```
|
395
|
+
|
396
|
+
All errors include detailed context and are automatically instrumented for monitoring.
|
397
|
+
|
398
|
+
## Monitoring
|
399
|
+
|
400
|
+
ActionWebPush integrates with ActiveSupport::Notifications for comprehensive monitoring:
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
# Subscribe to all ActionWebPush events
|
404
|
+
ActiveSupport::Notifications.subscribe(/^action_web_push\./) do |name, start, finish, id, payload|
|
405
|
+
duration = (finish - start) * 1000
|
406
|
+
Rails.logger.info "#{name}: #{duration.round(2)}ms #{payload.inspect}"
|
407
|
+
end
|
408
|
+
|
409
|
+
# Monitor specific events
|
410
|
+
ActiveSupport::Notifications.subscribe("action_web_push.notification_delivery") do |name, start, finish, id, payload|
|
411
|
+
if payload[:success]
|
412
|
+
Metrics.increment('push.delivery.success')
|
413
|
+
else
|
414
|
+
Metrics.increment('push.delivery.failure', tags: { code: payload[:response_code] })
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
# Track rate limiting
|
419
|
+
ActiveSupport::Notifications.subscribe("action_web_push.rate_limit_exceeded") do |name, start, finish, id, payload|
|
420
|
+
Metrics.increment('push.rate_limit_exceeded',
|
421
|
+
tags: { resource_type: payload[:resource_type] })
|
422
|
+
end
|
423
|
+
|
424
|
+
# Pool overflow monitoring
|
425
|
+
ActiveSupport::Notifications.subscribe("action_web_push.pool_overflow") do |name, start, finish, id, payload|
|
426
|
+
Metrics.gauge('push.pool.overflow_rate', payload[:overflow_rate])
|
427
|
+
end
|
428
|
+
```
|
429
|
+
|
430
|
+
### Available Events
|
431
|
+
|
432
|
+
- `action_web_push.notification_delivery` - Individual notification delivery
|
433
|
+
- `action_web_push.subscription_expired` - Subscription marked as expired
|
434
|
+
- `action_web_push.rate_limit_exceeded` - Rate limit threshold hit
|
435
|
+
- `action_web_push.pool_overflow` - Thread pool queue overflow
|
436
|
+
- `action_web_push.notification_delivery_failed` - Delivery failure
|
437
|
+
- `action_web_push.configuration_error` - Configuration validation error
|
438
|
+
- `action_web_push.unexpected_error` - Unexpected error occurred
|
439
|
+
|
440
|
+
## Testing
|
441
|
+
|
442
|
+
ActionWebPush includes comprehensive test helpers:
|
443
|
+
|
444
|
+
```ruby
|
445
|
+
# Test mode (add to test environment)
|
446
|
+
ActionWebPush.configure do |config|
|
447
|
+
config.delivery_method = :test
|
448
|
+
end
|
449
|
+
|
450
|
+
# In your tests
|
451
|
+
require 'action_web_push/test_helper'
|
452
|
+
|
453
|
+
class NotificationTest < ActiveSupport::TestCase
|
454
|
+
include ActionWebPush::TestHelper
|
455
|
+
|
456
|
+
test "sends welcome notification" do
|
457
|
+
user = users(:alice)
|
458
|
+
|
459
|
+
assert_enqueued_push_deliveries 1 do
|
460
|
+
UserNotifications.welcome(user).deliver_later
|
461
|
+
end
|
462
|
+
|
463
|
+
assert_push_delivered_to user.push_subscriptions.first do |notification|
|
464
|
+
assert_equal "Welcome!", notification[:title]
|
465
|
+
assert_equal "welcome", notification[:data][:type]
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
test "handles expired subscriptions" do
|
470
|
+
expired_subscription = push_subscriptions(:expired)
|
471
|
+
|
472
|
+
assert_raises ActionWebPush::ExpiredSubscriptionError do
|
473
|
+
notification = expired_subscription.build_notification(
|
474
|
+
title: "Test", body: "Test"
|
475
|
+
)
|
476
|
+
notification.deliver_now
|
477
|
+
end
|
478
|
+
end
|
479
|
+
end
|
480
|
+
```
|
481
|
+
|
482
|
+
### Test Helpers
|
483
|
+
|
484
|
+
- `assert_push_delivered_to(subscription, &block)` - Assert notification delivered
|
485
|
+
- `assert_enqueued_push_deliveries(count, &block)` - Assert jobs enqueued
|
486
|
+
- `assert_no_push_deliveries(&block)` - Assert no deliveries
|
487
|
+
- `clear_push_deliveries` - Clear test delivery queue
|
488
|
+
|
489
|
+
## Configuration Reference
|
490
|
+
|
491
|
+
Complete configuration options:
|
492
|
+
|
493
|
+
```ruby
|
494
|
+
ActionWebPush.configure do |config|
|
495
|
+
# VAPID Configuration (Required)
|
496
|
+
config.vapid_subject = "mailto:admin@example.com" # or "https://example.com"
|
497
|
+
config.vapid_public_key = "your_public_key"
|
498
|
+
config.vapid_private_key = "your_private_key"
|
499
|
+
|
500
|
+
# Delivery Method
|
501
|
+
config.delivery_method = :web_push # :web_push, :test, or custom
|
502
|
+
|
503
|
+
# Thread Pool Configuration
|
504
|
+
config.pool_size = 10 # Max concurrent deliveries
|
505
|
+
config.queue_size = 100 # Queue size before overflow
|
506
|
+
config.connection_pool_size = 5 # HTTP connection pool size
|
507
|
+
|
508
|
+
# Batch Processing
|
509
|
+
config.batch_size = 100 # Default batch size
|
510
|
+
|
511
|
+
# Rate Limiting (optional - uses sensible defaults)
|
512
|
+
config.rate_limits = {
|
513
|
+
endpoint: { max_requests: 100, window: 3600 },
|
514
|
+
user: { max_requests: 1000, window: 3600 },
|
515
|
+
global: { max_requests: 10000, window: 3600 },
|
516
|
+
subscription: { max_requests: 50, window: 3600 }
|
517
|
+
}
|
518
|
+
|
519
|
+
# Logging
|
520
|
+
config.logger = Rails.logger # Custom logger
|
521
|
+
end
|
522
|
+
```
|
523
|
+
|
524
|
+
## API Documentation
|
525
|
+
|
526
|
+
For detailed API documentation, see [API.md](API.md).
|
527
|
+
|
528
|
+
## Requirements
|
529
|
+
|
530
|
+
- Ruby 3.0+
|
531
|
+
- Rails 7.0+
|
532
|
+
- Redis (optional, for rate limiting)
|
533
|
+
|
534
|
+
## Dependencies
|
535
|
+
|
536
|
+
- `web-push` - Web Push protocol implementation
|
537
|
+
- `concurrent-ruby` - Thread pool management
|
538
|
+
- `activejob` - Background job processing
|
539
|
+
- `activerecord` - Database integration
|
540
|
+
|
541
|
+
## Contributing
|
542
|
+
|
543
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
544
|
+
|
545
|
+
1. Fork the repository
|
546
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
547
|
+
3. Write tests for your changes
|
548
|
+
4. Ensure all tests pass (`bundle exec rspec`)
|
549
|
+
5. Commit your changes (`git commit -am 'Add some feature'`)
|
550
|
+
6. Push to the branch (`git push origin my-new-feature`)
|
551
|
+
7. Create a Pull Request
|
552
|
+
|
553
|
+
## Changelog
|
554
|
+
|
555
|
+
See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
|
556
|
+
|
557
|
+
## License
|
558
|
+
|
559
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
560
|
+
|
561
|
+
## Security
|
562
|
+
|
563
|
+
For security issues, please email security@yourapp.com instead of using the issue tracker.
|
564
|
+
|
565
|
+
## Support
|
566
|
+
|
567
|
+
- GitHub Issues: Report bugs and feature requests
|
568
|
+
- Documentation: [API.md](API.md)
|
569
|
+
- Examples: See `examples/` directory
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList[
|
10
|
+
"test/basic_test.rb",
|
11
|
+
"test/comprehensive_test.rb",
|
12
|
+
"test/delivery_methods_test.rb"
|
13
|
+
]
|
14
|
+
t.verbose = true
|
15
|
+
end
|
16
|
+
|
17
|
+
task default: :test
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionWebPush
|
4
|
+
class SubscriptionsController < ActionController::Base
|
5
|
+
before_action :authenticate_user!, if: :respond_to_authenticate_user?
|
6
|
+
before_action :set_user
|
7
|
+
before_action :set_subscription, only: [:show, :destroy]
|
8
|
+
|
9
|
+
def index
|
10
|
+
@subscriptions = ActionWebPush::Subscription.for_user(@user)
|
11
|
+
render json: @subscriptions
|
12
|
+
end
|
13
|
+
|
14
|
+
def show
|
15
|
+
render json: @subscription
|
16
|
+
end
|
17
|
+
|
18
|
+
def create
|
19
|
+
@subscription = ActionWebPush::Subscription.find_or_create_subscription(
|
20
|
+
user: @user,
|
21
|
+
**subscription_params,
|
22
|
+
user_agent: request.user_agent
|
23
|
+
)
|
24
|
+
|
25
|
+
if @subscription.persisted?
|
26
|
+
render json: @subscription, status: :created
|
27
|
+
else
|
28
|
+
render json: { errors: @subscription.errors }, status: :unprocessable_entity
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def destroy
|
33
|
+
@subscription.destroy
|
34
|
+
head :no_content
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def set_user
|
40
|
+
@user = current_user if respond_to?(:current_user)
|
41
|
+
@user ||= User.find(params[:user_id]) if params[:user_id]
|
42
|
+
|
43
|
+
unless @user
|
44
|
+
render json: { error: "User not found or not authenticated" }, status: :unauthorized
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def set_subscription
|
49
|
+
@subscription = ActionWebPush::Subscription.for_user(@user).find(params[:id])
|
50
|
+
end
|
51
|
+
|
52
|
+
def subscription_params
|
53
|
+
params.require(:subscription).permit(:endpoint, :p256dh_key, :auth_key)
|
54
|
+
end
|
55
|
+
|
56
|
+
def respond_to_authenticate_user?
|
57
|
+
respond_to?(:authenticate_user!)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|