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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +36 -0
  5. data/app/assets/config/pushkin_manifest.js +2 -0
  6. data/app/assets/javascripts/pushkin/application.js +14 -0
  7. data/app/assets/javascripts/pushkin/firebase/push_notifications.js +100 -0
  8. data/app/assets/javascripts/pushkin/main.js +5 -0
  9. data/app/assets/stylesheets/pushkin/application.css +15 -0
  10. data/app/controllers/pushkin/api/v1/concerns/api_helper.rb +43 -0
  11. data/app/controllers/pushkin/api/v1/concerns/tokens_helper.rb +94 -0
  12. data/app/controllers/pushkin/application_controller.rb +5 -0
  13. data/app/fabrics/pushkin/notification_fabric.rb +83 -0
  14. data/app/helpers/pushkin/application_helper.rb +4 -0
  15. data/app/jobs/pushkin/application_job.rb +4 -0
  16. data/app/jobs/pushkin/send_job.rb +11 -0
  17. data/app/mailers/pushkin/application_mailer.rb +6 -0
  18. data/app/models/pushkin/application_record.rb +5 -0
  19. data/app/models/pushkin/concerns/pushkin_user.rb +10 -0
  20. data/app/models/pushkin/notification.rb +42 -0
  21. data/app/models/pushkin/payload.rb +21 -0
  22. data/app/models/pushkin/push_sending_result.rb +13 -0
  23. data/app/models/pushkin/token.rb +19 -0
  24. data/app/models/pushkin/token_result.rb +39 -0
  25. data/app/models/pushkin/tokens_provider.rb +22 -0
  26. data/app/models/pushkin/tokens_provider_user.rb +12 -0
  27. data/app/services/pushkin/fcm/send_android_push_service.rb +22 -0
  28. data/app/services/pushkin/fcm/send_fcm_push_service.rb +120 -0
  29. data/app/services/pushkin/fcm/send_ios_push_service.rb +22 -0
  30. data/app/services/pushkin/fcm/send_web_push_service.rb +22 -0
  31. data/app/services/pushkin/send_push_service.rb +39 -0
  32. data/app/services/pushkin/token_filters/android_token_filter.rb +15 -0
  33. data/app/services/pushkin/token_filters/ios_token_filter.rb +15 -0
  34. data/app/services/pushkin/token_filters/token_filter.rb +24 -0
  35. data/app/services/pushkin/token_filters/web_token_filter.rb +15 -0
  36. data/app/views/layouts/pushkin/application.html.erb +14 -0
  37. data/lib/generators/pushkin/setup_generator.rb +27 -0
  38. data/lib/generators/pushkin/templates/create_pushkin_tables.rb +89 -0
  39. data/lib/generators/pushkin/templates/tokens_controller.rb +16 -0
  40. data/lib/pushkin-library.rb +1 -0
  41. data/lib/pushkin.rb +29 -0
  42. data/lib/pushkin/engine.rb +5 -0
  43. data/lib/pushkin/version.rb +3 -0
  44. data/lib/tasks/pushkin_tasks.rake +4 -0
  45. metadata +128 -0
@@ -0,0 +1,4 @@
1
+ module Pushkin
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Pushkin
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,11 @@
1
+ module Pushkin
2
+ class SendJob < ApplicationJob
3
+
4
+ queue_as :pushkin
5
+
6
+ def perform(notification_id)
7
+ SendPushService.new(notification_id).call
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module Pushkin
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Pushkin
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ # Базовый контроллер для всех остальных контроллеров Пушкина
2
+ module Pushkin::Concerns::PushkinUser
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ has_many :pushkin_tokens, class_name: "Pushkin::Token"
8
+ end
9
+
10
+ end
@@ -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