parse-stack-next 4.5.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/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../query.rb"
|
|
5
|
+
require_relative "../client.rb"
|
|
6
|
+
require "active_model/serializers/json"
|
|
7
|
+
|
|
8
|
+
module Parse
|
|
9
|
+
# This class represents the API to send push notification to devices that are
|
|
10
|
+
# available in the Installation table. Push notifications are implemented
|
|
11
|
+
# through the `Parse::Push` class. To send push notifications through the
|
|
12
|
+
# REST API, you must enable `REST push enabled?` option in the `Push
|
|
13
|
+
# Notification Settings` section of the `Settings` page in your Parse
|
|
14
|
+
# application. Push notifications targeting uses the Installation Parse
|
|
15
|
+
# class to determine which devices receive the notification. You can provide
|
|
16
|
+
# any query constraint, similar to using `Parse::Query`, in order to target
|
|
17
|
+
# the specific set of devices you want given the columns you have configured
|
|
18
|
+
# in your `Installation` class. The `Parse::Push` class supports many other
|
|
19
|
+
# options not listed here.
|
|
20
|
+
#
|
|
21
|
+
# @example Traditional API
|
|
22
|
+
# # simple channel push (targeted, sends without opt-in)
|
|
23
|
+
# push = Parse::Push.new
|
|
24
|
+
# push.channels = ["addicted2salsa"]
|
|
25
|
+
# push.send "You are subscribed to Addicted2Salsa!"
|
|
26
|
+
#
|
|
27
|
+
# # advanced targeting
|
|
28
|
+
# push = Parse::Push.new({..where query constraints..})
|
|
29
|
+
# push.where :device_type.in => ['ios','android'], :location.near => some_geopoint
|
|
30
|
+
# push.alert = "Hello World!"
|
|
31
|
+
# push.sound = "soundfile.caf"
|
|
32
|
+
# push.data = { uri: "app://deep_link_path" }
|
|
33
|
+
# push.send
|
|
34
|
+
#
|
|
35
|
+
# # broadcast to every Installation (requires explicit opt-in)
|
|
36
|
+
# Parse::Push.new.broadcast!.send("Hello World!")
|
|
37
|
+
# # or set process-wide: Parse::Push.allow_broadcast = true
|
|
38
|
+
#
|
|
39
|
+
# @example Builder Pattern API (Fluent Interface)
|
|
40
|
+
# # Simple channel push with builder pattern
|
|
41
|
+
# Parse::Push.new
|
|
42
|
+
# .to_channel("news")
|
|
43
|
+
# .with_alert("Breaking news!")
|
|
44
|
+
# .send!
|
|
45
|
+
#
|
|
46
|
+
# # Rich push with scheduling
|
|
47
|
+
# Parse::Push.new
|
|
48
|
+
# .to_channels("sports", "updates")
|
|
49
|
+
# .with_title("Game Alert")
|
|
50
|
+
# .with_body("Your team is playing now!")
|
|
51
|
+
# .with_badge(1)
|
|
52
|
+
# .with_sound("alert.caf")
|
|
53
|
+
# .with_data(game_id: "12345")
|
|
54
|
+
# .schedule(1.hour.from_now)
|
|
55
|
+
# .expires_in(3600)
|
|
56
|
+
# .send!
|
|
57
|
+
#
|
|
58
|
+
# # Using class method shortcut
|
|
59
|
+
# Parse::Push.to_channel("alerts")
|
|
60
|
+
# .with_alert("Important update")
|
|
61
|
+
# .send!
|
|
62
|
+
#
|
|
63
|
+
# # Using query block for advanced targeting
|
|
64
|
+
# Parse::Push.new
|
|
65
|
+
# .to_query { |q| q.where(:device_type => "ios", :app_version.gte => "2.0") }
|
|
66
|
+
# .with_alert("iOS 2.0+ users only")
|
|
67
|
+
# .send!
|
|
68
|
+
#
|
|
69
|
+
class Push
|
|
70
|
+
include Client::Connectable
|
|
71
|
+
|
|
72
|
+
# Raised when a {Parse::Push} would broadcast to every Installation
|
|
73
|
+
# because no `where` constraints and no `channels` are set, and the
|
|
74
|
+
# caller did not explicitly opt in via {Parse::Push.allow_broadcast}
|
|
75
|
+
# or per-instance {#broadcast!}.
|
|
76
|
+
#
|
|
77
|
+
# This is a fail-closed guard against the +to_audience+ /
|
|
78
|
+
# +to_audience_id+ class of footguns where a typo, deleted audience,
|
|
79
|
+
# or unset-param silently degrades a targeted push into a global one.
|
|
80
|
+
class BroadcastNotAllowed < StandardError; end
|
|
81
|
+
|
|
82
|
+
# Raised when {#to_audience} or {#to_audience_id} cannot resolve the
|
|
83
|
+
# requested audience. Previously these methods warned and returned
|
|
84
|
+
# +self+, which let the subsequent +send!+ silently broadcast to every
|
|
85
|
+
# Installation. They now raise so typos and renames surface at the
|
|
86
|
+
# call site instead.
|
|
87
|
+
class AudienceNotFound < ArgumentError; end
|
|
88
|
+
|
|
89
|
+
# @!attribute [rw] allow_broadcast
|
|
90
|
+
# Whether {Parse::Push} permits an unconstrained push (no `where`,
|
|
91
|
+
# no `channels`) to broadcast to every Installation. Defaults to
|
|
92
|
+
# +false+ — sending an unconstrained push raises {BroadcastNotAllowed}.
|
|
93
|
+
#
|
|
94
|
+
# Set to +true+ at boot for apps that legitimately broadcast (e.g.,
|
|
95
|
+
# `Parse::Push.allow_broadcast = true`). Or opt in per-instance with
|
|
96
|
+
# {#broadcast!}, which is auditable in code review.
|
|
97
|
+
# @return [Boolean]
|
|
98
|
+
class << self
|
|
99
|
+
attr_accessor :allow_broadcast
|
|
100
|
+
end
|
|
101
|
+
self.allow_broadcast = false
|
|
102
|
+
|
|
103
|
+
# Device types that support push notifications.
|
|
104
|
+
# These are the device types that Parse Server has push adapters for.
|
|
105
|
+
# @see https://docs.parseplatform.org/parse-server/guide/#push-notifications
|
|
106
|
+
SUPPORTED_PUSH_DEVICE_TYPES = %w[ios android osx tvos watchos web expo].freeze
|
|
107
|
+
|
|
108
|
+
# Device types that are known but may not have push support configured.
|
|
109
|
+
# These will generate warnings when targeted.
|
|
110
|
+
UNSUPPORTED_PUSH_DEVICE_TYPES = %w[win other unknown unsupported].freeze
|
|
111
|
+
|
|
112
|
+
# @!attribute [rw] query
|
|
113
|
+
# Sending a push notification is done by performing a query against the Installation
|
|
114
|
+
# collection with a Parse::Query. This query contains the constraints that will be
|
|
115
|
+
# sent to Parse with the push payload.
|
|
116
|
+
# @return [Parse::Query] the query containing Installation constraints.
|
|
117
|
+
|
|
118
|
+
# @!attribute [rw] alert
|
|
119
|
+
# @return [String]
|
|
120
|
+
# @!attribute [rw] badge
|
|
121
|
+
# @return [Integer]
|
|
122
|
+
# @!attribute [rw] sound
|
|
123
|
+
# @return [String] the name of the sound file
|
|
124
|
+
# @!attribute [rw] title
|
|
125
|
+
# @return [String]
|
|
126
|
+
# @!attribute [rw] data
|
|
127
|
+
# @return [Hash] specific payload data.
|
|
128
|
+
# @!attribute [rw] expiration_time
|
|
129
|
+
# @return [Parse::Date]
|
|
130
|
+
# @!attribute [rw] expiration_interval
|
|
131
|
+
# @return [Integer]
|
|
132
|
+
# @!attribute [rw] push_time
|
|
133
|
+
# @return [Parse::Date]
|
|
134
|
+
# @!attribute [rw] channels
|
|
135
|
+
# @return [Array] an array of strings for subscribed channels.
|
|
136
|
+
# @!attribute [rw] content_available
|
|
137
|
+
# @return [Boolean] whether this is a silent push (iOS content-available).
|
|
138
|
+
# @!attribute [rw] mutable_content
|
|
139
|
+
# @return [Boolean] whether this notification can be modified by a service extension (iOS).
|
|
140
|
+
# @!attribute [rw] category
|
|
141
|
+
# @return [String] the notification category for action buttons (iOS).
|
|
142
|
+
# @!attribute [rw] image_url
|
|
143
|
+
# @return [String] URL for an image attachment (requires mutable-content).
|
|
144
|
+
# @!attribute [rw] localized_alerts
|
|
145
|
+
# @return [Hash] language-specific alert messages (e.g., {"en" => "Hello", "fr" => "Bonjour"})
|
|
146
|
+
# @!attribute [rw] localized_titles
|
|
147
|
+
# @return [Hash] language-specific titles (e.g., {"en" => "Welcome", "fr" => "Bienvenue"})
|
|
148
|
+
attr_writer :query
|
|
149
|
+
attr_reader :channels, :data
|
|
150
|
+
attr_accessor :alert, :badge, :sound, :title,
|
|
151
|
+
:expiration_time, :expiration_interval, :push_time,
|
|
152
|
+
:content_available, :mutable_content, :category, :image_url,
|
|
153
|
+
:localized_alerts, :localized_titles
|
|
154
|
+
|
|
155
|
+
alias_method :message, :alert
|
|
156
|
+
alias_method :message=, :alert=
|
|
157
|
+
|
|
158
|
+
# Send a push notification using a push notification hash
|
|
159
|
+
# @param payload [Hash] a push notification hash payload
|
|
160
|
+
def self.send(payload)
|
|
161
|
+
client.push payload.as_json
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Create a new Push targeting a specific channel.
|
|
165
|
+
# @param channel [String] the channel name to target
|
|
166
|
+
# @return [Parse::Push] a new Push instance for chaining
|
|
167
|
+
# @example
|
|
168
|
+
# Parse::Push.to_channel("news").with_alert("Hello!").send!
|
|
169
|
+
def self.to_channel(channel)
|
|
170
|
+
new.to_channel(channel)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Create a new Push targeting multiple channels.
|
|
174
|
+
# @param channels [Array<String>] the channel names to target
|
|
175
|
+
# @return [Parse::Push] a new Push instance for chaining
|
|
176
|
+
# @example
|
|
177
|
+
# Parse::Push.to_channels("news", "sports").with_alert("Update!").send!
|
|
178
|
+
def self.to_channels(*channels)
|
|
179
|
+
new.to_channels(*channels)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# List all available channels from the Installation collection.
|
|
183
|
+
# This is a convenience method that delegates to {Installation.all_channels}.
|
|
184
|
+
# @return [Array<String>] array of channel names
|
|
185
|
+
# @example
|
|
186
|
+
# available_channels = Parse::Push.channels
|
|
187
|
+
# # => ["news", "sports", "weather"]
|
|
188
|
+
def self.channels
|
|
189
|
+
Parse::Installation.all_channels
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Create a new Push targeting a specific user.
|
|
193
|
+
# @param user [Parse::User, Hash, String] the user to target
|
|
194
|
+
# @return [Parse::Push] a new Push instance for chaining
|
|
195
|
+
# @example
|
|
196
|
+
# Parse::Push.to_user(current_user).with_alert("Hello!").send!
|
|
197
|
+
def self.to_user(user)
|
|
198
|
+
new.to_user(user)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Create a new Push targeting a user by their objectId.
|
|
202
|
+
# @param user_id [String] the objectId of the user to target
|
|
203
|
+
# @return [Parse::Push] a new Push instance for chaining
|
|
204
|
+
# @example
|
|
205
|
+
# Parse::Push.to_user_id("abc123").with_alert("Hello!").send!
|
|
206
|
+
def self.to_user_id(user_id)
|
|
207
|
+
new.to_user_id(user_id)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Create a new Push targeting multiple users.
|
|
211
|
+
# @param users [Array<Parse::User, Hash, String>] the users to target
|
|
212
|
+
# @return [Parse::Push] a new Push instance for chaining
|
|
213
|
+
# @example
|
|
214
|
+
# Parse::Push.to_users(user1, user2).with_alert("Group message!").send!
|
|
215
|
+
def self.to_users(*users)
|
|
216
|
+
new.to_users(*users)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Create a new Push targeting a specific installation.
|
|
220
|
+
# @param installation [Parse::Installation, Hash, String] the installation to target
|
|
221
|
+
# @return [Parse::Push] a new Push instance for chaining
|
|
222
|
+
# @example
|
|
223
|
+
# Parse::Push.to_installation(device).with_alert("Hello!").send!
|
|
224
|
+
def self.to_installation(installation)
|
|
225
|
+
new.to_installation(installation)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Create a new Push targeting an installation by its objectId.
|
|
229
|
+
# @param installation_id [String] the objectId of the installation to target
|
|
230
|
+
# @return [Parse::Push] a new Push instance for chaining
|
|
231
|
+
# @example
|
|
232
|
+
# Parse::Push.to_installation_id("abc123").with_alert("Hello!").send!
|
|
233
|
+
def self.to_installation_id(installation_id)
|
|
234
|
+
new.to_installation_id(installation_id)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Create a new Push targeting multiple installations.
|
|
238
|
+
# @param installations [Array<Parse::Installation, Hash, String>] the installations to target
|
|
239
|
+
# @return [Parse::Push] a new Push instance for chaining
|
|
240
|
+
# @example
|
|
241
|
+
# Parse::Push.to_installations(device1, device2).with_alert("Hello!").send!
|
|
242
|
+
def self.to_installations(*installations)
|
|
243
|
+
new.to_installations(*installations)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Initialize a new push notification request.
|
|
247
|
+
# @param constraints [Hash] a set of query constraints
|
|
248
|
+
def initialize(constraints = {})
|
|
249
|
+
self.where constraints
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def query
|
|
253
|
+
@query ||= Parse::Query.new(Parse::Model::CLASS_INSTALLATION)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Set a hash of conditions for this push query.
|
|
257
|
+
# @return [Parse::Query]
|
|
258
|
+
def where=(where_clauses)
|
|
259
|
+
query.where where_clauses
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Apply a set of constraints.
|
|
263
|
+
# @param constraints [Hash] the set of {Parse::Query} cosntraints
|
|
264
|
+
# @return [Hash] if no constraints were passed, returns a compiled query.
|
|
265
|
+
# @return [Parse::Query] if constraints were passed, returns the chainable query.
|
|
266
|
+
def where(constraints = nil)
|
|
267
|
+
return query.compile_where unless constraints.is_a?(Hash)
|
|
268
|
+
query.where constraints
|
|
269
|
+
query
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def channels=(list)
|
|
273
|
+
@channels = Array.wrap(list)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Check if this push has content-available set (silent push).
|
|
277
|
+
# @return [Boolean] true if content-available is enabled
|
|
278
|
+
def content_available?
|
|
279
|
+
@content_available == true
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Check if this push has mutable-content set (rich notifications).
|
|
283
|
+
# @return [Boolean] true if mutable-content is enabled
|
|
284
|
+
def mutable_content?
|
|
285
|
+
@mutable_content == true
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def data=(h)
|
|
289
|
+
if h.is_a?(String)
|
|
290
|
+
@alert = h
|
|
291
|
+
else
|
|
292
|
+
@data = h.symbolize_keys
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# @return [Hash] a JSON encoded hash.
|
|
297
|
+
def as_json(*args)
|
|
298
|
+
payload.as_json
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# @return [String] a JSON encoded string.
|
|
302
|
+
def to_json(*args)
|
|
303
|
+
as_json.to_json
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# This method takes all the parameters of the instance and creates a proper
|
|
307
|
+
# hash structure, required by Parse, in order to process the push notification.
|
|
308
|
+
# @return [Hash] the prepared push payload to be used in the request.
|
|
309
|
+
def payload
|
|
310
|
+
msg = {
|
|
311
|
+
data: {
|
|
312
|
+
alert: alert,
|
|
313
|
+
badge: badge || "Increment",
|
|
314
|
+
},
|
|
315
|
+
}
|
|
316
|
+
msg[:data][:sound] = sound if sound.present?
|
|
317
|
+
msg[:data][:title] = title if title.present?
|
|
318
|
+
msg[:data][:"content-available"] = 1 if content_available?
|
|
319
|
+
msg[:data][:"mutable-content"] = 1 if mutable_content?
|
|
320
|
+
msg[:data][:category] = @category if @category.present?
|
|
321
|
+
msg[:data][:image] = @image_url if @image_url.present?
|
|
322
|
+
|
|
323
|
+
# Add localized alerts (e.g., "alert-en", "alert-fr")
|
|
324
|
+
if @localized_alerts.is_a?(Hash)
|
|
325
|
+
@localized_alerts.each do |lang, text|
|
|
326
|
+
msg[:data][:"alert-#{lang}"] = text
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Add localized titles (e.g., "title-en", "title-fr")
|
|
331
|
+
if @localized_titles.is_a?(Hash)
|
|
332
|
+
@localized_titles.each do |lang, text|
|
|
333
|
+
msg[:data][:"title-#{lang}"] = text
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
msg[:data].merge! @data if @data.is_a?(Hash)
|
|
338
|
+
|
|
339
|
+
if @expiration_time.present?
|
|
340
|
+
msg[:expiration_time] = @expiration_time.respond_to?(:iso8601) ? @expiration_time.iso8601(3) : @expiration_time
|
|
341
|
+
end
|
|
342
|
+
if @push_time.present?
|
|
343
|
+
msg[:push_time] = @push_time.respond_to?(:iso8601) ? @push_time.iso8601(3) : @push_time
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
if @expiration_interval.is_a?(Numeric)
|
|
347
|
+
msg[:expiration_interval] = @expiration_interval.to_i
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
if query.where.present?
|
|
351
|
+
q = @query.dup
|
|
352
|
+
if @channels.is_a?(Array) && @channels.empty? == false
|
|
353
|
+
q.where :channels.in => @channels
|
|
354
|
+
end
|
|
355
|
+
msg[:where] = q.compile_where unless q.where.empty?
|
|
356
|
+
elsif @channels.is_a?(Array) && @channels.empty? == false
|
|
357
|
+
msg[:channels] = @channels
|
|
358
|
+
end
|
|
359
|
+
msg
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# helper method to send a message
|
|
363
|
+
# @param message [String] the message to send
|
|
364
|
+
# @raise [BroadcastNotAllowed] if the push has no `where` constraints
|
|
365
|
+
# and no `channels`, and neither {Parse::Push.allow_broadcast} nor
|
|
366
|
+
# per-instance {#broadcast!} was set.
|
|
367
|
+
def send(message = nil)
|
|
368
|
+
@alert = message if message.is_a?(String)
|
|
369
|
+
@data = message if message.is_a?(Hash)
|
|
370
|
+
assert_broadcast_allowed!
|
|
371
|
+
client.push(payload.as_json)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Opt this specific push in to broadcasting to every Installation.
|
|
375
|
+
# Use when you legitimately want a global push and have not set the
|
|
376
|
+
# process-wide {Parse::Push.allow_broadcast}. The explicit call site
|
|
377
|
+
# is the audit trail.
|
|
378
|
+
# @return [self] for chaining
|
|
379
|
+
# @example
|
|
380
|
+
# Parse::Push.new.broadcast!.with_alert("Maintenance window").send!
|
|
381
|
+
def broadcast!
|
|
382
|
+
@broadcast_allowed = true
|
|
383
|
+
self
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# @return [Boolean] true when broadcasting is allowed for this push,
|
|
387
|
+
# either via the class-level {Parse::Push.allow_broadcast} flag or
|
|
388
|
+
# the per-instance {#broadcast!} opt-in.
|
|
389
|
+
def broadcast_allowed?
|
|
390
|
+
@broadcast_allowed == true || self.class.allow_broadcast == true
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
private
|
|
394
|
+
|
|
395
|
+
# Raise {BroadcastNotAllowed} when the assembled payload would
|
|
396
|
+
# broadcast (no `where`, no `channels`) and neither the class-level
|
|
397
|
+
# nor the per-instance opt-in is set. Called from {#send} and {#send!}.
|
|
398
|
+
def assert_broadcast_allowed!
|
|
399
|
+
return if broadcast_allowed?
|
|
400
|
+
compiled = payload
|
|
401
|
+
has_where = compiled[:where].is_a?(Hash) && !compiled[:where].empty?
|
|
402
|
+
has_channels = compiled[:channels].is_a?(Array) && !compiled[:channels].empty?
|
|
403
|
+
return if has_where || has_channels
|
|
404
|
+
raise BroadcastNotAllowed,
|
|
405
|
+
"Refusing to broadcast push to every Installation: no `where` " \
|
|
406
|
+
"constraints and no `channels` are set. Add targeting (channels, " \
|
|
407
|
+
"to_audience, to_user, to_query), or opt in explicitly via " \
|
|
408
|
+
"Parse::Push.allow_broadcast = true or per-instance #broadcast!."
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
public
|
|
412
|
+
|
|
413
|
+
# =========================================================================
|
|
414
|
+
# Builder Pattern Methods (Fluent Interface)
|
|
415
|
+
# =========================================================================
|
|
416
|
+
|
|
417
|
+
# Target a specific channel for this push notification.
|
|
418
|
+
# @param channel [String] the channel name to target
|
|
419
|
+
# @return [self] returns self for method chaining
|
|
420
|
+
# @example
|
|
421
|
+
# push.to_channel("news").with_alert("Update!").send!
|
|
422
|
+
def to_channel(channel)
|
|
423
|
+
self.channels = [channel]
|
|
424
|
+
self
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Target multiple channels for this push notification.
|
|
428
|
+
# @param channels [Array<String>] the channel names to target
|
|
429
|
+
# @return [self] returns self for method chaining
|
|
430
|
+
# @example
|
|
431
|
+
# push.to_channels("news", "sports").with_alert("Update!").send!
|
|
432
|
+
def to_channels(*channels)
|
|
433
|
+
self.channels = channels.flatten
|
|
434
|
+
self
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Configure the push query using a block.
|
|
438
|
+
# The block receives the query object for adding constraints.
|
|
439
|
+
# @yield [Parse::Query] the Installation query to configure
|
|
440
|
+
# @return [self] returns self for method chaining
|
|
441
|
+
# @example
|
|
442
|
+
# push.to_query { |q| q.where(:device_type => "ios") }.send!
|
|
443
|
+
def to_query
|
|
444
|
+
yield query if block_given?
|
|
445
|
+
self
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Set the alert message for this push notification.
|
|
449
|
+
# @param message [String] the alert message
|
|
450
|
+
# @return [self] returns self for method chaining
|
|
451
|
+
# @example
|
|
452
|
+
# push.with_alert("Hello World!").send!
|
|
453
|
+
def with_alert(message)
|
|
454
|
+
self.alert = message
|
|
455
|
+
self
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Alias for {#with_alert} - sets the body text of the notification.
|
|
459
|
+
# @param body [String] the body/alert message
|
|
460
|
+
# @return [self] returns self for method chaining
|
|
461
|
+
# @see #with_alert
|
|
462
|
+
def with_body(body)
|
|
463
|
+
with_alert(body)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Set the title for this push notification (appears above the alert).
|
|
467
|
+
# @param title [String] the notification title
|
|
468
|
+
# @return [self] returns self for method chaining
|
|
469
|
+
# @example
|
|
470
|
+
# push.with_title("News").with_body("Article published").send!
|
|
471
|
+
def with_title(title)
|
|
472
|
+
self.title = title
|
|
473
|
+
self
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Set the badge number for this push notification.
|
|
477
|
+
# @param count [Integer, String] the badge count, or "Increment" to increment
|
|
478
|
+
# @return [self] returns self for method chaining
|
|
479
|
+
# @example
|
|
480
|
+
# push.with_badge(5).send! # Set to 5
|
|
481
|
+
# push.with_badge(0).send! # Clear badge
|
|
482
|
+
def with_badge(count)
|
|
483
|
+
self.badge = count
|
|
484
|
+
self
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Set the sound file for this push notification.
|
|
488
|
+
# @param sound_name [String] the name of the sound file
|
|
489
|
+
# @return [self] returns self for method chaining
|
|
490
|
+
# @example
|
|
491
|
+
# push.with_sound("notification.caf").send!
|
|
492
|
+
def with_sound(sound_name)
|
|
493
|
+
self.sound = sound_name
|
|
494
|
+
self
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Set custom data payload for this push notification.
|
|
498
|
+
# @param hash [Hash] custom key-value pairs to include in the payload
|
|
499
|
+
# @return [self] returns self for method chaining
|
|
500
|
+
# @example
|
|
501
|
+
# push.with_data(article_id: "123", action: "open").send!
|
|
502
|
+
def with_data(hash)
|
|
503
|
+
@data ||= {}
|
|
504
|
+
@data.merge!(hash.symbolize_keys)
|
|
505
|
+
self
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Schedule the push notification for a future time.
|
|
509
|
+
# @param time [Time, DateTime, String] when to send the push
|
|
510
|
+
# @return [self] returns self for method chaining
|
|
511
|
+
# @example
|
|
512
|
+
# push.schedule(1.hour.from_now).send!
|
|
513
|
+
# push.schedule(Time.new(2025, 12, 25, 9, 0, 0)).send!
|
|
514
|
+
def schedule(time)
|
|
515
|
+
self.push_time = time
|
|
516
|
+
self
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Set the expiration time for this push notification.
|
|
520
|
+
# The push will not be delivered after this time.
|
|
521
|
+
# @param time [Time, DateTime, String] when the push expires
|
|
522
|
+
# @return [self] returns self for method chaining
|
|
523
|
+
# @example
|
|
524
|
+
# push.expires_at(2.hours.from_now).send!
|
|
525
|
+
def expires_at(time)
|
|
526
|
+
self.expiration_time = time
|
|
527
|
+
self
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Set the expiration interval for this push notification.
|
|
531
|
+
# The push will expire after this many seconds from now.
|
|
532
|
+
# @param seconds [Integer] number of seconds until expiration
|
|
533
|
+
# @return [self] returns self for method chaining
|
|
534
|
+
# @example
|
|
535
|
+
# push.expires_in(3600).send! # Expires in 1 hour
|
|
536
|
+
# push.expires_in(86400).send! # Expires in 24 hours
|
|
537
|
+
def expires_in(seconds)
|
|
538
|
+
self.expiration_interval = seconds.to_i
|
|
539
|
+
self
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Mark this as a silent push notification (iOS content-available).
|
|
543
|
+
# Silent pushes wake the app in the background without displaying an alert.
|
|
544
|
+
# @return [self] returns self for method chaining
|
|
545
|
+
# @example
|
|
546
|
+
# push.silent!.with_data(action: "sync").send!
|
|
547
|
+
# @see https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app
|
|
548
|
+
def silent!
|
|
549
|
+
@content_available = true
|
|
550
|
+
self
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# =========================================================================
|
|
554
|
+
# Rich Push Methods (iOS Notification Service Extension)
|
|
555
|
+
# =========================================================================
|
|
556
|
+
|
|
557
|
+
# Add an image attachment to the push notification.
|
|
558
|
+
# This automatically enables mutable-content for iOS service extension processing.
|
|
559
|
+
# @param url [String] the URL of the image to attach
|
|
560
|
+
# @return [self] returns self for method chaining
|
|
561
|
+
# @example
|
|
562
|
+
# push.with_image("https://example.com/image.jpg").with_alert("Check this out!").send!
|
|
563
|
+
# @see https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications
|
|
564
|
+
def with_image(url)
|
|
565
|
+
@image_url = url
|
|
566
|
+
@mutable_content = true
|
|
567
|
+
self
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Set the notification category for action buttons (iOS).
|
|
571
|
+
# Categories must be registered in the app's notification settings.
|
|
572
|
+
# @param category_name [String] the notification category identifier
|
|
573
|
+
# @return [self] returns self for method chaining
|
|
574
|
+
# @example
|
|
575
|
+
# push.with_category("MESSAGE_ACTIONS").with_alert("New message").send!
|
|
576
|
+
# @see https://developer.apple.com/documentation/usernotifications/declaring_your_actionable_notification_types
|
|
577
|
+
def with_category(category_name)
|
|
578
|
+
@category = category_name
|
|
579
|
+
self
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Enable mutable-content for iOS notification service extension.
|
|
583
|
+
# This allows the notification to be modified by a service extension before display.
|
|
584
|
+
# @return [self] returns self for method chaining
|
|
585
|
+
# @example
|
|
586
|
+
# push.mutable!.with_data(encrypted_body: "...").send!
|
|
587
|
+
def mutable!
|
|
588
|
+
@mutable_content = true
|
|
589
|
+
self
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Send the push notification, raising an error on failure.
|
|
593
|
+
# This is the bang version that raises {Parse::Error} if the push fails.
|
|
594
|
+
# @return [Parse::Response] the response from the Parse server
|
|
595
|
+
# @raise [BroadcastNotAllowed] if the push has no `where` constraints
|
|
596
|
+
# and no `channels`, and neither {Parse::Push.allow_broadcast} nor
|
|
597
|
+
# per-instance {#broadcast!} was set.
|
|
598
|
+
# @raise [Parse::Error] if the push notification fails
|
|
599
|
+
# @example
|
|
600
|
+
# push.with_alert("Hello!").send!
|
|
601
|
+
def send!
|
|
602
|
+
assert_broadcast_allowed!
|
|
603
|
+
response = client.push(payload.as_json)
|
|
604
|
+
if response.error?
|
|
605
|
+
raise Parse::Error.new(response.code, response.error)
|
|
606
|
+
end
|
|
607
|
+
response
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
# =========================================================================
|
|
611
|
+
# Localization Methods
|
|
612
|
+
# =========================================================================
|
|
613
|
+
|
|
614
|
+
# Add a localized alert message for a specific language.
|
|
615
|
+
# Parse Server will automatically send the appropriate message based on device locale.
|
|
616
|
+
# @param lang [String, Symbol] the language code (e.g., :en, :fr, :es, :de)
|
|
617
|
+
# @param message [String] the alert message in that language
|
|
618
|
+
# @return [self] returns self for method chaining
|
|
619
|
+
# @example
|
|
620
|
+
# push.with_localized_alert(:en, "Hello!")
|
|
621
|
+
# .with_localized_alert(:fr, "Bonjour!")
|
|
622
|
+
# .with_localized_alert(:es, "Hola!")
|
|
623
|
+
# .send!
|
|
624
|
+
def with_localized_alert(lang, message)
|
|
625
|
+
@localized_alerts ||= {}
|
|
626
|
+
@localized_alerts[lang.to_s] = message
|
|
627
|
+
self
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# Add a localized title for a specific language.
|
|
631
|
+
# Parse Server will automatically send the appropriate title based on device locale.
|
|
632
|
+
# @param lang [String, Symbol] the language code (e.g., :en, :fr, :es, :de)
|
|
633
|
+
# @param title [String] the title in that language
|
|
634
|
+
# @return [self] returns self for method chaining
|
|
635
|
+
# @example
|
|
636
|
+
# push.with_localized_title(:en, "Welcome")
|
|
637
|
+
# .with_localized_title(:fr, "Bienvenue")
|
|
638
|
+
# .with_alert("Default message")
|
|
639
|
+
# .send!
|
|
640
|
+
def with_localized_title(lang, title)
|
|
641
|
+
@localized_titles ||= {}
|
|
642
|
+
@localized_titles[lang.to_s] = title
|
|
643
|
+
self
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# Set multiple localized alerts at once.
|
|
647
|
+
# @param translations [Hash] a hash of language codes to messages
|
|
648
|
+
# @return [self] returns self for method chaining
|
|
649
|
+
# @example
|
|
650
|
+
# push.with_localized_alerts(en: "Hello!", fr: "Bonjour!", es: "Hola!").send!
|
|
651
|
+
def with_localized_alerts(translations)
|
|
652
|
+
@localized_alerts ||= {}
|
|
653
|
+
translations.each { |lang, msg| @localized_alerts[lang.to_s] = msg }
|
|
654
|
+
self
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# Set multiple localized titles at once.
|
|
658
|
+
# @param translations [Hash] a hash of language codes to titles
|
|
659
|
+
# @return [self] returns self for method chaining
|
|
660
|
+
# @example
|
|
661
|
+
# push.with_localized_titles(en: "Welcome", fr: "Bienvenue").send!
|
|
662
|
+
def with_localized_titles(translations)
|
|
663
|
+
@localized_titles ||= {}
|
|
664
|
+
translations.each { |lang, title| @localized_titles[lang.to_s] = title }
|
|
665
|
+
self
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# =========================================================================
|
|
669
|
+
# Badge Increment Methods
|
|
670
|
+
# =========================================================================
|
|
671
|
+
|
|
672
|
+
# Increment the badge count instead of setting an absolute value.
|
|
673
|
+
# This is useful when you want to add to the existing badge rather than replace it.
|
|
674
|
+
# @param amount [Integer] the amount to increment by (default: 1)
|
|
675
|
+
# @return [self] returns self for method chaining
|
|
676
|
+
# @example
|
|
677
|
+
# push.increment_badge.with_alert("New message!").send! # +1
|
|
678
|
+
# push.increment_badge(5).with_alert("5 new items!").send! # +5
|
|
679
|
+
def increment_badge(amount = 1)
|
|
680
|
+
if amount == 1
|
|
681
|
+
self.badge = "Increment"
|
|
682
|
+
else
|
|
683
|
+
self.badge = { "__op" => "Increment", "amount" => amount.to_i }
|
|
684
|
+
end
|
|
685
|
+
self
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Clear the badge (set to 0).
|
|
689
|
+
# @return [self] returns self for method chaining
|
|
690
|
+
# @example
|
|
691
|
+
# push.clear_badge.silent!.send! # Clear badge silently
|
|
692
|
+
def clear_badge
|
|
693
|
+
self.badge = 0
|
|
694
|
+
self
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# =========================================================================
|
|
698
|
+
# Audience Targeting Methods
|
|
699
|
+
# =========================================================================
|
|
700
|
+
|
|
701
|
+
# Target a saved audience by name.
|
|
702
|
+
# Audiences are pre-defined in the _Audience collection and can be reused.
|
|
703
|
+
# Uses caching by default for better performance.
|
|
704
|
+
#
|
|
705
|
+
# @param audience_name [String] the name of the saved audience
|
|
706
|
+
# @param cache [Boolean] whether to use audience cache (default: true)
|
|
707
|
+
# @return [self] returns self for method chaining
|
|
708
|
+
# @raise [AudienceNotFound] if no audience exists with the given name.
|
|
709
|
+
# Previously this method emitted a `warn` and returned `self`, which
|
|
710
|
+
# let the subsequent `send!` broadcast to every Installation. The
|
|
711
|
+
# raise makes typos and renames loud at the call site.
|
|
712
|
+
# @example
|
|
713
|
+
# push.to_audience("VIP Users").with_alert("Exclusive offer!").send!
|
|
714
|
+
# @note The audience must exist in the _Audience collection
|
|
715
|
+
def to_audience(audience_name, cache: true)
|
|
716
|
+
# Use cached audience lookup for better performance
|
|
717
|
+
audience = Parse::Audience.find_by_name(audience_name, cache: cache)
|
|
718
|
+
|
|
719
|
+
if audience.nil?
|
|
720
|
+
raise AudienceNotFound,
|
|
721
|
+
"Audience '#{audience_name}' not found in _Audience collection"
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
if audience.query_constraint.present?
|
|
725
|
+
# Merge the audience's query constraints into our query
|
|
726
|
+
audience.query_constraint.each do |key, value|
|
|
727
|
+
query.where(key.to_sym => value)
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
self
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Target a saved audience by its object ID.
|
|
734
|
+
# @param audience_id [String] the objectId of the saved audience
|
|
735
|
+
# @return [self] returns self for method chaining
|
|
736
|
+
# @raise [AudienceNotFound] if no audience exists with the given id.
|
|
737
|
+
# @example
|
|
738
|
+
# push.to_audience_id("abc123").with_alert("Hello!").send!
|
|
739
|
+
def to_audience_id(audience_id)
|
|
740
|
+
audience = Parse::Audience.find(audience_id)
|
|
741
|
+
if audience.nil?
|
|
742
|
+
raise AudienceNotFound,
|
|
743
|
+
"Audience id '#{audience_id}' not found in _Audience collection"
|
|
744
|
+
end
|
|
745
|
+
if audience.query_constraint.present?
|
|
746
|
+
audience.query_constraint.each do |key, value|
|
|
747
|
+
query.where(key.to_sym => value)
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
self
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
# =========================================================================
|
|
754
|
+
# User Targeting Methods
|
|
755
|
+
# =========================================================================
|
|
756
|
+
|
|
757
|
+
# Target installations belonging to a specific user (or multiple users).
|
|
758
|
+
# This queries the Installation collection for devices where the user pointer
|
|
759
|
+
# matches the given user(s).
|
|
760
|
+
#
|
|
761
|
+
# @param user [Parse::User, Hash, String, Array] the user(s) to target. Can be:
|
|
762
|
+
# - A Parse::User object
|
|
763
|
+
# - A pointer hash (e.g., { "__type" => "Pointer", "className" => "_User", "objectId" => "abc123" })
|
|
764
|
+
# - A user objectId string (will be converted to a pointer)
|
|
765
|
+
# - An array of any of the above (delegates to to_users)
|
|
766
|
+
# @return [self] returns self for method chaining
|
|
767
|
+
# @example With a Parse::User object
|
|
768
|
+
# user = Parse::User.find("abc123")
|
|
769
|
+
# Parse::Push.new.to_user(user).with_alert("Hello!").send!
|
|
770
|
+
#
|
|
771
|
+
# @example With a user objectId
|
|
772
|
+
# Parse::Push.new.to_user("abc123").with_alert("Hello!").send!
|
|
773
|
+
#
|
|
774
|
+
# @example With an array of users
|
|
775
|
+
# Parse::Push.new.to_user([user1, user2]).with_alert("Hello!").send!
|
|
776
|
+
#
|
|
777
|
+
# @example Using class method shortcut
|
|
778
|
+
# Parse::Push.to_user(current_user).with_alert("Welcome back!").send!
|
|
779
|
+
def to_user(user)
|
|
780
|
+
# Delegate to to_users if given an array
|
|
781
|
+
return to_users(user) if user.is_a?(Array)
|
|
782
|
+
|
|
783
|
+
pointer = case user
|
|
784
|
+
when Parse::User
|
|
785
|
+
user.pointer
|
|
786
|
+
when Hash
|
|
787
|
+
user
|
|
788
|
+
when String
|
|
789
|
+
Parse::Pointer.new(Parse::Model::CLASS_USER, user).to_h
|
|
790
|
+
else
|
|
791
|
+
raise ArgumentError, "Expected Parse::User, Hash, String, or Array, got #{user.class}"
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
query.where(user: pointer)
|
|
795
|
+
self
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
# Target installations belonging to a user by their objectId.
|
|
799
|
+
# This is a convenience method equivalent to to_user with a string ID.
|
|
800
|
+
#
|
|
801
|
+
# @param user_id [String] the objectId of the user to target
|
|
802
|
+
# @return [self] returns self for method chaining
|
|
803
|
+
# @example
|
|
804
|
+
# Parse::Push.new.to_user_id("abc123").with_alert("Hello!").send!
|
|
805
|
+
#
|
|
806
|
+
# @example Using class method shortcut
|
|
807
|
+
# Parse::Push.to_user_id("abc123").with_alert("You have a message").send!
|
|
808
|
+
def to_user_id(user_id)
|
|
809
|
+
pointer = Parse::Pointer.new(Parse::Model::CLASS_USER, user_id).to_h
|
|
810
|
+
query.where(user: pointer)
|
|
811
|
+
self
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
# Target installations belonging to multiple users.
|
|
815
|
+
# This queries the Installation collection for devices where the user pointer
|
|
816
|
+
# matches any of the given users.
|
|
817
|
+
#
|
|
818
|
+
# @param users [Array<Parse::User, Hash, String>] the users to target
|
|
819
|
+
# @return [self] returns self for method chaining
|
|
820
|
+
# @example
|
|
821
|
+
# Parse::Push.new.to_users(user1, user2, user3).with_alert("Group message!").send!
|
|
822
|
+
#
|
|
823
|
+
# @example With user IDs
|
|
824
|
+
# Parse::Push.new.to_users("id1", "id2", "id3").with_alert("Hello everyone!").send!
|
|
825
|
+
def to_users(*users)
|
|
826
|
+
pointers = users.flatten.map do |user|
|
|
827
|
+
case user
|
|
828
|
+
when Parse::User
|
|
829
|
+
user.pointer
|
|
830
|
+
when Hash
|
|
831
|
+
user
|
|
832
|
+
when String
|
|
833
|
+
Parse::Pointer.new(Parse::Model::CLASS_USER, user).to_h
|
|
834
|
+
else
|
|
835
|
+
raise ArgumentError, "Expected Parse::User, Hash, or String, got #{user.class}"
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
query.where(:user.in => pointers)
|
|
840
|
+
self
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
# =========================================================================
|
|
844
|
+
# Installation Targeting Methods
|
|
845
|
+
# =========================================================================
|
|
846
|
+
|
|
847
|
+
# Target a specific installation (or multiple installations) by object or objectId.
|
|
848
|
+
# This directly targets device installation(s).
|
|
849
|
+
#
|
|
850
|
+
# When given a Parse::Installation object, this method validates:
|
|
851
|
+
# - The installation has a device_token (raises ArgumentError if missing)
|
|
852
|
+
# - The device_type is supported for push (warns if unsupported)
|
|
853
|
+
#
|
|
854
|
+
# @param installation [Parse::Installation, Hash, String, Array] the installation(s) to target. Can be:
|
|
855
|
+
# - A Parse::Installation object
|
|
856
|
+
# - A hash with objectId key
|
|
857
|
+
# - An objectId string
|
|
858
|
+
# - An array of any of the above (delegates to to_installations)
|
|
859
|
+
# @return [self] returns self for method chaining
|
|
860
|
+
# @raise [ArgumentError] if installation object has no device_token
|
|
861
|
+
# @example With a Parse::Installation object
|
|
862
|
+
# device = Parse::Installation.find("abc123")
|
|
863
|
+
# Parse::Push.new.to_installation(device).with_alert("Hello!").send!
|
|
864
|
+
#
|
|
865
|
+
# @example With an objectId
|
|
866
|
+
# Parse::Push.new.to_installation("abc123").with_alert("Hello!").send!
|
|
867
|
+
#
|
|
868
|
+
# @example With an array of installations
|
|
869
|
+
# Parse::Push.new.to_installation([device1, device2]).with_alert("Hello!").send!
|
|
870
|
+
#
|
|
871
|
+
# @example Using class method shortcut
|
|
872
|
+
# Parse::Push.to_installation(device).with_alert("Device notification").send!
|
|
873
|
+
def to_installation(installation)
|
|
874
|
+
# Delegate to to_installations if given an array
|
|
875
|
+
return to_installations(installation) if installation.is_a?(Array)
|
|
876
|
+
|
|
877
|
+
object_id = case installation
|
|
878
|
+
when Parse::Installation
|
|
879
|
+
validate_installation_for_push!(installation)
|
|
880
|
+
installation.id
|
|
881
|
+
when Hash
|
|
882
|
+
installation[:objectId] || installation["objectId"] || installation[:id] || installation["id"]
|
|
883
|
+
when String
|
|
884
|
+
installation
|
|
885
|
+
else
|
|
886
|
+
raise ArgumentError, "Expected Parse::Installation, Hash, String, or Array, got #{installation.class}"
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
query.where(objectId: object_id)
|
|
890
|
+
self
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
# Target a specific installation by its objectId.
|
|
894
|
+
# This is a convenience method equivalent to to_installation with a string ID.
|
|
895
|
+
#
|
|
896
|
+
# @param installation_id [String] the objectId of the installation to target
|
|
897
|
+
# @return [self] returns self for method chaining
|
|
898
|
+
# @example
|
|
899
|
+
# Parse::Push.new.to_installation_id("abc123").with_alert("Hello!").send!
|
|
900
|
+
#
|
|
901
|
+
# @example Using class method shortcut
|
|
902
|
+
# Parse::Push.to_installation_id("abc123").with_alert("Device notification").send!
|
|
903
|
+
def to_installation_id(installation_id)
|
|
904
|
+
query.where(objectId: installation_id)
|
|
905
|
+
self
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
# Target multiple installations.
|
|
909
|
+
# This queries the Installation collection for devices matching any of the given
|
|
910
|
+
# installation objectIds.
|
|
911
|
+
#
|
|
912
|
+
# When given Parse::Installation objects, this method validates each:
|
|
913
|
+
# - The installation has a device_token (raises ArgumentError if missing)
|
|
914
|
+
# - The device_type is supported for push (warns if unsupported)
|
|
915
|
+
#
|
|
916
|
+
# @param installations [Array<Parse::Installation, Hash, String>] the installations to target
|
|
917
|
+
# @return [self] returns self for method chaining
|
|
918
|
+
# @raise [ArgumentError] if any installation object has no device_token
|
|
919
|
+
# @example
|
|
920
|
+
# Parse::Push.new.to_installations(device1, device2, device3).with_alert("Group notification!").send!
|
|
921
|
+
#
|
|
922
|
+
# @example With objectIds
|
|
923
|
+
# Parse::Push.new.to_installations("id1", "id2", "id3").with_alert("Hello devices!").send!
|
|
924
|
+
def to_installations(*installations)
|
|
925
|
+
object_ids = installations.flatten.map do |installation|
|
|
926
|
+
case installation
|
|
927
|
+
when Parse::Installation
|
|
928
|
+
validate_installation_for_push!(installation)
|
|
929
|
+
installation.id
|
|
930
|
+
when Hash
|
|
931
|
+
installation[:objectId] || installation["objectId"] || installation[:id] || installation["id"]
|
|
932
|
+
when String
|
|
933
|
+
installation
|
|
934
|
+
else
|
|
935
|
+
raise ArgumentError, "Expected Parse::Installation, Hash, or String, got #{installation.class}"
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
query.where(:objectId.in => object_ids)
|
|
940
|
+
self
|
|
941
|
+
end
|
|
942
|
+
|
|
943
|
+
private
|
|
944
|
+
|
|
945
|
+
# Validate that an installation can receive push notifications.
|
|
946
|
+
# @param installation [Parse::Installation] the installation to validate
|
|
947
|
+
# @raise [ArgumentError] if the installation has no device_token
|
|
948
|
+
# @return [void]
|
|
949
|
+
def validate_installation_for_push!(installation)
|
|
950
|
+
# Access instance variables directly to avoid triggering autofetch
|
|
951
|
+
device_token = installation.instance_variable_get(:@device_token)
|
|
952
|
+
device_type = installation.instance_variable_get(:@device_type).to_s
|
|
953
|
+
installation_id = installation.id
|
|
954
|
+
|
|
955
|
+
# Check for device_token - required for push delivery
|
|
956
|
+
if device_token.blank?
|
|
957
|
+
raise ArgumentError,
|
|
958
|
+
"Cannot send push to installation #{installation_id}: missing device_token. " \
|
|
959
|
+
"Push notifications require a valid device_token."
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
# Check for unsupported device types - warn but allow
|
|
963
|
+
if device_type.present? && !SUPPORTED_PUSH_DEVICE_TYPES.include?(device_type)
|
|
964
|
+
if UNSUPPORTED_PUSH_DEVICE_TYPES.include?(device_type)
|
|
965
|
+
warn "[Parse::Push] Warning: device_type '#{device_type}' may not be supported for push notifications. " \
|
|
966
|
+
"Supported types: #{SUPPORTED_PUSH_DEVICE_TYPES.join(', ')}"
|
|
967
|
+
else
|
|
968
|
+
warn "[Parse::Push] Warning: unknown device_type '#{device_type}' for installation #{installation_id}. " \
|
|
969
|
+
"This device type may not receive push notifications. " \
|
|
970
|
+
"Supported types: #{SUPPORTED_PUSH_DEVICE_TYPES.join(', ')}"
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
end
|
|
974
|
+
end
|
|
975
|
+
end
|