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 +4 -4
- data/README.md +35 -59
- data/app/controllers/concerns/maia/controller.rb +0 -5
- data/app/models/maia/device.rb +4 -0
- data/lib/maia.rb +23 -11
- data/lib/maia/devices.rb +21 -0
- data/lib/maia/error/generic.rb +7 -0
- data/lib/maia/error/no_credentials.rb +9 -0
- data/lib/maia/error/unregistered.rb +12 -0
- data/lib/maia/fcm/connection.rb +18 -18
- data/lib/maia/fcm/credentials.rb +41 -0
- data/lib/maia/fcm/gateway.rb +30 -0
- data/lib/maia/fcm/notification.rb +14 -12
- data/lib/maia/fcm/platform/android.rb +37 -0
- data/lib/maia/fcm/platform/apns.rb +42 -0
- data/lib/maia/fcm/response.rb +17 -31
- data/lib/maia/fcm/serializer.rb +38 -0
- data/lib/maia/message.rb +24 -65
- data/lib/maia/messengers/activejob.rb +19 -0
- data/lib/maia/messengers/array.rb +21 -0
- data/lib/maia/messengers/inline.rb +13 -0
- data/lib/maia/token.rb +21 -0
- data/lib/maia/topic.rb +21 -0
- data/lib/maia/version.rb +1 -1
- metadata +21 -15
- data/lib/maia/dry_run.rb +0 -15
- data/lib/maia/error.rb +0 -4
- data/lib/maia/fcm.rb +0 -7
- data/lib/maia/fcm/response_collection.rb +0 -34
- data/lib/maia/fcm/result.rb +0 -31
- data/lib/maia/fcm/result_collection.rb +0 -35
- data/lib/maia/fcm/service.rb +0 -48
- data/lib/maia/messenger.rb +0 -61
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c639cbbcef83011669667b77953c031472de6770f2a9f84bc742530c90420ef6
|
4
|
+
data.tar.gz: 22aace941c9ab9957ce5b4d692c088eb9f34bb45aa6df3ab2bfc9ba51ac9c695
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
19
|
+
### Messenger configuration
|
17
20
|
|
18
|
-
Maia
|
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
|
-
|
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
|
-
```
|
23
|
-
Maia
|
36
|
+
```
|
37
|
+
Maia.gateway = Maia::FCM::Gateway.new CustomFCMCredentials.new
|
24
38
|
```
|
25
39
|
|
26
|
-
|
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
|
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
|
97
|
+
def image
|
91
98
|
'icn_maia'
|
92
99
|
end
|
93
100
|
|
94
|
-
#
|
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
|
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
|
-
|
146
|
+
You can also send a message directly to a raw token:
|
173
147
|
|
174
|
-
|
148
|
+
```ruby
|
149
|
+
ExampleMessage.new(...).send_to token: 'token123'
|
150
|
+
```
|
175
151
|
|
176
|
-
|
152
|
+
or to a topic:
|
177
153
|
|
178
154
|
```ruby
|
179
|
-
ExampleMessage.new(...).send_to
|
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
|
data/app/models/maia/device.rb
CHANGED
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/
|
9
|
-
require 'maia/
|
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/
|
17
|
-
require 'maia/fcm/
|
18
|
-
require 'maia/fcm/
|
23
|
+
require 'maia/fcm/serializer'
|
24
|
+
require 'maia/fcm/platform/android'
|
25
|
+
require 'maia/fcm/platform/apns'
|
19
26
|
|
20
27
|
module Maia
|
21
|
-
|
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
|
data/lib/maia/devices.rb
ADDED
@@ -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
|
data/lib/maia/fcm/connection.rb
CHANGED
@@ -1,34 +1,34 @@
|
|
1
1
|
module Maia
|
2
2
|
module FCM
|
3
3
|
class Connection
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
11
|
+
request.body = payload
|
13
12
|
http.request request
|
14
13
|
end
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
private
|
16
|
+
def uri
|
17
|
+
URI("https://fcm.googleapis.com/v1/projects/#{@project}/messages:send")
|
18
|
+
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
20
|
+
def headers
|
21
|
+
{
|
22
|
+
'Content-Type' => 'application/json',
|
23
|
+
'Authorization' => "Bearer #{@token}"
|
24
|
+
}
|
25
|
+
end
|
26
26
|
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
5
|
-
|
6
|
-
def initialize(attributes = {})
|
7
|
-
@attributes = attributes
|
4
|
+
def initialize(message)
|
5
|
+
@message = message
|
8
6
|
end
|
9
7
|
|
10
|
-
def
|
11
|
-
@
|
8
|
+
def title
|
9
|
+
@message.title
|
12
10
|
end
|
13
11
|
|
14
|
-
def
|
15
|
-
|
12
|
+
def body
|
13
|
+
@message.body
|
16
14
|
end
|
17
15
|
|
18
|
-
def
|
19
|
-
@
|
16
|
+
def image
|
17
|
+
@message.image
|
20
18
|
end
|
21
19
|
|
22
|
-
def
|
23
|
-
|
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
|
data/lib/maia/fcm/response.rb
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
module Maia
|
2
2
|
module FCM
|
3
3
|
class Response
|
4
|
-
|
4
|
+
def initialize(response)
|
5
|
+
@response = response
|
6
|
+
end
|
5
7
|
|
6
|
-
def
|
7
|
-
@
|
8
|
-
@tokens = tokens
|
8
|
+
def body
|
9
|
+
@response.body
|
9
10
|
end
|
10
11
|
|
11
12
|
def status
|
12
|
-
|
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
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
data/lib/maia/message.rb
CHANGED
@@ -1,82 +1,41 @@
|
|
1
1
|
module Maia
|
2
2
|
class Message
|
3
|
-
def
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
44
|
-
|
15
|
+
def sound
|
16
|
+
'default'
|
45
17
|
end
|
46
18
|
|
47
|
-
def
|
48
|
-
|
19
|
+
def targeting(target)
|
20
|
+
tap { @target = target }
|
49
21
|
end
|
50
22
|
|
51
|
-
def
|
52
|
-
|
23
|
+
def to_json
|
24
|
+
to_h.to_json
|
53
25
|
end
|
54
26
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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
|
data/lib/maia/token.rb
ADDED
data/lib/maia/topic.rb
ADDED
data/lib/maia/version.rb
CHANGED
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
|
+
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-
|
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:
|
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: :
|
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:
|
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:
|
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
|
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/
|
142
|
+
- lib/maia/devices.rb
|
143
143
|
- lib/maia/engine.rb
|
144
|
-
- lib/maia/error.rb
|
145
|
-
- lib/maia/
|
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/
|
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/
|
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:
|
data/lib/maia/dry_run.rb
DELETED
data/lib/maia/error.rb
DELETED
data/lib/maia/fcm.rb
DELETED
@@ -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
|
data/lib/maia/fcm/result.rb
DELETED
@@ -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
|
data/lib/maia/fcm/service.rb
DELETED
@@ -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
|
data/lib/maia/messenger.rb
DELETED
@@ -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
|