maia 4.0.3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1164306f881ef2bbcafa53947387cd80389f0f41c7b7cc3b1b5c685ac2d52ad3
4
- data.tar.gz: a0b5faf5699c23b693c161906ab14546e0a96656a88307e8d96cac82d81ff78e
3
+ metadata.gz: c639cbbcef83011669667b77953c031472de6770f2a9f84bc742530c90420ef6
4
+ data.tar.gz: 22aace941c9ab9957ce5b4d692c088eb9f34bb45aa6df3ab2bfc9ba51ac9c695
5
5
  SHA512:
6
- metadata.gz: 7b2b6a594dafbd284acfed06d67714910f04ee2795bd40b261260366ebd930334b649a2b4ed28a12a7cf9610a7931963445a2e32156ed11a930216c2ee773845
7
- data.tar.gz: f8c80d2f8f1cfcb57cbad64db76b7cfc6600f161141982285221157fb0540f899b8950f03485089504e02b2f4796df6fe6011a9abacd80afed7de2432c2182d0
6
+ metadata.gz: 4a9c6e9b6feafc62fbd044848687685eb1882bc7e513e484a34da2c57103132440e33c1ee4c643e161f694f9a4569ba8c7d52ad41adf26e018870ec3b0d99658
7
+ data.tar.gz: ff3bf75e9238f9a4861626b7372ea3abb1c2e3b27458e8108fa84c39b00c5648c1737557d2afca35f067710188d6925761f5fa4e17b6ab2cb376fdc9ff5d2398
data/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  This project maintains a `Maia::Device` model and facilitates the delivery of push notifications for iOS and Android through FCM (Firebase Cloud Messaging).
4
4
 
5
+ As of Maia 5, only the FCM HTTP v1 is supported. Use an older version of Maia if
6
+ you need to use the FCM legacy API.
7
+
5
8
  ## Installation
6
9
 
7
10
  ```
@@ -13,27 +16,39 @@ bin/rake db:migrate
13
16
 
14
17
  This will copy the `maia_devices` table into your project.
15
18
 
16
- ## Setup
19
+ ### Messenger configuration
17
20
 
18
- Maia relies on [ActiveJob](https://github.com/rails/rails/tree/master/activejob) to enqueue messages. Ensure your application is properly setup with ActiveJob!
21
+ Maia is setup to use [ActiveJob](https://github.com/rails/rails/tree/master/activejob)
22
+ as it's default messenger. If you want to send messages inline instead, use the inline adapter:
19
23
 
20
- Set your FCM key in an initializer, such as `config/initializers/maia.rb`:
24
+ ```
25
+ Maia.messenger = Maia::Messengers::Inline.new
26
+ ```
27
+
28
+ or set it to anything that responds to `#deliver(payload)`.
29
+
30
+ ### Gateway configuration
31
+
32
+ Maia uses the FCM HTTP v1 gateway by default. This assumes you are using `['GOOGLE_APPLICATION_CREDENTIALS']`
33
+ for authentication, so you should be good to go if this environment variable is set. If not, you can pass a custom
34
+ object to the FCM gateway as long as it responds to `#project` and `#token`.
21
35
 
22
- ```ruby
23
- Maia::FCM.key = Rails.application.secrets[:fcm][:key]
36
+ ```
37
+ Maia.gateway = Maia::FCM::Gateway.new CustomFCMCredentials.new
24
38
  ```
25
39
 
26
- Alternatively, you can set `ENV['FCM_KEY']`, which Maia will check first.
40
+ ### Rails configuration
27
41
 
28
42
  Include `Maia::Model` into your User model. This will attach the `has_many` relationship you need with `Maia::Device`:
29
43
 
30
44
  ```ruby
31
45
  class User
32
46
  include Maia::Model
33
- # ...
34
47
  end
35
48
  ```
36
49
 
50
+ ## Device Registration
51
+
37
52
  Create a Devices controller where you need it, which is most likely an API. The controller itself will be generated within your application so that Maia does not make any assumptions about your method of authentication, `respond_with` mimetypes, etc. The only requirement is that `current_user` exists and returns whatever model included `Maia::Model`.
38
53
 
39
54
  Here's an example of getting setup with an API Devices controller that mobile apps can register with:
@@ -50,25 +65,17 @@ class API::DevicesController
50
65
 
51
66
  Maia provides the `create` method for you, so devices can now register themselves by POSTing to that controller. If you'd like to add any other actions, feel free.
52
67
 
53
- ## Device Registration
54
-
55
68
  Devices can register with your application by submitting a POST to your devices controller with these params:
56
69
 
57
70
  ```
58
71
  { "device": { "token": "<TOKEN>" } }
59
72
  ```
60
73
 
61
- Where `<TOKEN>` is the token from FCM registration.
62
-
63
- ## Device Management
64
-
65
- When FCM responds with an invalid or unregistered device token, the device record will be destroyed from the database.
66
-
67
- When FCM responds with a canonical ID, the device record will be updated so that it's `token` field will be equal to the canonical ID given by FCM.
74
+ Where `<TOKEN>` is the token from FCM registration. Maia will automatically destroy devices when FCM responds with an `UNREGISTERED` error.
68
75
 
69
76
  ## Device Expiration
70
77
 
71
- Devices will expire after 14 days. This is to ensure user's who sell or otherwise give away their device will not be tied to that device forever. Each time a POST to Devices is received, the token expiration will be refreshed.
78
+ Devices will expire after 14 days. This is to ensure users who sell or otherwise give away their device will not be tied to that device forever. Each time a POST to Devices is received, the token expiration will be refreshed.
72
79
 
73
80
  ## Defining Messages
74
81
 
@@ -87,16 +94,16 @@ class ExampleMessage < Maia::Message
87
94
  end
88
95
 
89
96
  # Determines the icon to load on Android phones
90
- def icon
97
+ def image
91
98
  'icn_maia'
92
99
  end
93
100
 
94
- # Will use 'default' by default. Overriding to nil will prevent sound
101
+ # Sound to play on arrival (nil by default)
95
102
  def sound
96
103
  'default'
97
104
  end
98
105
 
99
- # Badge to use on iOS
106
+ # Badge to use on iOS (nil by default)
100
107
  def badge
101
108
  1
102
109
  end
@@ -106,11 +113,6 @@ class ExampleMessage < Maia::Message
106
113
  '#ffffff'
107
114
  end
108
115
 
109
- # click_action on Android, category on iOS
110
- def on_click
111
- 'SOMETHING_HAPPENED'
112
- end
113
-
114
116
  # Any additional data to send with the payload
115
117
  def data
116
118
  { foo: :bar }
@@ -122,45 +124,17 @@ class ExampleMessage < Maia::Message
122
124
  end
123
125
 
124
126
  # Override to true in order to send the iOS content-available flag
125
- def content_available?
126
- false
127
- end
128
-
129
- # Override to true in order to send a dry run push. This can help debug any device errors without actually sending a push message
130
- def dry_run?
127
+ def background?
131
128
  false
132
129
  end
133
130
  end
134
131
  ```
135
132
 
136
- This message will generate the following FCM payload:
137
-
138
- ```json
139
- {
140
- "priority": "normal",
141
- "dry_run": false,
142
- "content_available": false,
143
- "data": {
144
- "foo": "bar"
145
- },
146
- "notification": {
147
- "title": "Something happened!",
148
- "body": "'Something very important has happened, check it out!'",
149
- "icon": "icn_maia",
150
- "sound": "default",
151
- "badge": 1,
152
- "color": "#ffffff",
153
- "click_action": "SOMETHING_HAPPENED",
154
- },
155
- "registration_ids": ["<TOKEN1>", "<TOKEN2>"]
156
- }
157
- ```
158
-
159
133
  `Maia::Message` does not define a constructor so you can construct your message however you want.
160
134
 
161
135
  ## Sending messages
162
136
 
163
- `Maia::Message` provides a `send_to` that pushes the message out to a user (or collection of users). The argument to `send_to` should be a single record or relation of records.
137
+ `Maia::Message` provides a `send_to` method that pushes the message out to a user (or collection of users). The argument to `send_to` should be a single record or relation of records.
164
138
 
165
139
  For example:
166
140
 
@@ -169,14 +143,16 @@ ExampleMessage.new(...).send_to User.first
169
143
  ExampleMessage.new(...).send_to User.where(beta: true)
170
144
  ```
171
145
 
172
- `send_to` will batch users in groups of 999 tokens (FCM limitation) and send them via ActiveJob.
146
+ You can also send a message directly to a raw token:
173
147
 
174
- ## Specifying job options (`wait`, `queue`, etc)
148
+ ```ruby
149
+ ExampleMessage.new(...).send_to token: 'token123'
150
+ ```
175
151
 
176
- The `send_to` method passes it's last argument into [ActiveJob's `set` method](http://apidock.com/rails/ActiveJob/Core/ClassMethods/set), for example:
152
+ or to a topic:
177
153
 
178
154
  ```ruby
179
- ExampleMessage.new(...).send_to User.first, wait: 10.seconds, queue: :maia
155
+ ExampleMessage.new(...).send_to topic: 'my-topic'
180
156
  ```
181
157
 
182
158
  ## Sending a test push
@@ -8,7 +8,6 @@ module Maia
8
8
  update_device @device
9
9
  else
10
10
  @device = create_device_token
11
- send_dry_run_to current_user
12
11
  end
13
12
  respond_with @device
14
13
  end
@@ -38,10 +37,6 @@ module Maia
38
37
  current_user.devices.create permitted_params
39
38
  end
40
39
 
41
- def send_dry_run_to(user)
42
- Maia::DryRun.new.send_to user
43
- end
44
-
45
40
  def permitted_params
46
41
  params.require(:device).permit :token, :platform
47
42
  end
@@ -23,6 +23,10 @@ module Maia
23
23
  where(pushable: pushable).distinct
24
24
  end
25
25
 
26
+ def self.tokens
27
+ pluck(:token)
28
+ end
29
+
26
30
  def self.ios
27
31
  where platform: 'ios'
28
32
  end
@@ -1,22 +1,34 @@
1
- require 'rails'
2
- require 'active_support/core_ext/enumerable'
1
+ require 'rails/all'
3
2
 
4
3
  require 'maia/engine'
5
4
  require 'maia/message'
6
- require 'maia/messenger'
7
5
  require 'maia/poke'
8
- require 'maia/dry_run'
9
- require 'maia/error'
6
+ require 'maia/token'
7
+ require 'maia/topic'
8
+ require 'maia/devices'
9
+
10
+ require 'maia/messengers/array'
11
+ require 'maia/messengers/inline'
12
+ require 'maia/messengers/activejob'
13
+
14
+ require 'maia/error/generic'
15
+ require 'maia/error/unregistered'
16
+ require 'maia/error/no_credentials'
10
17
 
11
- require 'maia/fcm'
12
18
  require 'maia/fcm/connection'
19
+ require 'maia/fcm/credentials'
20
+ require 'maia/fcm/gateway'
13
21
  require 'maia/fcm/notification'
14
- require 'maia/fcm/response_collection'
15
22
  require 'maia/fcm/response'
16
- require 'maia/fcm/result_collection'
17
- require 'maia/fcm/result'
18
- require 'maia/fcm/service'
23
+ require 'maia/fcm/serializer'
24
+ require 'maia/fcm/platform/android'
25
+ require 'maia/fcm/platform/apns'
19
26
 
20
27
  module Maia
21
- BATCH_SIZE = 999
28
+ class << self
29
+ attr_accessor :gateway, :messenger
30
+ end
22
31
  end
32
+
33
+ Maia.gateway = Maia::FCM::Gateway.new
34
+ Maia.messenger = Maia::Messengers::ActiveJob.new
@@ -0,0 +1,21 @@
1
+ module Maia
2
+ class Devices
3
+ include Enumerable
4
+
5
+ def initialize(models)
6
+ @models = models
7
+ end
8
+
9
+ def each(&block)
10
+ tokens.each(&block)
11
+ end
12
+
13
+ def tokens
14
+ return [] if @models.empty?
15
+
16
+ Maia::Device.owned_by(@models).tokens.map do |token|
17
+ Maia::Token.new token
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ module Maia
2
+ module Error
3
+ class Generic < ::StandardError
4
+ attr_accessor :payload
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module Maia
2
+ module Error
3
+ class NoCredentials < Generic
4
+ def initialize
5
+ super 'No credentials were found for this gateway.'
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module Maia
2
+ module Error
3
+ class Unregistered < Generic
4
+ def token
5
+ json = JSON.parse(payload)
6
+ json['token']
7
+ rescue JSON::ParserError
8
+ nil
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,34 +1,34 @@
1
1
  module Maia
2
2
  module FCM
3
3
  class Connection
4
- URL = 'https://fcm.googleapis.com/fcm/send'.freeze
5
-
6
- def initialize(key)
7
- @key = key
4
+ def initialize(project, token)
5
+ @project = project
6
+ @token = token
8
7
  end
9
8
 
10
9
  def write(payload = {})
11
10
  request = Net::HTTP::Post.new uri, headers
12
- request.body = payload.to_json
11
+ request.body = payload
13
12
  http.request request
14
13
  end
15
14
 
16
- def uri
17
- URI(URL)
18
- end
15
+ private
16
+ def uri
17
+ URI("https://fcm.googleapis.com/v1/projects/#{@project}/messages:send")
18
+ end
19
19
 
20
- def headers
21
- {
22
- 'Content-Type' => 'application/json',
23
- 'Authorization' => "key=#{@key}"
24
- }
25
- end
20
+ def headers
21
+ {
22
+ 'Content-Type' => 'application/json',
23
+ 'Authorization' => "Bearer #{@token}"
24
+ }
25
+ end
26
26
 
27
- def http
28
- @_http ||= Net::HTTP.new(uri.host, uri.port).tap do |h|
29
- h.use_ssl = true
27
+ def http
28
+ @_http ||= Net::HTTP.new(uri.host, uri.port).tap do |h|
29
+ h.use_ssl = true
30
+ end
30
31
  end
31
- end
32
32
  end
33
33
  end
34
34
  end
@@ -0,0 +1,41 @@
1
+ require 'googleauth'
2
+
3
+ module Maia
4
+ module FCM
5
+ class Credentials
6
+ SCOPE = 'https://www.googleapis.com/auth/firebase.messaging'.freeze
7
+
8
+ def initialize(path = ENV['GOOGLE_APPLICATION_CREDENTIALS'])
9
+ @path = path
10
+ end
11
+
12
+ def project
13
+ to_h['project_id']
14
+ end
15
+
16
+ def token
17
+ credentials.fetch_access_token!['access_token']
18
+ end
19
+
20
+ def to_h
21
+ @to_h ||= JSON.parse file.read
22
+ end
23
+
24
+ private
25
+ def file
26
+ if @path && File.exist?(@path)
27
+ File.new @path
28
+ else
29
+ raise Maia::Error::NoCredentials
30
+ end
31
+ end
32
+
33
+ def credentials
34
+ @credentials ||= Google::Auth::ServiceAccountCredentials.make_creds(
35
+ json_key_io: file,
36
+ scope: SCOPE
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,30 @@
1
+ module Maia
2
+ module FCM
3
+ class Gateway
4
+ def initialize(auth = Maia::FCM::Credentials.new)
5
+ @auth = auth
6
+ end
7
+
8
+ def deliver(payload)
9
+ response = Maia::FCM::Response.new connection.write(payload)
10
+
11
+ if response.fail?
12
+ error = response.error
13
+ error.payload = payload
14
+ raise error
15
+ end
16
+
17
+ response
18
+ end
19
+
20
+ def serialize(message, target)
21
+ Maia::FCM::Serializer.new(message, target).to_json
22
+ end
23
+
24
+ private
25
+ def connection
26
+ @connection ||= Maia::FCM::Connection.new(@auth.project, @auth.token)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,26 +1,28 @@
1
1
  module Maia
2
2
  module FCM
3
3
  class Notification
4
- attr_accessor :attributes
5
-
6
- def initialize(attributes = {})
7
- @attributes = attributes
4
+ def initialize(message)
5
+ @message = message
8
6
  end
9
7
 
10
- def to_h
11
- @attributes
8
+ def title
9
+ @message.title
12
10
  end
13
11
 
14
- def ==(other)
15
- attributes == other.attributes
12
+ def body
13
+ @message.body
16
14
  end
17
15
 
18
- def method_missing(method_name, *args, &block)
19
- @attributes.fetch(method_name) { super }
16
+ def image
17
+ @message.image
20
18
  end
21
19
 
22
- def respond_to_missing?(method_name)
23
- @attributes.include? method_name
20
+ def to_h
21
+ {
22
+ title: title,
23
+ body: body,
24
+ image: image
25
+ }.compact
24
26
  end
25
27
  end
26
28
  end
@@ -0,0 +1,37 @@
1
+ module Maia
2
+ module FCM
3
+ module Platform
4
+ class Android
5
+ def initialize(message)
6
+ @message = message
7
+ end
8
+
9
+ def color
10
+ @message.color
11
+ end
12
+
13
+ def sound
14
+ @message.sound
15
+ end
16
+
17
+ def priority
18
+ if @message.priority == :high
19
+ :high
20
+ else
21
+ :normal
22
+ end
23
+ end
24
+
25
+ def to_h
26
+ {
27
+ priority: priority.to_s,
28
+ notification: {
29
+ color: color,
30
+ sound: sound,
31
+ }.compact
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ module Maia
2
+ module FCM
3
+ module Platform
4
+ class APNS
5
+ def initialize(message)
6
+ @message = message
7
+ end
8
+
9
+ def badge
10
+ @message.badge
11
+ end
12
+
13
+ def sound
14
+ @message.sound
15
+ end
16
+
17
+ def priority
18
+ if @message.priority == :high && !@message.background?
19
+ 10
20
+ else
21
+ 5
22
+ end
23
+ end
24
+
25
+ def to_h
26
+ {
27
+ headers: {
28
+ 'apns-priority': priority.to_s
29
+ }.compact,
30
+ payload: {
31
+ aps: {
32
+ badge: badge,
33
+ sound: sound,
34
+ 'content-available': (1 if @message.background?)
35
+ }.compact
36
+ }
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,15 +1,16 @@
1
1
  module Maia
2
2
  module FCM
3
3
  class Response
4
- attr_reader :http_response, :tokens
4
+ def initialize(response)
5
+ @response = response
6
+ end
5
7
 
6
- def initialize(http_response, tokens = [])
7
- @http_response = http_response
8
- @tokens = tokens
8
+ def body
9
+ @response.body
9
10
  end
10
11
 
11
12
  def status
12
- http_response.code
13
+ @response.code.to_i
13
14
  end
14
15
 
15
16
  def success?
@@ -20,36 +21,21 @@ module Maia
20
21
  !success?
21
22
  end
22
23
 
23
- def results
24
- @_results ||= begin
25
- results = to_h.fetch 'results', []
26
- results.map!.with_index do |attributes, i|
27
- Result.new attributes, tokens[i]
28
- end
29
- ResultCollection.new(results)
30
- end
31
- end
32
-
33
24
  def error
34
- case status
35
- when 400
36
- 'Invalid JSON was sent to FCM.'
37
- when 401
38
- 'Authentication error with FCM. Check the server whitelist and the validity of your project key.'
39
- when 500..599
40
- 'FCM Internal server error.'
25
+ case json.dig('error', 'status')
26
+ when 'UNREGISTERED'
27
+ Maia::Error::Unregistered.new
28
+ else
29
+ Maia::Error::Generic.new json.dig('error', 'message')
41
30
  end
42
31
  end
43
32
 
44
- def retry_after
45
- http_response.headers['Retry-After']
46
- end
47
-
48
- def to_h
49
- JSON.parse http_response.body
50
- rescue JSON::ParserError
51
- {}
52
- end
33
+ private
34
+ def json
35
+ JSON.parse body
36
+ rescue JSON::ParserError
37
+ {}
38
+ end
53
39
  end
54
40
  end
55
41
  end
@@ -0,0 +1,38 @@
1
+ module Maia
2
+ module FCM
3
+ class Serializer
4
+ def initialize(message, target)
5
+ @message = message
6
+ @target = target
7
+ end
8
+
9
+ def to_h
10
+ {
11
+ message: {
12
+ data: @message.data.to_h,
13
+ notification: notification.to_h,
14
+ android: android.to_h,
15
+ apns: apns.to_h
16
+ }.merge(@target.to_h)
17
+ }
18
+ end
19
+
20
+ def to_json
21
+ to_h.to_json
22
+ end
23
+
24
+ private
25
+ def notification
26
+ Maia::FCM::Notification.new @message
27
+ end
28
+
29
+ def android
30
+ Maia::FCM::Platform::Android.new @message
31
+ end
32
+
33
+ def apns
34
+ Maia::FCM::Platform::APNS.new @message
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,82 +1,41 @@
1
1
  module Maia
2
2
  class Message
3
- def send_to(pushable, job_options = {})
4
- devices = Device.owned_by pushable
5
- worker = Messenger.set job_options
6
-
7
- enqueue worker, devices.android
8
- enqueue worker, devices.ios
9
- enqueue worker, devices.unknown
10
- end
11
-
12
- def enqueue(worker, devices)
13
- devices.in_batches(of: Maia::BATCH_SIZE) do |devices|
14
- worker.perform_later devices.pluck(:token), to_h.deep_stringify_keys
15
- end
16
- end
17
-
18
- def title
19
- end
20
-
21
- def body
22
- end
23
-
24
- def on_click
25
- end
26
-
27
- def icon
28
- end
29
-
30
- def sound
31
- :default
32
- end
33
-
34
- def badge
35
- end
36
-
37
- def color
38
- end
3
+ def title; end
4
+ def body; end
5
+ def image; end
6
+ def badge; end
7
+ def color; end
8
+ def background?; end
9
+ def priority; end
39
10
 
40
11
  def data
12
+ {}
41
13
  end
42
14
 
43
- def priority
44
- :normal
15
+ def sound
16
+ 'default'
45
17
  end
46
18
 
47
- def content_available?
48
- false
19
+ def targeting(target)
20
+ tap { @target = target }
49
21
  end
50
22
 
51
- def content_mutable?
52
- false
23
+ def to_json
24
+ to_h.to_json
53
25
  end
54
26
 
55
- def dry_run?
56
- false
57
- end
27
+ def send_to(*models, topic: nil, token: nil, messenger: Maia.messenger)
28
+ targets = []
29
+ targets << Maia::Topic.new(topic) if topic
30
+ targets << Maia::Token.new(token) if token
58
31
 
59
- def notification
60
- {
61
- title: title,
62
- body: body,
63
- icon: icon,
64
- sound: sound.to_s,
65
- badge: badge,
66
- color: color,
67
- click_action: on_click
68
- }.compact
69
- end
32
+ Maia::Devices.new(models).each do |t|
33
+ targets << t
34
+ end
70
35
 
71
- def to_h
72
- {
73
- priority: priority.to_s,
74
- dry_run: dry_run?,
75
- content_available: content_available?,
76
- mutable_content: content_mutable?,
77
- data: data,
78
- notification: notification
79
- }.compact
36
+ targets.map do |target|
37
+ messenger.deliver Maia.gateway.serialize(self, target)
38
+ end
80
39
  end
81
40
  end
82
41
  end
@@ -0,0 +1,19 @@
1
+ module Maia
2
+ module Messengers
3
+ class ActiveJob
4
+ def initialize(options = {})
5
+ @options = options
6
+ end
7
+
8
+ def deliver(payload)
9
+ MessengerJob.set(@options).perform_later payload
10
+ end
11
+
12
+ class MessengerJob < ::ActiveJob::Base
13
+ def perform(payload)
14
+ Maia::Messengers::Inline.new.deliver payload
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module Maia
2
+ module Messengers
3
+ class Array
4
+ include Enumerable
5
+
6
+ attr_reader :messages
7
+
8
+ def initialize
9
+ @messages = []
10
+ end
11
+
12
+ def deliver(payload)
13
+ @messages << payload
14
+ end
15
+
16
+ def each(&block)
17
+ @messages.map { |msg| JSON.parse(msg) }.each(&block)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ module Maia
2
+ module Messengers
3
+ class Inline
4
+ def deliver(payload, gateway: Maia.gateway)
5
+ gateway.deliver payload
6
+ rescue Maia::Error::Unregistered => e
7
+ device = Maia::Device.find_by(token: e.token)
8
+ device.destroy
9
+ raise
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ module Maia
2
+ class Token
3
+ include Enumerable
4
+
5
+ def initialize(token)
6
+ @token = token
7
+ end
8
+
9
+ def each(&block)
10
+ [self].each(&block)
11
+ end
12
+
13
+ def to_s
14
+ @token
15
+ end
16
+
17
+ def to_h
18
+ { token: @token }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Maia
2
+ class Topic
3
+ include Enumerable
4
+
5
+ def initialize(topic)
6
+ @topic = topic
7
+ end
8
+
9
+ def each(&block)
10
+ [self].each(&block)
11
+ end
12
+
13
+ def to_s
14
+ @topic
15
+ end
16
+
17
+ def to_h
18
+ { topic: @topic }
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module Maia
2
- VERSION = '4.0.3'.freeze
2
+ VERSION = '5.0.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maia
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.3
4
+ version: 5.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Logan Serman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-12 00:00:00.000000000 Z
11
+ date: 2019-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -65,13 +65,13 @@ dependencies:
65
65
  - !ruby/object:Gem::Version
66
66
  version: '0'
67
67
  - !ruby/object:Gem::Dependency
68
- name: sqlite3
68
+ name: googleauth
69
69
  requirement: !ruby/object:Gem::Requirement
70
70
  requirements:
71
71
  - - ">="
72
72
  - !ruby/object:Gem::Version
73
73
  version: '0'
74
- type: :development
74
+ type: :runtime
75
75
  prerelease: false
76
76
  version_requirements: !ruby/object:Gem::Requirement
77
77
  requirements:
@@ -79,7 +79,7 @@ dependencies:
79
79
  - !ruby/object:Gem::Version
80
80
  version: '0'
81
81
  - !ruby/object:Gem::Dependency
82
- name: rspec-rails
82
+ name: sqlite3
83
83
  requirement: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - ">="
@@ -93,7 +93,7 @@ dependencies:
93
93
  - !ruby/object:Gem::Version
94
94
  version: '0'
95
95
  - !ruby/object:Gem::Dependency
96
- name: webmock
96
+ name: rspec-rails
97
97
  requirement: !ruby/object:Gem::Requirement
98
98
  requirements:
99
99
  - - ">="
@@ -107,7 +107,7 @@ dependencies:
107
107
  - !ruby/object:Gem::Version
108
108
  version: '0'
109
109
  - !ruby/object:Gem::Dependency
110
- name: webmock-rspec-helper
110
+ name: webmock
111
111
  requirement: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - ">="
@@ -139,20 +139,26 @@ files:
139
139
  - config/routes.rb
140
140
  - db/migrate/20150302191320_create_maia_devices.rb
141
141
  - lib/maia.rb
142
- - lib/maia/dry_run.rb
142
+ - lib/maia/devices.rb
143
143
  - lib/maia/engine.rb
144
- - lib/maia/error.rb
145
- - lib/maia/fcm.rb
144
+ - lib/maia/error/generic.rb
145
+ - lib/maia/error/no_credentials.rb
146
+ - lib/maia/error/unregistered.rb
146
147
  - lib/maia/fcm/connection.rb
148
+ - lib/maia/fcm/credentials.rb
149
+ - lib/maia/fcm/gateway.rb
147
150
  - lib/maia/fcm/notification.rb
151
+ - lib/maia/fcm/platform/android.rb
152
+ - lib/maia/fcm/platform/apns.rb
148
153
  - lib/maia/fcm/response.rb
149
- - lib/maia/fcm/response_collection.rb
150
- - lib/maia/fcm/result.rb
151
- - lib/maia/fcm/result_collection.rb
152
- - lib/maia/fcm/service.rb
154
+ - lib/maia/fcm/serializer.rb
153
155
  - lib/maia/message.rb
154
- - lib/maia/messenger.rb
156
+ - lib/maia/messengers/activejob.rb
157
+ - lib/maia/messengers/array.rb
158
+ - lib/maia/messengers/inline.rb
155
159
  - lib/maia/poke.rb
160
+ - lib/maia/token.rb
161
+ - lib/maia/topic.rb
156
162
  - lib/maia/version.rb
157
163
  homepage: https://github.com/lserman/maia
158
164
  licenses:
@@ -1,15 +0,0 @@
1
- module Maia
2
- class DryRun < Message
3
- def title
4
- ''
5
- end
6
-
7
- def body
8
- ''
9
- end
10
-
11
- def dry_run?
12
- true
13
- end
14
- end
15
- end
@@ -1,4 +0,0 @@
1
- module Maia
2
- class Error < StandardError
3
- end
4
- end
@@ -1,7 +0,0 @@
1
- module Maia
2
- module FCM
3
- class << self
4
- attr_accessor :key
5
- end
6
- end
7
- end
@@ -1,34 +0,0 @@
1
- module Maia
2
- module FCM
3
- class ResponseCollection
4
- include Enumerable
5
-
6
- def initialize(notification, responses = [])
7
- @notification = notification
8
- @responses = responses
9
- end
10
-
11
- def results
12
- collection = ResultCollection.new
13
- @responses.each do |response|
14
- response.results.each do |result|
15
- collection << result
16
- end
17
- end
18
- collection
19
- end
20
-
21
- def [](index)
22
- @responses[index]
23
- end
24
-
25
- def <<(response)
26
- @responses.concat Array(response).flatten
27
- end
28
-
29
- def each(&block)
30
- @responses.each(&block)
31
- end
32
- end
33
- end
34
- end
@@ -1,31 +0,0 @@
1
- module Maia
2
- module FCM
3
- class Result
4
- include ActiveModel::Model
5
-
6
- attr_accessor :message_id, :registration_id, :error
7
- attr_reader :token
8
-
9
- def initialize(attributes, token)
10
- super attributes
11
- @token = token
12
- end
13
-
14
- def success?
15
- message_id.present?
16
- end
17
-
18
- def fail?
19
- !success?
20
- end
21
-
22
- def canonical_id
23
- registration_id
24
- end
25
-
26
- def has_canonical_id?
27
- canonical_id.present?
28
- end
29
- end
30
- end
31
- end
@@ -1,35 +0,0 @@
1
- module Maia
2
- module FCM
3
- class ResultCollection
4
- include Enumerable
5
-
6
- def initialize(results = [])
7
- @results = results
8
- end
9
-
10
- def succeeded
11
- @results.select(&:success?)
12
- end
13
-
14
- def failed
15
- @results.select(&:fail?)
16
- end
17
-
18
- def with_canonical_ids
19
- @results.select(&:has_canonical_id?)
20
- end
21
-
22
- def [](index)
23
- @results[index]
24
- end
25
-
26
- def <<(result)
27
- @results << result
28
- end
29
-
30
- def each(&block)
31
- @results.each(&block)
32
- end
33
- end
34
- end
35
- end
@@ -1,48 +0,0 @@
1
- module Maia
2
- module FCM
3
- class Service
4
- def initialize
5
- @connection ||= FCM::Connection.new key
6
- end
7
-
8
- def key
9
- ENV.fetch 'FCM_KEY', Maia::FCM.key
10
- end
11
-
12
- def deliver(notification, *tokens, topic: nil)
13
- responses = ResponseCollection.new notification
14
- responses << deliver_all(notification, tokens)
15
- responses << deliver_all(notification, "/topics/#{topic}") if topic
16
- responses
17
- end
18
-
19
- private
20
- def deliver_all(notification, tokens)
21
- batch(tokens).map do |batch|
22
- if batch.one?
23
- unicast notification, batch.first
24
- elsif batch.many?
25
- multicast notification, batch
26
- end
27
- end
28
- end
29
-
30
- def unicast(notification, recipient)
31
- deliver! notification, recipient, to: recipient
32
- end
33
-
34
- def multicast(notification, recipients)
35
- deliver! notification, recipients, registration_ids: recipients
36
- end
37
-
38
- def deliver!(notification, recipients, params = {})
39
- payload = notification.to_h.merge params
40
- Response.new @connection.write(payload), Array(recipients)
41
- end
42
-
43
- def batch(recipients, batch_size: Maia::BATCH_SIZE)
44
- Array(recipients).flatten.compact.each_slice batch_size
45
- end
46
- end
47
- end
48
- end
@@ -1,61 +0,0 @@
1
- module Maia
2
- class Messenger < ActiveJob::Base
3
- def perform(tokens, payload)
4
- logger.info "Pushing to #{tokens.size} token(s)..."
5
- logger.info "Payload: #{payload}"
6
-
7
- notification = FCM::Notification.new payload
8
- responses = fcm.deliver notification, tokens
9
-
10
- responses.each do |response|
11
- raise Maia::Error, response.error if response.error
12
- handle_errors response.results.failed
13
- update_devices_to_use_canonical_ids response.results.with_canonical_ids
14
- end
15
- end
16
-
17
- private
18
- def fcm
19
- @_service ||= FCM::Service.new
20
- end
21
-
22
- def handle_errors(results)
23
- results.each do |result|
24
- device = Maia::Device.find_by token: result.token
25
- next unless device.present?
26
-
27
- if device_unrecoverable? result.error
28
- log_error "Destroying device #{device.id}", result, device
29
- device.destroy
30
- else
31
- log_error "Push to device #{device.id} failed", result, device
32
- end
33
- end
34
- end
35
-
36
- def device_unrecoverable?(error)
37
- error =~ /InvalidRegistration|NotRegistered|MismatchSenderId/
38
- end
39
-
40
- def log_error(message, result, device)
41
- logger.info "#{message} (error: #{result.error}, token: #{device.token})"
42
- end
43
-
44
- def update_devices_to_use_canonical_ids(results)
45
- results.each do |result|
46
- device = Maia::Device.find_by token: result.token
47
- next if device.nil?
48
-
49
- if user_already_has_token_registered?(device.pushable, result.canonical_id)
50
- device.destroy
51
- else
52
- device.update token: result.canonical_id
53
- end
54
- end
55
- end
56
-
57
- def user_already_has_token_registered?(user, token)
58
- user.devices.exists? token: token
59
- end
60
- end
61
- end