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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/CONTRIBUTING.md +256 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +569 -0
  7. data/Rakefile +17 -0
  8. data/app/controllers/actionwebpush/subscriptions_controller.rb +60 -0
  9. data/app/models/actionwebpush/subscription.rb +164 -0
  10. data/config/routes.rb +5 -0
  11. data/db/migrate/001_create_action_web_push_subscriptions.rb +17 -0
  12. data/lib/actionwebpush/analytics.rb +197 -0
  13. data/lib/actionwebpush/authorization.rb +236 -0
  14. data/lib/actionwebpush/base.rb +107 -0
  15. data/lib/actionwebpush/batch_delivery.rb +92 -0
  16. data/lib/actionwebpush/configuration.rb +91 -0
  17. data/lib/actionwebpush/delivery_job.rb +52 -0
  18. data/lib/actionwebpush/delivery_methods/base.rb +19 -0
  19. data/lib/actionwebpush/delivery_methods/test.rb +36 -0
  20. data/lib/actionwebpush/delivery_methods/web_push.rb +74 -0
  21. data/lib/actionwebpush/engine.rb +20 -0
  22. data/lib/actionwebpush/error_handler.rb +99 -0
  23. data/lib/actionwebpush/generators/campfire_migration_generator.rb +69 -0
  24. data/lib/actionwebpush/generators/install_generator.rb +47 -0
  25. data/lib/actionwebpush/generators/templates/campfire_compatibility.rb +173 -0
  26. data/lib/actionwebpush/generators/templates/campfire_data_migration.rb +98 -0
  27. data/lib/actionwebpush/generators/templates/create_action_web_push_subscriptions.rb +17 -0
  28. data/lib/actionwebpush/generators/templates/initializer.rb +16 -0
  29. data/lib/actionwebpush/generators/vapid_keys_generator.rb +38 -0
  30. data/lib/actionwebpush/instrumentation.rb +31 -0
  31. data/lib/actionwebpush/logging.rb +38 -0
  32. data/lib/actionwebpush/metrics.rb +67 -0
  33. data/lib/actionwebpush/notification.rb +97 -0
  34. data/lib/actionwebpush/pool.rb +167 -0
  35. data/lib/actionwebpush/railtie.rb +48 -0
  36. data/lib/actionwebpush/rate_limiter.rb +167 -0
  37. data/lib/actionwebpush/sentry_integration.rb +104 -0
  38. data/lib/actionwebpush/status_broadcaster.rb +62 -0
  39. data/lib/actionwebpush/status_channel.rb +21 -0
  40. data/lib/actionwebpush/tenant_configuration.rb +106 -0
  41. data/lib/actionwebpush/test_helper.rb +68 -0
  42. data/lib/actionwebpush/version.rb +5 -0
  43. data/lib/actionwebpush.rb +78 -0
  44. data/sig/actionwebpush.rbs +4 -0
  45. 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