maia 4.0.0 → 5.0.1

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
- SHA1:
3
- metadata.gz: 1f16e5e9cba3546a3653e1fd2e3f87d19fa46784
4
- data.tar.gz: aa5f6350dd17689e8dd01c999c3f509680b73e80
2
+ SHA256:
3
+ metadata.gz: d3efb00ad0b208362f022cc62718c3e7acf26f850007dfa899123f5b1de33250
4
+ data.tar.gz: 77b3a882f44ee407c1bc00075ba591646097adbb284f8a7429de575b43ca28a8
5
5
  SHA512:
6
- metadata.gz: 51ae47908f1ae0e3a1e0b04eb3812023a20db60276b07fa2156583b325191ea7c3231057d0f461c36832dcfd9db6d37c831b10f1a58f5e2cd304431197f4ad71
7
- data.tar.gz: 1bbdf160cdeaefebd160966ad92c39ec18d795af55a2e0e4ec291b91e8af91dd199e0d1374cda1bea091f29f150a8f523584221c505274d12cd1ad72f9c82a9c
6
+ metadata.gz: d8ff83629565645b0b6ea9c8802974d0d924f08c596f61364956b73e92bf4610f801ba41b8a9e9b486cc8c19a0db0e1f3445d1217dda66e205554c2c28e962d3
7
+ data.tar.gz: 31da736742c1b67abbdea6863f2cd090576f7f08bde0466dabee5f1cee6e088ff6428439731886d14c060207498a673b4f5171c62f7354a1212e670f9772ca2a
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
@@ -25,7 +24,7 @@ module Maia
25
24
  end
26
25
 
27
26
  def find_device(token = params[:device][:token])
28
- current_user.devices.find_by token: token
27
+ current_user.devices.find_by! token: token
29
28
  end
30
29
 
31
30
  def update_device(device)
@@ -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
data/lib/maia.rb CHANGED
@@ -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,44 @@
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'], cache: Rails.cache)
9
+ @path = path
10
+ @cache = cache
11
+ end
12
+
13
+ def project
14
+ @project ||= to_h['project_id']
15
+ end
16
+
17
+ def token
18
+ @cache.fetch('maia-fcm-token', expires_in: 1.hour) do
19
+ credentials.fetch_access_token!['access_token']
20
+ end
21
+ end
22
+
23
+ def to_h
24
+ @to_h ||= JSON.parse file.read
25
+ end
26
+
27
+ private
28
+ def file
29
+ if @path && File.exist?(@path)
30
+ File.new @path
31
+ else
32
+ raise Maia::Error::NoCredentials
33
+ end
34
+ end
35
+
36
+ def credentials
37
+ @credentials ||= Google::Auth::ServiceAccountCredentials.make_creds(
38
+ json_key_io: file,
39
+ scope: SCOPE
40
+ )
41
+ end
42
+ end
43
+ end
44
+ 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
+ Maia::FCM::Connection.new(@auth.project, @auth.token)
27
+ end
28
+ end
29
+ end
30
+ end