pushkin-library 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +36 -0
- data/app/assets/config/pushkin_manifest.js +2 -0
- data/app/assets/javascripts/pushkin/application.js +14 -0
- data/app/assets/javascripts/pushkin/firebase/push_notifications.js +100 -0
- data/app/assets/javascripts/pushkin/main.js +5 -0
- data/app/assets/stylesheets/pushkin/application.css +15 -0
- data/app/controllers/pushkin/api/v1/concerns/api_helper.rb +43 -0
- data/app/controllers/pushkin/api/v1/concerns/tokens_helper.rb +94 -0
- data/app/controllers/pushkin/application_controller.rb +5 -0
- data/app/fabrics/pushkin/notification_fabric.rb +83 -0
- data/app/helpers/pushkin/application_helper.rb +4 -0
- data/app/jobs/pushkin/application_job.rb +4 -0
- data/app/jobs/pushkin/send_job.rb +11 -0
- data/app/mailers/pushkin/application_mailer.rb +6 -0
- data/app/models/pushkin/application_record.rb +5 -0
- data/app/models/pushkin/concerns/pushkin_user.rb +10 -0
- data/app/models/pushkin/notification.rb +42 -0
- data/app/models/pushkin/payload.rb +21 -0
- data/app/models/pushkin/push_sending_result.rb +13 -0
- data/app/models/pushkin/token.rb +19 -0
- data/app/models/pushkin/token_result.rb +39 -0
- data/app/models/pushkin/tokens_provider.rb +22 -0
- data/app/models/pushkin/tokens_provider_user.rb +12 -0
- data/app/services/pushkin/fcm/send_android_push_service.rb +22 -0
- data/app/services/pushkin/fcm/send_fcm_push_service.rb +120 -0
- data/app/services/pushkin/fcm/send_ios_push_service.rb +22 -0
- data/app/services/pushkin/fcm/send_web_push_service.rb +22 -0
- data/app/services/pushkin/send_push_service.rb +39 -0
- data/app/services/pushkin/token_filters/android_token_filter.rb +15 -0
- data/app/services/pushkin/token_filters/ios_token_filter.rb +15 -0
- data/app/services/pushkin/token_filters/token_filter.rb +24 -0
- data/app/services/pushkin/token_filters/web_token_filter.rb +15 -0
- data/app/views/layouts/pushkin/application.html.erb +14 -0
- data/lib/generators/pushkin/setup_generator.rb +27 -0
- data/lib/generators/pushkin/templates/create_pushkin_tables.rb +89 -0
- data/lib/generators/pushkin/templates/tokens_controller.rb +16 -0
- data/lib/pushkin-library.rb +1 -0
- data/lib/pushkin.rb +29 -0
- data/lib/pushkin/engine.rb +5 -0
- data/lib/pushkin/version.rb +3 -0
- data/lib/tasks/pushkin_tasks.rake +4 -0
- metadata +128 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# Уведомление для отправки
|
2
|
+
module Pushkin
|
3
|
+
class Notification < ApplicationRecord
|
4
|
+
|
5
|
+
# Предоставляет токены устройств, на которые нужно отправить уведомление.
|
6
|
+
# polymorphic сделан для того, чтобы можно было реализовывать различные логики
|
7
|
+
# получения токенов для конкретного уведомления, например:
|
8
|
+
# - В момент планирования уведомления сохранить в базу пользователей, которым нужно отправить уведомления.
|
9
|
+
# В момент отправки для этих пользователей будут получены актуальные устройства.
|
10
|
+
# Данная реализация находится в Pushkin::TokensProvider
|
11
|
+
# - В момент планирования уведомления сохранить в базу токены, на которые будет отправлено уведомление.
|
12
|
+
# - В момент планирования сохранить ссылку на какую-нибо сущность, из которой в момент отправки
|
13
|
+
# будет получен актуальный список токенов пользователей.
|
14
|
+
belongs_to :tokens_provider, polymorphic: true, dependent: :destroy
|
15
|
+
|
16
|
+
# Предоставляет данные, которые будут отправлены в пуш уведомлении.
|
17
|
+
# polymorphic нужен для того, чтобы реализовывать различные способы получения данных
|
18
|
+
# для различных уведомлений, например:
|
19
|
+
# - В момент планирования уведомления сохранить всю информацию в базу в статическом виде.
|
20
|
+
# Такая релизация находится в Pushkin::Payload
|
21
|
+
# - В момент планированя уведомления сохранить какую-либо сущность, которая в момент отправки
|
22
|
+
# уведомления предоставит нужную актуальную информацию на лету.
|
23
|
+
belongs_to :payload, polymorphic: true, dependent: :destroy, inverse_of: :notification
|
24
|
+
|
25
|
+
# Статистика по отправленным сообщениям. Один объект - одна групповая отправка.
|
26
|
+
has_many :push_sending_results, dependent: :destroy
|
27
|
+
has_many :token_results, through: :push_sending_results
|
28
|
+
|
29
|
+
# Тип уведомления для разделения уведомлений по назначениям.
|
30
|
+
# Обязательный, чтобы потом не запутаться и легко фильтроваться.
|
31
|
+
validates :notification_type, presence: true
|
32
|
+
|
33
|
+
# Отправляет уведомление прямо сейчас
|
34
|
+
def send_now
|
35
|
+
# Заполняем как дату, в которую нужно отправить, так и дату, в которую началась отправка.
|
36
|
+
# Это позволит периодической операции по отправке уведомлений не отвлекаться на такое уведомление.
|
37
|
+
now = DateTime.now
|
38
|
+
self.update_attributes(start_at: now, started_at: now)
|
39
|
+
SendJob.perform_later(self.id)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Статическая информация, отправляемая в пуш уведомлении
|
2
|
+
module Pushkin
|
3
|
+
class Payload < ApplicationRecord
|
4
|
+
|
5
|
+
# Связь с уведомлением, к которому относится данная информация
|
6
|
+
has_one :notification, as: :payload, inverse_of: :payload
|
7
|
+
|
8
|
+
validates :title, presence: true
|
9
|
+
|
10
|
+
def data
|
11
|
+
value = read_attribute(:data)
|
12
|
+
value = value.present? ? JSON.parse(value, symbolize_names: true) : {}
|
13
|
+
value
|
14
|
+
end
|
15
|
+
|
16
|
+
def data=(value)
|
17
|
+
write_attribute(:data, value.present? ? value.to_json : value)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Результат отправки на платформу
|
2
|
+
module Pushkin
|
3
|
+
class PushSendingResult < ApplicationRecord
|
4
|
+
|
5
|
+
belongs_to :notification, optional: true
|
6
|
+
|
7
|
+
has_many :token_results, dependent: :destroy
|
8
|
+
|
9
|
+
# Соответствует платформам в токенах
|
10
|
+
enum platform: [:android, :ios, :web]
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Токены для отправки пуш уведомлений в приложения
|
2
|
+
module Pushkin
|
3
|
+
class Token < ApplicationRecord
|
4
|
+
|
5
|
+
belongs_to :user, optional: true
|
6
|
+
|
7
|
+
validates :token, presence: true, uniqueness: { scope: :platform }
|
8
|
+
validates :platform, presence: true
|
9
|
+
validates_inclusion_of :is_active, in: [true, false]
|
10
|
+
|
11
|
+
enum platform: [:android, :ios, :web]
|
12
|
+
|
13
|
+
scope :active, -> { self.where(is_active: true) }
|
14
|
+
|
15
|
+
# По умолчанию нужно работать только с активными устройствами.
|
16
|
+
default_scope { self.active }
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Pushkin
|
2
|
+
class TokenResult < ApplicationRecord
|
3
|
+
|
4
|
+
STATUS_SUCCESS = "success"
|
5
|
+
STATUS_INVALID = "invalid"
|
6
|
+
STATUS_ERROR = "error"
|
7
|
+
|
8
|
+
belongs_to :push_sending_result, optional: true
|
9
|
+
belongs_to :token
|
10
|
+
|
11
|
+
scope :invalid, -> { self.where(status: STATUS_INVALID) }
|
12
|
+
|
13
|
+
def set_success
|
14
|
+
self.status = STATUS_SUCCESS
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_invalid
|
18
|
+
self.status = STATUS_INVALID
|
19
|
+
end
|
20
|
+
|
21
|
+
def set_error(error)
|
22
|
+
self.status = STATUS_ERROR
|
23
|
+
self.error = error
|
24
|
+
end
|
25
|
+
|
26
|
+
def success?
|
27
|
+
self.status == STATUS_SUCCESS
|
28
|
+
end
|
29
|
+
|
30
|
+
def invalid?
|
31
|
+
self.status == STATUS_INVALID
|
32
|
+
end
|
33
|
+
|
34
|
+
def error?
|
35
|
+
self.status == STATUS_ERROR
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Возвращает токены устройств для пользователей, которые к нему прикреплены
|
2
|
+
module Pushkin
|
3
|
+
class TokensProvider < ApplicationRecord
|
4
|
+
|
5
|
+
# Ссылка на уведомление
|
6
|
+
has_one :notification, as: :tokens_provider
|
7
|
+
|
8
|
+
# Ссылки на пользователей
|
9
|
+
has_many :tokens_provider_users, foreign_key: :tokens_provider_id,
|
10
|
+
dependent: :destroy
|
11
|
+
|
12
|
+
# Пользователи, которым нужно отправить пуш-уведомление
|
13
|
+
has_many :users, through: :tokens_provider_users
|
14
|
+
|
15
|
+
# Возвращает список токенов для отправки пуш уведомления.
|
16
|
+
# Токен - объект PushToken.
|
17
|
+
def get_tokens
|
18
|
+
self.users.includes(:pushkin_tokens).map { |user| user.pushkin_tokens }.flatten
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Связка Провайдер-Пользователь для отправки пуш уведомлений
|
2
|
+
module Pushkin
|
3
|
+
class TokensProviderUser < ApplicationRecord
|
4
|
+
|
5
|
+
# Ссылка на пользователя
|
6
|
+
belongs_to :user
|
7
|
+
|
8
|
+
# Ссылка на провайдер (Связывает уведомление и пользователей, которым нужно отправить уведомление)
|
9
|
+
belongs_to :tokens_provider, optional: true
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Pushkin
|
2
|
+
module Fcm
|
3
|
+
class SendAndroidPushService < SendFcmPushService
|
4
|
+
|
5
|
+
def get_notification_hash
|
6
|
+
notification_hash = super
|
7
|
+
notification_hash[:icon] = self.payload.android_icon if self.payload.android_icon.present?
|
8
|
+
notification_hash[:click_action] = self.payload.android_click_action if self.payload.android_click_action.present?
|
9
|
+
notification_hash
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_platform
|
13
|
+
:android
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_data_message
|
17
|
+
self.payload.is_android_data_message
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# Отправляет пуш уведомления через firebase.
|
2
|
+
# Это базовый класс для наследников, специфических для платформ
|
3
|
+
module Pushkin
|
4
|
+
module Fcm
|
5
|
+
class SendFcmPushService
|
6
|
+
|
7
|
+
FCM_API_DOMAIN = "https://fcm.googleapis.com"
|
8
|
+
FCM_API_URL = "/fcm/send"
|
9
|
+
|
10
|
+
attr_accessor :tokens, :payload
|
11
|
+
attr_accessor :server_key
|
12
|
+
|
13
|
+
def initialize(tokens, payload)
|
14
|
+
@tokens = tokens
|
15
|
+
@payload = payload
|
16
|
+
@server_key = ENV["FCM_SERVER_KEY"]
|
17
|
+
raise Exception.new("No FCM_SERVER_KEY in ENV") if @server_key.blank?
|
18
|
+
end
|
19
|
+
|
20
|
+
def call
|
21
|
+
# Сохранение в БД информации по отправке
|
22
|
+
@push_sending_result = PushSendingResult.create(started_at: DateTime.now, platform: self.get_platform)
|
23
|
+
|
24
|
+
# Выполнение HTTP запроса по отправке уведомлений
|
25
|
+
conn = Faraday.new(:url => FCM_API_DOMAIN)
|
26
|
+
response = conn.post do |req|
|
27
|
+
req.url FCM_API_URL
|
28
|
+
req.headers['Content-Type'] = 'application/json'
|
29
|
+
req.headers['Authorization'] = "key=#{self.server_key}"
|
30
|
+
req.body = self.get_request_body.to_json
|
31
|
+
end
|
32
|
+
|
33
|
+
# Сохранение результатов отправки в БД
|
34
|
+
self.process_response(response)
|
35
|
+
@push_sending_result
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_response(response)
|
39
|
+
@push_sending_result.finished_at = DateTime.now
|
40
|
+
|
41
|
+
if response.status == 200
|
42
|
+
@push_sending_result.success = true
|
43
|
+
|
44
|
+
# Обработка ответа с сохранением в БД
|
45
|
+
response_body = JSON.parse(response.body, symbolize_names: true)
|
46
|
+
response_body[:results].each_with_index.map do |result, index|
|
47
|
+
token_result = self.parse_token_result(result, self.tokens[index])
|
48
|
+
token_result.push_sending_result_id = @push_sending_result.id
|
49
|
+
token_result.save
|
50
|
+
end
|
51
|
+
else
|
52
|
+
@push_sending_result.success = false
|
53
|
+
@push_sending_result.error = response.body
|
54
|
+
end
|
55
|
+
|
56
|
+
@push_sending_result.save
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_token_result(result, token)
|
60
|
+
error = result[:error]
|
61
|
+
token_result = TokenResult.new(token_id: token.id)
|
62
|
+
|
63
|
+
if error.blank?
|
64
|
+
token_result.set_success
|
65
|
+
elsif error == "NotRegistered" || error == "InvalidRegistration"
|
66
|
+
token_result.set_invalid
|
67
|
+
else
|
68
|
+
token_result.set_error(error)
|
69
|
+
end
|
70
|
+
|
71
|
+
token_result.save
|
72
|
+
token_result
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_request_body
|
76
|
+
body = {
|
77
|
+
registration_ids: self.tokens.map { |token_object| token_object.token },
|
78
|
+
content_available: self.is_data_message
|
79
|
+
}
|
80
|
+
|
81
|
+
notification_hash = self.get_notification_hash
|
82
|
+
data_hash = self.get_data_hash
|
83
|
+
|
84
|
+
if self.is_data_message
|
85
|
+
data_hash = data_hash.merge(notification_hash)
|
86
|
+
notification_hash = nil
|
87
|
+
end
|
88
|
+
|
89
|
+
body[:notification] = notification_hash if notification_hash.present?
|
90
|
+
body[:data] = data_hash if data_hash.present?
|
91
|
+
|
92
|
+
body
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_notification_hash
|
96
|
+
return {
|
97
|
+
title: self.payload.title,
|
98
|
+
body: self.payload.body,
|
99
|
+
tag: self.payload.notification.id.to_s
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_data_hash
|
104
|
+
(self.payload.data || {}).merge({
|
105
|
+
notification_type: self.payload.notification.notification_type,
|
106
|
+
notification_id: self.payload.notification.id
|
107
|
+
})
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_platform
|
111
|
+
raise Exception.new("You must implement 'get_platform' method in child class")
|
112
|
+
end
|
113
|
+
|
114
|
+
def is_data_message
|
115
|
+
raise Exception.new("You must implement 'is_data_message' method in child class")
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Pushkin
|
2
|
+
module Fcm
|
3
|
+
class SendIosPushService < SendFcmPushService
|
4
|
+
|
5
|
+
def get_notification_hash
|
6
|
+
notification_hash = super
|
7
|
+
notification_hash[:icon] = self.payload.ios_icon if self.payload.ios_icon.present?
|
8
|
+
notification_hash[:click_action] = self.payload.ios_click_action if self.payload.ios_click_action.present?
|
9
|
+
notification_hash
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_platform
|
13
|
+
:ios
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_data_message
|
17
|
+
self.payload.is_ios_data_message
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Pushkin
|
2
|
+
module Fcm
|
3
|
+
class SendWebPushService < SendFcmPushService
|
4
|
+
|
5
|
+
def get_notification_hash
|
6
|
+
notification_hash = super
|
7
|
+
notification_hash[:icon] = self.payload.web_icon if self.payload.web_icon.present?
|
8
|
+
notification_hash[:click_action] = self.payload.web_click_action if self.payload.web_click_action.present?
|
9
|
+
notification_hash
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_platform
|
13
|
+
:web
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_data_message
|
17
|
+
self.payload.is_web_data_message
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Отправляет уведомление на все платформы,
|
2
|
+
# сохраняет статистику по отправке в БД,
|
3
|
+
# и обновляет инорфмацию об активности токенов
|
4
|
+
module Pushkin
|
5
|
+
class SendPushService
|
6
|
+
|
7
|
+
def initialize(notification_id)
|
8
|
+
@notification = Notification.find(notification_id)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
# Получим все токены, на которые нужно отправлять уведомления.
|
13
|
+
all_tokens = @notification.tokens_provider.get_tokens.to_a
|
14
|
+
|
15
|
+
# Создаем список помощников, которые отфильтруют уведомления по платформам.
|
16
|
+
# Они так же предоставляют соответствующие сервисы для отправки уведомлений.
|
17
|
+
token_filters = [Pushkin::TokenFilters::IosTokenFilter.new,
|
18
|
+
Pushkin::TokenFilters::AndroidTokenFilter.new,
|
19
|
+
Pushkin::TokenFilters::WebTokenFilter.new]
|
20
|
+
|
21
|
+
# Отправка на конкретные платформы
|
22
|
+
push_sending_results = token_filters.map do |token_filter|
|
23
|
+
tokens = token_filter.filter_tokens(all_tokens)
|
24
|
+
token_filter.get_sending_service(tokens, @notification.payload).call if tokens.present?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Актуализация информации об активности токенов
|
28
|
+
push_sending_results.compact.each do |push_sending_result|
|
29
|
+
push_sending_result.update_attributes(notification_id: @notification.id)
|
30
|
+
push_sending_result.token_results.joins(:token).invalid.each do |token_result|
|
31
|
+
token_result.token.update_attributes(is_active: false)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@notification.update_attributes(finished_at: DateTime.now)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|