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
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
|
5
|
+
module ActionWebPush
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def create_migration
|
11
|
+
migration_template(
|
12
|
+
"create_action_web_push_subscriptions.rb",
|
13
|
+
"db/migrate/create_action_web_push_subscriptions.rb",
|
14
|
+
migration_version: migration_version
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_initializer
|
19
|
+
template "initializer.rb", "config/initializers/action_web_push.rb"
|
20
|
+
end
|
21
|
+
|
22
|
+
def mount_engine
|
23
|
+
route "mount ActionWebPush::Engine => '/push'"
|
24
|
+
end
|
25
|
+
|
26
|
+
def show_readme
|
27
|
+
say "\n" + "="*50
|
28
|
+
say "ActionWebPush has been installed!"
|
29
|
+
say "="*50
|
30
|
+
say "\nNext steps:"
|
31
|
+
say "1. Run: rails db:migrate"
|
32
|
+
say "2. Configure VAPID keys in config/initializers/action_web_push.rb"
|
33
|
+
say "3. Add 'has_many :push_subscriptions, class_name: \"ActionWebPush::Subscription\"' to your User model"
|
34
|
+
say "4. Generate VAPID keys with: bundle exec rails generate action_web_push:vapid_keys"
|
35
|
+
say "\nFor more information, visit: https://github.com/keshav-k3/actionwebpush"
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def migration_version
|
41
|
+
if Rails::VERSION::MAJOR >= 5
|
42
|
+
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Compatibility module for transitioning from Campfire's web push to ActionWebPush
|
4
|
+
# Include this in your existing code during the migration period
|
5
|
+
|
6
|
+
module ActionWebPush
|
7
|
+
module CampfireCompatibility
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
# Alias old method names to new ones
|
12
|
+
alias_method :push_subscriptions, :action_web_push_subscriptions if respond_to?(:action_web_push_subscriptions)
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
# Provide backward compatibility for existing Campfire classes
|
17
|
+
def setup_campfire_compatibility
|
18
|
+
# Map old WebPush module to ActionWebPush
|
19
|
+
if defined?(::WebPush::Notification)
|
20
|
+
::WebPush::Notification.class_eval do
|
21
|
+
def self.new(*args, **kwargs)
|
22
|
+
ActionWebPush::Notification.new(*args, **kwargs)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Map old Pool class
|
28
|
+
if defined?(::WebPush::Pool)
|
29
|
+
::WebPush::Pool.class_eval do
|
30
|
+
def self.new(*args, **kwargs)
|
31
|
+
ActionWebPush::Pool.new(*args, **kwargs)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Legacy notification pusher that mimics Campfire's Room::MessagePusher
|
39
|
+
class LegacyMessagePusher
|
40
|
+
attr_reader :room, :message
|
41
|
+
|
42
|
+
def initialize(room:, message:)
|
43
|
+
@room, @message = room, message
|
44
|
+
end
|
45
|
+
|
46
|
+
def push
|
47
|
+
build_payload.tap do |payload|
|
48
|
+
push_to_users_involved_in_everything(payload)
|
49
|
+
push_to_users_involved_in_mentions(payload)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def build_payload
|
56
|
+
if room.direct?
|
57
|
+
build_direct_payload
|
58
|
+
else
|
59
|
+
build_shared_payload
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_direct_payload
|
64
|
+
{
|
65
|
+
title: message.creator.name,
|
66
|
+
body: message.plain_text_body,
|
67
|
+
data: { path: room_path }
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def build_shared_payload
|
72
|
+
{
|
73
|
+
title: room.name,
|
74
|
+
body: "#{message.creator.name}: #{message.plain_text_body}",
|
75
|
+
data: { path: room_path }
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def push_to_users_involved_in_everything(payload)
|
80
|
+
subscriptions = relevant_subscriptions.merge(involved_in_everything_scope)
|
81
|
+
ActionWebPush::BatchDelivery.deliver(
|
82
|
+
subscriptions.map { |sub| sub.build_notification(**payload) }
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def push_to_users_involved_in_mentions(payload)
|
87
|
+
return unless message.mentionees.any?
|
88
|
+
|
89
|
+
subscriptions = relevant_subscriptions
|
90
|
+
.merge(involved_in_mentions_scope)
|
91
|
+
.where(user_id: message.mentionees.ids)
|
92
|
+
|
93
|
+
ActionWebPush::BatchDelivery.deliver(
|
94
|
+
subscriptions.map { |sub| sub.build_notification(**payload) }
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
def relevant_subscriptions
|
99
|
+
ActionWebPush::Subscription
|
100
|
+
.joins(user: :memberships)
|
101
|
+
.merge(visible_disconnected_scope)
|
102
|
+
.where.not(user: message.creator)
|
103
|
+
end
|
104
|
+
|
105
|
+
def involved_in_everything_scope
|
106
|
+
# This should match your existing Membership.involved_in_everything scope
|
107
|
+
# Placeholder implementation:
|
108
|
+
Membership.where(involved_in_everything: true)
|
109
|
+
end
|
110
|
+
|
111
|
+
def involved_in_mentions_scope
|
112
|
+
# This should match your existing Membership.involved_in_mentions scope
|
113
|
+
# Placeholder implementation:
|
114
|
+
Membership.where(involved_in_mentions: true)
|
115
|
+
end
|
116
|
+
|
117
|
+
def visible_disconnected_scope
|
118
|
+
# This should match your existing Membership.visible.disconnected.where(room: room) scope
|
119
|
+
# Placeholder implementation:
|
120
|
+
Membership.where(room: room, visible: true, connected: false)
|
121
|
+
end
|
122
|
+
|
123
|
+
def room_path
|
124
|
+
# Use your existing route helper
|
125
|
+
Rails.application.routes.url_helpers.room_path(room)
|
126
|
+
rescue
|
127
|
+
"/rooms/#{room.id}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Helper methods for migrating existing code
|
132
|
+
module MigrationHelpers
|
133
|
+
def migrate_push_subscription_creation(params)
|
134
|
+
# Convert old push subscription params to ActionWebPush format
|
135
|
+
{
|
136
|
+
endpoint: params[:endpoint],
|
137
|
+
p256dh_key: params[:p256dh_key] || params[:keys]&.dig(:p256dh),
|
138
|
+
auth_key: params[:auth_key] || params[:keys]&.dig(:auth),
|
139
|
+
user_agent: params[:user_agent]
|
140
|
+
}
|
141
|
+
end
|
142
|
+
|
143
|
+
def create_action_web_push_subscription(user, params)
|
144
|
+
ActionWebPush::Subscription.find_or_create_subscription(
|
145
|
+
user: user,
|
146
|
+
**migrate_push_subscription_creation(params)
|
147
|
+
)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Batch migrate existing Push::Subscription records
|
151
|
+
def migrate_legacy_subscriptions!(batch_size: 1000)
|
152
|
+
return unless defined?(Push::Subscription)
|
153
|
+
|
154
|
+
Push::Subscription.find_in_batches(batch_size: batch_size) do |batch|
|
155
|
+
batch.each do |legacy_subscription|
|
156
|
+
ActionWebPush::Subscription.find_or_create_subscription(
|
157
|
+
user: legacy_subscription.user,
|
158
|
+
endpoint: legacy_subscription.endpoint,
|
159
|
+
p256dh_key: legacy_subscription.p256dh_key,
|
160
|
+
auth_key: legacy_subscription.auth_key,
|
161
|
+
user_agent: legacy_subscription.user_agent
|
162
|
+
)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Auto-setup compatibility when this file is loaded
|
171
|
+
if Rails.env.development? || Rails.env.staging?
|
172
|
+
ActionWebPush::CampfireCompatibility.setup_campfire_compatibility
|
173
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MigrateCampfirePushSubscriptionsToActionWebPush < ActiveRecord::Migration<%= migration_version %>
|
4
|
+
def up
|
5
|
+
# Create new ActionWebPush subscriptions table if it doesn't exist
|
6
|
+
unless table_exists?(:action_web_push_subscriptions)
|
7
|
+
create_table :action_web_push_subscriptions do |t|
|
8
|
+
t.references :user, null: false, foreign_key: true
|
9
|
+
t.string :endpoint, null: false
|
10
|
+
t.string :p256dh_key, null: false
|
11
|
+
t.string :auth_key, null: false
|
12
|
+
t.string :user_agent
|
13
|
+
t.timestamps null: false
|
14
|
+
|
15
|
+
t.index [:user_id]
|
16
|
+
t.index [:endpoint, :p256dh_key, :auth_key], name: "idx_action_web_push_subscription_keys"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
<% if preserve_data? %>
|
21
|
+
# Migrate existing data
|
22
|
+
if table_exists?(:<%= old_table_name %>)
|
23
|
+
say "Migrating data from <%= old_table_name %> to action_web_push_subscriptions..."
|
24
|
+
|
25
|
+
# Migrate in batches to avoid memory issues
|
26
|
+
batch_size = 1000
|
27
|
+
offset = 0
|
28
|
+
|
29
|
+
loop do
|
30
|
+
batch = connection.select_all(
|
31
|
+
"SELECT * FROM <%= old_table_name %> LIMIT #{batch_size} OFFSET #{offset}"
|
32
|
+
)
|
33
|
+
|
34
|
+
break if batch.empty?
|
35
|
+
|
36
|
+
batch.each do |record|
|
37
|
+
# Map old columns to new columns
|
38
|
+
new_record = {
|
39
|
+
user_id: record['user_id'],
|
40
|
+
endpoint: record['endpoint'],
|
41
|
+
p256dh_key: record['p256dh_key'],
|
42
|
+
auth_key: record['auth_key'],
|
43
|
+
user_agent: record['user_agent'],
|
44
|
+
created_at: record['created_at'] || Time.current,
|
45
|
+
updated_at: record['updated_at'] || Time.current
|
46
|
+
}
|
47
|
+
|
48
|
+
# Insert with duplicate handling
|
49
|
+
connection.execute(
|
50
|
+
sanitize_sql_array([
|
51
|
+
"INSERT INTO action_web_push_subscriptions (user_id, endpoint, p256dh_key, auth_key, user_agent, created_at, updated_at)
|
52
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
53
|
+
ON CONFLICT (endpoint, p256dh_key, auth_key) DO NOTHING",
|
54
|
+
new_record[:user_id],
|
55
|
+
new_record[:endpoint],
|
56
|
+
new_record[:p256dh_key],
|
57
|
+
new_record[:auth_key],
|
58
|
+
new_record[:user_agent],
|
59
|
+
new_record[:created_at],
|
60
|
+
new_record[:updated_at]
|
61
|
+
])
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
offset += batch_size
|
66
|
+
say "Migrated batch #{offset / batch_size} (#{offset} total records)"
|
67
|
+
end
|
68
|
+
|
69
|
+
migrated_count = connection.select_value("SELECT COUNT(*) FROM action_web_push_subscriptions")
|
70
|
+
original_count = connection.select_value("SELECT COUNT(*) FROM <%= old_table_name %>")
|
71
|
+
|
72
|
+
say "Migration completed: #{migrated_count} records in new table, #{original_count} in original table"
|
73
|
+
end
|
74
|
+
<% end %>
|
75
|
+
|
76
|
+
# Create a backup table for safety
|
77
|
+
if table_exists?(:<%= old_table_name %>) && !table_exists?(:<%= old_table_name %>_backup)
|
78
|
+
connection.execute("CREATE TABLE <%= old_table_name %>_backup AS SELECT * FROM <%= old_table_name %>")
|
79
|
+
say "Created backup table: <%= old_table_name %>_backup"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def down
|
84
|
+
# Remove ActionWebPush table and restore from backup if needed
|
85
|
+
drop_table :action_web_push_subscriptions if table_exists?(:action_web_push_subscriptions)
|
86
|
+
|
87
|
+
if table_exists?(:<%= old_table_name %>_backup) && !table_exists?(:<%= old_table_name %>)
|
88
|
+
connection.execute("CREATE TABLE <%= old_table_name %> AS SELECT * FROM <%= old_table_name %>_backup")
|
89
|
+
say "Restored from backup table: <%= old_table_name %>_backup"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def sanitize_sql_array(array)
|
96
|
+
ActiveRecord::Base.sanitize_sql_array(array)
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateActionWebPushSubscriptions < ActiveRecord::Migration<%= migration_version %>
|
4
|
+
def change
|
5
|
+
create_table :action_web_push_subscriptions do |t|
|
6
|
+
t.references :user, null: false, foreign_key: true
|
7
|
+
t.string :endpoint, null: false
|
8
|
+
t.string :p256dh_key, null: false
|
9
|
+
t.string :auth_key, null: false
|
10
|
+
t.string :user_agent
|
11
|
+
t.timestamps null: false
|
12
|
+
|
13
|
+
t.index [:user_id]
|
14
|
+
t.index [:endpoint, :p256dh_key, :auth_key], name: "idx_action_web_push_subscription_keys"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActionWebPush.configure do |config|
|
4
|
+
# VAPID keys for Web Push notifications
|
5
|
+
# Generate with: bundle exec rails generate action_web_push:vapid_keys
|
6
|
+
config.vapid_public_key = ENV['VAPID_PUBLIC_KEY']
|
7
|
+
config.vapid_private_key = ENV['VAPID_PRIVATE_KEY']
|
8
|
+
config.vapid_subject = 'mailto:support@example.com'
|
9
|
+
|
10
|
+
# Thread pool configuration
|
11
|
+
config.pool_size = 50
|
12
|
+
config.queue_size = 10000
|
13
|
+
|
14
|
+
# Delivery method
|
15
|
+
config.delivery_method = :web_push
|
16
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators/base"
|
4
|
+
require "web-push"
|
5
|
+
|
6
|
+
module ActionWebPush
|
7
|
+
module Generators
|
8
|
+
class VapidKeysGenerator < Rails::Generators::Base
|
9
|
+
desc "Generate VAPID key pair for Web Push notifications"
|
10
|
+
|
11
|
+
def generate_vapid_keys
|
12
|
+
vapid_key = WebPush.generate_key
|
13
|
+
|
14
|
+
say "Generated VAPID key pair:"
|
15
|
+
say "========================="
|
16
|
+
say ""
|
17
|
+
say "Add these to your environment variables or Rails credentials:"
|
18
|
+
say ""
|
19
|
+
say "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
|
20
|
+
say "VAPID_PRIVATE_KEY=#{vapid_key.private_key}"
|
21
|
+
say ""
|
22
|
+
say "Or add to config/credentials.yml.enc:"
|
23
|
+
say ""
|
24
|
+
say "action_web_push:"
|
25
|
+
say " vapid_public_key: #{vapid_key.public_key}"
|
26
|
+
say " vapid_private_key: #{vapid_key.private_key}"
|
27
|
+
say ""
|
28
|
+
say "Then update your config/initializers/action_web_push.rb:"
|
29
|
+
say ""
|
30
|
+
say "ActionWebPush.configure do |config|"
|
31
|
+
say " config.vapid_public_key = Rails.application.credentials.action_web_push[:vapid_public_key]"
|
32
|
+
say " config.vapid_private_key = Rails.application.credentials.action_web_push[:vapid_private_key]"
|
33
|
+
say " # ... other config"
|
34
|
+
say "end"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionWebPush
|
4
|
+
module Instrumentation
|
5
|
+
def self.instrument(event_name, payload = {})
|
6
|
+
if defined?(ActiveSupport::Notifications)
|
7
|
+
ActiveSupport::Notifications.instrument("action_web_push.#{event_name}", payload) do |notification_payload|
|
8
|
+
yield notification_payload if block_given?
|
9
|
+
end
|
10
|
+
else
|
11
|
+
yield payload if block_given?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.publish(event_name, payload = {})
|
16
|
+
if defined?(ActiveSupport::Notifications)
|
17
|
+
ActiveSupport::Notifications.publish("action_web_push.#{event_name}", payload)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Available events:
|
22
|
+
# action_web_push.notification_delivery
|
23
|
+
# action_web_push.notification_delivery_failed
|
24
|
+
# action_web_push.pool_overflow
|
25
|
+
# action_web_push.rate_limit_exceeded
|
26
|
+
# action_web_push.batch_delivery
|
27
|
+
# action_web_push.subscription_expired
|
28
|
+
# action_web_push.subscription_created
|
29
|
+
# action_web_push.subscription_destroyed
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
|
5
|
+
module ActionWebPush
|
6
|
+
module Logging
|
7
|
+
def logger
|
8
|
+
ActionWebPush.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
module_function :logger
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def logger
|
16
|
+
@logger ||= config.logger || default_logger
|
17
|
+
end
|
18
|
+
|
19
|
+
def logger=(logger)
|
20
|
+
@logger = logger
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def default_logger
|
26
|
+
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
27
|
+
Rails.logger
|
28
|
+
else
|
29
|
+
Logger.new(STDOUT).tap do |log|
|
30
|
+
log.level = Logger::INFO
|
31
|
+
log.formatter = proc do |severity, datetime, progname, msg|
|
32
|
+
"[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} -- ActionWebPush: #{msg}\n"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionWebPush
|
4
|
+
class Metrics
|
5
|
+
@mutex = Mutex.new
|
6
|
+
@stats = {
|
7
|
+
deliveries_attempted: 0,
|
8
|
+
deliveries_succeeded: 0,
|
9
|
+
deliveries_failed: 0,
|
10
|
+
expired_subscriptions: 0,
|
11
|
+
queue_size: 0
|
12
|
+
}
|
13
|
+
|
14
|
+
class << self
|
15
|
+
attr_reader :stats
|
16
|
+
|
17
|
+
def increment(metric, count = 1)
|
18
|
+
@mutex.synchronize { @stats[metric] += count }
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(metric, value)
|
22
|
+
@mutex.synchronize { @stats[metric] = value }
|
23
|
+
end
|
24
|
+
|
25
|
+
def get(metric)
|
26
|
+
@mutex.synchronize { @stats[metric] }
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset!
|
30
|
+
@mutex.synchronize do
|
31
|
+
@stats.keys.each { |key| @stats[key] = 0 }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def delivery_attempted!
|
36
|
+
increment(:deliveries_attempted)
|
37
|
+
end
|
38
|
+
|
39
|
+
def delivery_succeeded!
|
40
|
+
increment(:deliveries_succeeded)
|
41
|
+
end
|
42
|
+
|
43
|
+
def delivery_failed!
|
44
|
+
increment(:deliveries_failed)
|
45
|
+
end
|
46
|
+
|
47
|
+
def subscription_expired!
|
48
|
+
increment(:expired_subscriptions)
|
49
|
+
end
|
50
|
+
|
51
|
+
def success_rate
|
52
|
+
attempted = get(:deliveries_attempted)
|
53
|
+
return 0.0 if attempted.zero?
|
54
|
+
|
55
|
+
(get(:deliveries_succeeded).to_f / attempted * 100).round(2)
|
56
|
+
end
|
57
|
+
|
58
|
+
def failure_rate
|
59
|
+
100.0 - success_rate
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_h
|
63
|
+
@mutex.synchronize { @stats.dup }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "web-push"
|
4
|
+
|
5
|
+
module ActionWebPush
|
6
|
+
class Notification
|
7
|
+
include ActionWebPush::Authorization
|
8
|
+
attr_reader :title, :body, :data, :endpoint, :p256dh_key, :auth_key, :options, :current_user
|
9
|
+
|
10
|
+
def initialize(title:, body:, endpoint:, p256dh_key:, auth_key:, data: {}, current_user: nil, **options)
|
11
|
+
@title = title
|
12
|
+
@body = body
|
13
|
+
@data = data
|
14
|
+
@endpoint = endpoint
|
15
|
+
@p256dh_key = p256dh_key
|
16
|
+
@auth_key = auth_key
|
17
|
+
@options = options
|
18
|
+
@current_user = current_user || ActionWebPush::Authorization::Utils.current_user_context
|
19
|
+
end
|
20
|
+
|
21
|
+
def deliver(connection: nil)
|
22
|
+
# Authorization check if current_user is present
|
23
|
+
if @current_user && !ActionWebPush::Authorization::Utils.authorization_bypassed?
|
24
|
+
# Find subscription by endpoint for authorization
|
25
|
+
subscription = ActionWebPush::Subscription.find_by(endpoint: endpoint)
|
26
|
+
if subscription
|
27
|
+
authorize_notification_sending!(
|
28
|
+
current_user: @current_user,
|
29
|
+
subscriptions: [subscription]
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
delivery_method = ActionWebPush.config.delivery_method_class.new
|
35
|
+
delivery_method.deliver!(self, connection: connection)
|
36
|
+
end
|
37
|
+
|
38
|
+
def deliver_now(connection: nil)
|
39
|
+
deliver(connection: connection)
|
40
|
+
end
|
41
|
+
|
42
|
+
def deliver_later(wait: nil, wait_until: nil, queue: nil, priority: nil)
|
43
|
+
job = ActionWebPush::DeliveryJob.set(
|
44
|
+
wait: wait,
|
45
|
+
wait_until: wait_until,
|
46
|
+
queue: queue || :action_web_push,
|
47
|
+
priority: priority
|
48
|
+
)
|
49
|
+
|
50
|
+
job.perform_later(to_params)
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_params
|
54
|
+
{
|
55
|
+
title: title,
|
56
|
+
body: body,
|
57
|
+
data: data,
|
58
|
+
endpoint: endpoint,
|
59
|
+
p256dh_key: p256dh_key,
|
60
|
+
auth_key: auth_key
|
61
|
+
}.merge(options)
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_json(*args)
|
65
|
+
{
|
66
|
+
title: title,
|
67
|
+
body: body,
|
68
|
+
data: data
|
69
|
+
}.merge(options).to_json(*args)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def vapid_identification
|
75
|
+
config = ActionWebPush.config
|
76
|
+
{
|
77
|
+
subject: config.vapid_subject,
|
78
|
+
public_key: config.vapid_public_key,
|
79
|
+
private_key: config.vapid_private_key
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def encoded_message
|
84
|
+
payload = {
|
85
|
+
title: title,
|
86
|
+
options: {
|
87
|
+
body: body,
|
88
|
+
icon: options[:icon],
|
89
|
+
badge: options[:badge],
|
90
|
+
data: data
|
91
|
+
}
|
92
|
+
}
|
93
|
+
|
94
|
+
JSON.generate(payload)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|