pushpad 1.0.0 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48d7160263b210c7897b4171bb69b21916e155e3e3c5c252a23ab99f0d64252d
4
- data.tar.gz: e06bc7a3e1d946403e45cc4ed57672d1fd97e4076d9d0538bf42ba7b13a5aaf1
3
+ metadata.gz: 6ca523c6dd48111c5a369889092c80690401158c3f0a14142eb3aee236848062
4
+ data.tar.gz: 637c8461863686d6c3c1f9f666a7df74b2053aed5017318f360054f908b83fbe
5
5
  SHA512:
6
- metadata.gz: 33bc016ccd42f4ea8cced6e3b0a98e91c22c196ff1481b504f64ae231ec0ff0d67c4e47bd5fb5fd0ab42c544d3d97e559893ed85d71cce7057e355a5d138863e
7
- data.tar.gz: 927b49b786b2ed3114d6a1c8b88dddcea72a21c6688a4bc9adc51e6206a5a0d6229134fdaace40c7687e957f0eba3a7045edb1e512ccfac4436568704bcdf803
6
+ metadata.gz: 7aaed7d9218f698803bf1adda9ba1459365b49eaba897903b1cab109aeb9bb825ab40b25119a52cc620d515dd7cce36ad8509d34baacb7b74d58028c26327207
7
+ data.tar.gz: 0e89ea2619f3c6f39c4a073c95d56faa451e672b86ad3d898e681b53c7ecef59af7e2b7b5fd22b3be957ab02cb7fcc98b6c7e4a84b5a328e853f6c5abc8905ac
@@ -10,7 +10,7 @@ jobs:
10
10
  strategy:
11
11
  fail-fast: false
12
12
  matrix:
13
- ruby-version: ['2.7', '3.0', '3.1']
13
+ ruby-version: ['3.0', '3.1', '3.2', '3.3']
14
14
  steps:
15
15
  - uses: actions/checkout@v3
16
16
  - name: Set up Ruby
data/README.md CHANGED
@@ -3,13 +3,9 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/pushpad.svg)](https://badge.fury.io/rb/pushpad)
4
4
  ![Build Status](https://github.com/pushpad/pushpad-ruby/workflows/CI/badge.svg)
5
5
 
6
- [Pushpad](https://pushpad.xyz) is a service for sending push notifications from your web app. It supports the **Push API** (Chrome, Firefox, Opera, Edge) and **APNs** (Safari desktop). iOS support is coming in 2023.
6
+ [Pushpad](https://pushpad.xyz) is a service for sending push notifications from websites and web apps. It uses the **Push API**, which is a standard supported by all major browsers (Chrome, Firefox, Opera, Edge, Safari).
7
7
 
8
- Features:
9
-
10
- - notifications are delivered even when the user is not on your website
11
- - users don't need to install any app or plugin
12
- - you can target specific users or send bulk notifications
8
+ The notifications are delivered in real time even when the users are not on your website and you can target specific users or send bulk notifications.
13
9
 
14
10
  ## Installation
15
11
 
@@ -55,17 +51,40 @@ Pushpad.signature_for current_user.id
55
51
 
56
52
  ```ruby
57
53
  notification = Pushpad::Notification.new({
54
+ # required, the main content of the notification
58
55
  body: "Hello world!",
59
- title: "Website Name", # optional, defaults to your project name
60
- target_url: "https://example.com", # optional, defaults to your project website
61
- icon_url: "https://example.com/assets/icon.png", # optional, defaults to the project icon
62
- badge_url: "https://example.com/assets/badge.png", # optional, defaults to the project badge
63
- image_url: "https://example.com/assets/image.png", # optional, an image to display in the notification content
64
- ttl: 604800, # optional, drop the notification after this number of seconds if a device is offline
65
- require_interaction: true, # optional, prevent Chrome on desktop from automatically closing the notification after a few seconds
66
- silent: false, # optional, enable this option if you want a mute notification without any sound
67
- urgent: false, # optional, enable this option only for time-sensitive alerts (e.g. incoming phone call)
68
- custom_data: "123", # optional, a string that is passed as an argument to action button callbacks
56
+
57
+ # optional, the title of the notification (defaults to your project name)
58
+ title: "Website Name",
59
+
60
+ # optional, open this link on notification click (defaults to your project website)
61
+ target_url: "https://example.com",
62
+
63
+ # optional, the icon of the notification (defaults to the project icon)
64
+ icon_url: "https://example.com/assets/icon.png",
65
+
66
+ # optional, the small icon displayed in the status bar (defaults to the project badge)
67
+ badge_url: "https://example.com/assets/badge.png",
68
+
69
+ # optional, an image to display in the notification content
70
+ # see https://pushpad.xyz/docs/sending_images
71
+ image_url: "https://example.com/assets/image.png",
72
+
73
+ # optional, drop the notification after this number of seconds if a device is offline
74
+ ttl: 604800,
75
+
76
+ # optional, prevent Chrome on desktop from automatically closing the notification after a few seconds
77
+ require_interaction: true,
78
+
79
+ # optional, enable this option if you want a mute notification without any sound
80
+ silent: false,
81
+
82
+ # optional, enable this option only for time-sensitive alerts (e.g. incoming phone call)
83
+ urgent: false,
84
+
85
+ # optional, a string that is passed as an argument to action button callbacks
86
+ custom_data: "123",
87
+
69
88
  # optional, add some action buttons to the notification
70
89
  # see https://pushpad.xyz/docs/action_buttons
71
90
  actions: [
@@ -76,10 +95,14 @@ notification = Pushpad::Notification.new({
76
95
  action: "myActionName" # optional
77
96
  }
78
97
  ],
79
- starred: true, # optional, bookmark the notification in the Pushpad dashboard (e.g. to highlight manual notifications)
98
+
99
+ # optional, bookmark the notification in the Pushpad dashboard (e.g. to highlight manual notifications)
100
+ starred: true,
101
+
80
102
  # optional, use this option only if you need to create scheduled notifications (max 5 days)
81
103
  # see https://pushpad.xyz/docs/schedule_notifications
82
104
  send_at: Time.utc(2016, 7, 25, 10, 9),
105
+
83
106
  # optional, add the notification to custom categories for stats aggregation
84
107
  # see https://pushpad.xyz/docs/monitoring
85
108
  custom_metrics: ['examples', 'another_metric'] # up to 3 metrics per notification
@@ -100,7 +123,8 @@ notification.deliver_to users, tags: ['events']
100
123
  notification.broadcast tags: ['segment1', 'segment2']
101
124
 
102
125
  # you can use boolean expressions
103
- # they must be in the disjunctive normal form (without parenthesis)
126
+ # they can include parentheses and the operators !, &&, || (from highest to lowest precedence)
127
+ # https://pushpad.xyz/docs/tags
104
128
  notification.broadcast tags: ['zip_code:28865 && !optout:local_events || friend_of:Organizer123']
105
129
  notification.deliver_to users, tags: ['tag1 && tag2', 'tag3'] # equal to 'tag1 && tag2 || tag3'
106
130
 
@@ -177,6 +201,26 @@ to get the full list in multiple requests.
177
201
  notifications = Pushpad::Notification.find_all(project_id: 5, page: 2)
178
202
  ```
179
203
 
204
+ ## Scheduled notifications
205
+
206
+ You can create scheduled notifications that will be sent in the future:
207
+
208
+ ```ruby
209
+ notification = Pushpad::Notification.new({
210
+ body: "This notification will be sent after 60 seconds",
211
+ send_at: Time.now.utc + 60
212
+ })
213
+
214
+ notification.broadcast
215
+ ```
216
+
217
+ You can also cancel a scheduled notification:
218
+
219
+ ```ruby
220
+ notification = Pushpad::Notification.find(5)
221
+ notification.cancel
222
+ ```
223
+
180
224
  ## Getting subscription count
181
225
 
182
226
  You can retrieve the number of subscriptions for a given project,
@@ -193,6 +237,29 @@ Pushpad::Subscription.count(project_id: 5, uids: ['user1'], tags: 'sports && tra
193
237
  If `Pushpad.project_id` is defined, the `project_id` option can be
194
238
  omitted.
195
239
 
240
+ ## Getting push subscription data
241
+
242
+ You can retrieve the subscriptions for a given project,
243
+ optionally filtered by `tags` or `uids`:
244
+
245
+ ```ruby
246
+ Pushpad::Subscription.find_all(project_id: 5)
247
+ Pushpad::Subscription.find_all(project_id: 5, uids: ['user1'])
248
+ Pushpad::Subscription.find_all(project_id: 5, tags: ['sports'])
249
+ Pushpad::Subscription.find_all(project_id: 5, tags: 'sports && travel')
250
+ Pushpad::Subscription.find_all(project_id: 5, uids: ['user1'], tags: 'sports && travel')
251
+ ```
252
+
253
+ If `Pushpad.project_id` is defined, the `project_id` option can be
254
+ omitted.
255
+
256
+ The REST API paginates the result set. You can pass a `page` parameter
257
+ to get the full list in multiple requests.
258
+
259
+ ```ruby
260
+ subscriptions = Pushpad::Subscription.find_all(project_id: 5, page: 2)
261
+ ```
262
+
196
263
  ## License
197
264
 
198
265
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -8,6 +8,9 @@ module Pushpad
8
8
 
9
9
  class FindError < RuntimeError
10
10
  end
11
+
12
+ class CancelError < RuntimeError
13
+ end
11
14
 
12
15
  class ReadonlyError < RuntimeError
13
16
  end
@@ -87,6 +90,14 @@ module Pushpad
87
90
  end
88
91
  deliver req_body(uids, options[:tags]), options
89
92
  end
93
+
94
+ def cancel
95
+ response = Request.delete("https://pushpad.xyz/api/v1/notifications/#{id}/cancel")
96
+
97
+ unless response.code == "204"
98
+ raise CancelError, "Response #{response.code} #{response.message}: #{response.body}"
99
+ end
100
+ end
90
101
 
91
102
  private
92
103
 
@@ -18,6 +18,10 @@ module Pushpad
18
18
  request.body = body
19
19
  end
20
20
  end
21
+
22
+ def delete(endpoint, options = {})
23
+ perform(Net::HTTP::Delete, endpoint, options)
24
+ end
21
25
 
22
26
  private
23
27
 
@@ -2,51 +2,67 @@ module Pushpad
2
2
  class Subscription
3
3
  class CountError < RuntimeError
4
4
  end
5
-
6
- def self.count(options = {})
7
- CountQuery.new(options).perform
5
+
6
+ class FindError < RuntimeError
8
7
  end
8
+
9
+ attr_reader :id, :endpoint, :p256dh, :auth, :uid, :tags, :last_click_at, :created_at
9
10
 
10
- class CountQuery
11
- attr_reader :options
11
+ def initialize(options)
12
+ @id = options[:id]
13
+ @endpoint = options[:endpoint]
14
+ @p256dh = options[:p256dh]
15
+ @auth = options[:auth]
16
+ @uid = options[:uid]
17
+ @tags = options[:tags]
18
+ @last_click_at = options[:last_click_at] && Time.parse(options[:last_click_at])
19
+ @created_at = options[:created_at] && Time.parse(options[:created_at])
20
+ end
12
21
 
13
- def initialize(options)
14
- @options = options
15
- end
22
+ def self.count(options = {})
23
+ project_id = options[:project_id] || Pushpad.project_id
24
+ raise "You must set project_id" unless project_id
16
25
 
17
- def perform
18
- project_id = options[:project_id] || Pushpad.project_id
19
- raise "You must set project_id" unless project_id
26
+ endpoint = "https://pushpad.xyz/api/v1/projects/#{project_id}/subscriptions"
27
+ response = Request.head(endpoint, query_parameters: query_parameters(options))
20
28
 
21
- endpoint = "https://pushpad.xyz/api/v1/projects/#{project_id}/subscriptions"
22
- response = Request.head(endpoint, query_parameters: query_parameters)
29
+ unless response.code == "200"
30
+ raise CountError, "Response #{response.code} #{response.message}: #{response.body}"
31
+ end
23
32
 
24
- unless response.code == "200"
25
- raise CountError, "Response #{response.code} #{response.message}: #{response.body}"
26
- end
33
+ response["X-Total-Count"].to_i
34
+ end
35
+
36
+ def self.find_all(options = {})
37
+ project_id = options[:project_id] || Pushpad.project_id
38
+ raise "You must set project_id" unless project_id
27
39
 
28
- response["X-Total-Count"].to_i
29
- end
40
+ query_parameters_with_pagination = query_parameters(options)
41
+ query_parameters_with_pagination << ["page", options[:page]] if options.key?(:page)
30
42
 
31
- private
43
+ response = Request.get("https://pushpad.xyz/api/v1/projects/#{project_id}/subscriptions",
44
+ query_parameters: query_parameters_with_pagination)
32
45
 
33
- def query_parameters
34
- [uid_query_parameters, tag_query_parameters].flatten(1)
46
+ unless response.code == "200"
47
+ raise FindError, "Response #{response.code} #{response.message}: #{response.body}"
35
48
  end
36
49
 
37
- def uid_query_parameters
38
- options.fetch(:uids, []).map { |uid| ["uids[]", uid] }
50
+ JSON.parse(response.body, symbolize_names: true).map do |attributes|
51
+ new(attributes)
39
52
  end
53
+ end
40
54
 
41
- def tag_query_parameters
42
- tags = options.fetch(:tags, [])
55
+ private
43
56
 
44
- if tags.is_a?(String)
45
- [["tags", tags]]
46
- else
47
- tags.map { |tag| ["tags[]", tag] }
48
- end
49
- end
57
+ def self.query_parameters(options)
58
+ uids = options.fetch(:uids, [])
59
+ uids_query = uids.map { |uid| ["uids[]", uid] }
60
+
61
+ tags = options.fetch(:tags, [])
62
+ tags_query = tags.is_a?(String) ? [["tags", tags]] : tags.map { |tag| ["tags[]", tag] }
63
+
64
+ [uids_query, tags_query].flatten(1)
50
65
  end
66
+
51
67
  end
52
68
  end
data/pushpad.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "pushpad"
3
- spec.version = '1.0.0'
3
+ spec.version = '1.2.0'
4
4
  spec.authors = ["Pushpad"]
5
5
  spec.email = ["support@pushpad.xyz"]
6
6
  spec.summary = "Web push notifications for Chrome, Firefox, Opera, Edge and Safari using Pushpad."
@@ -28,7 +28,6 @@ module Pushpad
28
28
  end
29
29
 
30
30
  def stub_notification_post(project_id, params = {}, response_body = "{}")
31
-
32
31
  stub_request(:post, "https://pushpad.xyz/api/v1/projects/#{project_id}/notifications").
33
32
  with(body: hash_including(params)).
34
33
  to_return(status: 201, body: response_body)
@@ -38,6 +37,16 @@ module Pushpad
38
37
  stub_request(:post, "https://pushpad.xyz/api/v1/projects/#{project_id}/notifications").
39
38
  to_return(status: 403)
40
39
  end
40
+
41
+ def stub_notification_delete(notification_id)
42
+ stub_request(:delete, "https://pushpad.xyz/api/v1/notifications/#{notification_id}/cancel").
43
+ to_return(status: 204)
44
+ end
45
+
46
+ def stub_failing_notification_delete(notification_id)
47
+ stub_request(:delete, "https://pushpad.xyz/api/v1/notifications/#{notification_id}/cancel").
48
+ to_return(status: 404)
49
+ end
41
50
 
42
51
  describe ".new" do
43
52
  it "allows delivering notifications even if an id attribute is supplied" do
@@ -409,5 +418,27 @@ module Pushpad
409
418
  end
410
419
  end
411
420
  end
421
+
422
+ describe "#cancel" do
423
+ it "cancels a scheduled notification" do
424
+ stub_notification_delete(5)
425
+
426
+ notification = Notification.new(id: 5)
427
+
428
+ res = notification.cancel
429
+ expect(res).to be_nil
430
+ end
431
+
432
+ it "fails with CancelError if response status code is not 204" do
433
+ stub_failing_notification_delete(5)
434
+
435
+ notification = Notification.new(id: 5)
436
+
437
+ expect {
438
+ notification.cancel
439
+ }.to raise_error(Notification::CancelError)
440
+ end
441
+ end
442
+
412
443
  end
413
444
  end
@@ -13,6 +13,17 @@ module Pushpad
13
13
  stub_request(:head, "https://pushpad.xyz/api/v1/projects/#{options[:project_id]}/subscriptions").
14
14
  to_return(status: 503)
15
15
  end
16
+
17
+ def stub_subscriptions_get(options)
18
+ stub_request(:get, "https://pushpad.xyz/api/v1/projects/#{options[:project_id]}/subscriptions").
19
+ with(query: hash_including(options.fetch(:query, {}))).
20
+ to_return(status: 200, body: options[:list].to_json)
21
+ end
22
+
23
+ def stub_failing_subscriptions_get(options)
24
+ stub_request(:get, "https://pushpad.xyz/api/v1/projects/#{options[:project_id]}/subscriptions").
25
+ to_return(status: 403)
26
+ end
16
27
 
17
28
  describe ".count" do
18
29
  it "returns value from X-Total-Count header" do
@@ -81,5 +92,110 @@ module Pushpad
81
92
  }.to raise_error(Subscription::CountError)
82
93
  end
83
94
  end
95
+
96
+ describe ".find_all" do
97
+ it "returns subscriptions of project with attributes from json response" do
98
+ attributes = {
99
+ id: 1169,
100
+ endpoint: "https://example.com/push/f7Q1Eyf7EyfAb1",
101
+ p256dh: "BCQVDTlYWdl05lal3lG5SKr3VxTrEWpZErbkxWrzknHrIKFwihDoZpc_2sH6Sh08h-CacUYI-H8gW4jH-uMYZQ4=",
102
+ auth: "cdKMlhgVeSPzCXZ3V7FtgQ==",
103
+ uid: "exampleUid",
104
+ tags: ["exampleTag1", "exampleTag2"],
105
+ last_click_at: "2023-11-03T10:30:00.000Z",
106
+ created_at: "2016-09-06T10:47:05.494Z"
107
+ }
108
+ stub_subscriptions_get(project_id: 10, list: [attributes])
109
+
110
+ subscriptions = Subscription.find_all(project_id: 10)
111
+
112
+ attributes.delete(:last_click_at)
113
+ attributes.delete(:created_at)
114
+ expect(subscriptions[0]).to have_attributes(attributes)
115
+ expect(subscriptions[0].last_click_at.utc.to_s).to eq(Time.utc(2023, 11, 3, 10, 30, 0.0).to_s)
116
+ expect(subscriptions[0].created_at.utc.to_s).to eq(Time.utc(2016, 9, 6, 10, 47, 5.494).to_s)
117
+ end
118
+
119
+ it "falls back to global project id" do
120
+ attributes = { id: 5 }
121
+ stub_subscriptions_get(project_id: 10, list: [attributes])
122
+
123
+ Pushpad.project_id = 10
124
+ subscriptions = Subscription.find_all
125
+
126
+ expect(subscriptions[0]).to have_attributes(attributes)
127
+ end
128
+
129
+ it "fails with helpful error message when project_id is missing" do
130
+ Pushpad.project_id = nil
131
+
132
+ expect {
133
+ Subscription.find_all
134
+ }.to raise_error(/must set project_id/)
135
+ end
136
+
137
+ it "allows passing page parameter for pagination" do
138
+ attributes = { id: 5 }
139
+ stub_subscriptions_get(project_id: 10, list: [attributes], query: { page: "3" })
140
+
141
+ subscriptions = Subscription.find_all(project_id: 10, page: 3)
142
+
143
+ expect(subscriptions[0]).to have_attributes(attributes)
144
+ end
145
+
146
+ it "fails with FindError if response status code is not 200" do
147
+ stub_failing_subscriptions_get(project_id: 10)
148
+
149
+ expect {
150
+ Subscription.find_all(project_id: 10)
151
+ }.to raise_error(Subscription::FindError)
152
+ end
153
+
154
+ it "allows passing uids" do
155
+ attributes = { id: 5 }
156
+ request = stub_subscriptions_get(project_id: 5, list: [attributes], query: { uids: ["uid0", "uid1"] })
157
+
158
+ Subscription.find_all(project_id: 5, uids: ["uid0", "uid1"])
159
+
160
+ expect(request).to have_been_made.once
161
+ end
162
+
163
+ it "allows passing tags" do
164
+ attributes = { id: 5 }
165
+ request = stub_subscriptions_get(project_id: 5, list: [attributes], query: { tags: ["sports", "travel"] })
166
+
167
+ Subscription.find_all(project_id: 5, tags: ["sports", "travel"])
168
+
169
+ expect(request).to have_been_made.once
170
+ end
171
+
172
+ it "allows passing tags as boolean expression" do
173
+ attributes = { id: 5 }
174
+ request = stub_subscriptions_get(project_id: 5, list: [attributes], query: { tags: "sports || travel" })
175
+
176
+ Subscription.find_all(project_id: 5, tags: "sports || travel")
177
+
178
+ expect(request).to have_been_made.once
179
+ end
180
+
181
+ it "allows passing tags and uids" do
182
+ attributes = { id: 5 }
183
+ request = stub_subscriptions_get(project_id: 5, list: [attributes],
184
+ query: { tags: ["sports", "travel"], uids: ["uid0"] })
185
+
186
+ Subscription.find_all(project_id: 5, tags: ["sports", "travel"], uids: ["uid0"])
187
+
188
+ expect(request).to have_been_made.once
189
+ end
190
+
191
+ it "works properly when there are no results" do
192
+ stub_subscriptions_get(project_id: 5, list: [])
193
+
194
+ subscriptions = Subscription.find_all(project_id: 5)
195
+
196
+ expect(subscriptions).to eq([])
197
+ end
198
+ end
199
+
84
200
  end
85
201
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pushpad
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pushpad
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-30 00:00:00.000000000 Z
11
+ date: 2024-09-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -38,7 +38,7 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
- description:
41
+ description:
42
42
  email:
43
43
  - support@pushpad.xyz
44
44
  executables: []
@@ -66,7 +66,7 @@ homepage: https://pushpad.xyz
66
66
  licenses:
67
67
  - MIT
68
68
  metadata: {}
69
- post_install_message:
69
+ post_install_message:
70
70
  rdoc_options: []
71
71
  require_paths:
72
72
  - lib
@@ -81,8 +81,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  requirements: []
84
- rubygems_version: 3.3.26
85
- signing_key:
84
+ rubygems_version: 3.0.3.1
85
+ signing_key:
86
86
  specification_version: 4
87
87
  summary: Web push notifications for Chrome, Firefox, Opera, Edge and Safari using
88
88
  Pushpad.