hey-you 0.1.6

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.
@@ -0,0 +1,17 @@
1
+ require_relative '_base'
2
+
3
+ module HeyYou
4
+ class Builder
5
+ class Email < Base
6
+ attr_reader :subject, :body, :layout, :mailer_class, :mailer_method, :delivery_method
7
+
8
+ def build
9
+ @mailer_class = ch_data.fetch('mailer_class', nil)
10
+ @mailer_method = ch_data.fetch('mailer_method', nil)
11
+ @delivery_method = ch_data.fetch('delivery_method', nil)
12
+ @body = interpolate(ch_data.fetch('body'), options)
13
+ @subject = interpolate(ch_data.fetch('subject'), options)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ require_relative '_base'
2
+
3
+ module HeyYou
4
+ class Builder
5
+ class Push < Base
6
+ attr_reader :body, :title, :data
7
+
8
+ def build
9
+ @title = interpolate(ch_data.fetch('title'), options)
10
+ @body = interpolate(ch_data.fetch('body'), options)
11
+ @data = options[:data] || {}
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,51 @@
1
+ require 'i18n'
2
+ require_relative 'builder/email'
3
+ require_relative 'builder/push'
4
+
5
+ module HeyYou
6
+ class Builder
7
+ attr_reader :data, :options
8
+
9
+ # Load data from collection yaml via key and interpolate variables.
10
+ # Define methods for each registered channel. After initialize you can use
11
+ # `instance.<ch_name>`. It will be return instance of HeyYou::Builder::<YOUR_CHANNEL_NAME>
12
+ #
13
+ def initialize(key, **options)
14
+ @data = fetch_from_collection_by_key(key, options[:locale])
15
+ @options = options
16
+ config.registered_channels.each do |ch|
17
+ ch_builder =
18
+ HeyYou::Builder.const_get("#{ch.downcase.capitalize}").new(data, key, options)
19
+ instance_variable_set("@#{ch}".to_sym, ch_builder)
20
+
21
+ define_ch_method(ch)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def define_ch_method(ch)
28
+ method_proc = -> { instance_variable_get("@#{ch}".to_sym) }
29
+ self.class.send(:define_method, ch, method_proc)
30
+ end
31
+
32
+ def fetch_from_collection_by_key(key, locale)
33
+ keys = []
34
+ if config.localization
35
+ locale = locale || I18n.locale
36
+ raise UnknownLocale, 'You should pass locale.' unless locale
37
+ keys << locale
38
+ end
39
+ keys = keys + key.to_s.split(config.splitter)
40
+ keys.reduce(config.collection) do |memo, nested_key|
41
+ memo[nested_key.to_s] if memo
42
+ end
43
+ end
44
+
45
+ def config
46
+ Config.config
47
+ end
48
+
49
+ class UnknownLocale < StandardError; end
50
+ end
51
+ end
@@ -0,0 +1,42 @@
1
+ module HeyYou
2
+ module Channels
3
+ class Base
4
+ class << self
5
+ def send!
6
+ raise NotImplementedError, 'You should define #send! method in your channel.'
7
+ end
8
+
9
+ private
10
+
11
+ def credentials_present?
12
+ required_credentials.all? do |cred|
13
+ if config.send(self.name.split('::').last.downcase).send(cred).nil?
14
+ config.logger&.info(
15
+ "[WARN] required credential was not set. " +
16
+ "Set `config.#{self.name.downcase}.#{cred}` to send notification with #{self.name.downcase}"
17
+ )
18
+ return false
19
+ end
20
+ true
21
+ end
22
+ end
23
+
24
+ def required_credentials
25
+ []
26
+ end
27
+
28
+ def config
29
+ Config.config
30
+ end
31
+
32
+ def ch_name
33
+ self.class.name.split('::').last.downcase
34
+ end
35
+
36
+ def log(msg)
37
+ config.log("[#{ch_name.upcase}] #{msg}")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,87 @@
1
+ require_relative '_base'
2
+ require 'mail'
3
+
4
+ module HeyYou
5
+ module Channels
6
+ class Email < Base
7
+ class << self
8
+ def send!(builder, to:, **options)
9
+ raise CredentialsNotExists unless credentials_present?
10
+
11
+ method = config.email.default_mailing ? :send_via_mail : :send_via_custom_class
12
+ public_send(method, builder, to, options)
13
+ end
14
+
15
+ # Send email via custom class instance.
16
+ def send_via_custom_class(builder, to, **options)
17
+ mailer = mailer_class_from_builder(builder, options)
18
+
19
+ mailer_method = options[:mailer_method] ||
20
+ builder.email.mailer_method ||
21
+ config.email.default_mailer_method
22
+
23
+ delivery_method = options[:delivery_method] ||
24
+ builder.email.delivery_method ||
25
+ config.email.default_delivery_method
26
+
27
+ log("Build mail via #{mailer}##{mailer_method}. Delivery with #{delivery_method}")
28
+ mailer_msg = mailer.public_send(mailer_method, data: builder.email, to: to)
29
+ mailer_msg.public_send(delivery_method)
30
+ end
31
+
32
+ # Send email with standard mail (gem 'mail')
33
+ def send_via_mail(builder, to, **_)
34
+ context = self
35
+ mail = Mail.new do
36
+ from HeyYou::Config.instance.email.from
37
+ to to
38
+ subject builder.email.subject
39
+ body context.get_body(builder.email.body)
40
+ end
41
+
42
+ mail.delivery_method config.email.mail_delivery_method
43
+ log("Send mail #{mail}")
44
+ mail.deliver
45
+ end
46
+
47
+ def get_body(body_text)
48
+ # TODO: Load layout from config here.
49
+ body_text
50
+ end
51
+
52
+ def required_credentials
53
+ %i[from]
54
+ end
55
+
56
+ private
57
+
58
+ def mailer_class_from_builder(builder, **options)
59
+ mailer_class = options[:mailer_class] ||
60
+ builder.email.mailer_class ||
61
+ config.email.default_mailer_class
62
+ unless mailer_class
63
+ raise(
64
+ MailerClassNotDefined,
65
+ 'You must set mailer_class in notifications collection or pass :mailer_class option'
66
+ )
67
+ end
68
+
69
+ begin
70
+ mailer = Object.const_get(mailer_class.to_s)
71
+ rescue NameError
72
+ raise ActionMailerClassNotDefined, "Mailer #{mailer_class} not initialized"
73
+ end
74
+
75
+ mailer
76
+ end
77
+ end
78
+
79
+ class CredentialsNotExists < StandardError;
80
+ end
81
+ class ActionMailerClassNotDefined < StandardError;
82
+ end
83
+ class MailerClassNotDefined < StandardError;
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '_base'
2
+
3
+ module HeyYou
4
+ module Channels
5
+ class Push < Base
6
+ class << self
7
+ def send!(builder, to:, **options)
8
+ options = build_options(builder)
9
+ config.logger&.info("[PUSH] Send #{options} body for #{ids(to)}")
10
+ config.push.fcm_client.send(ids(to), options)
11
+ end
12
+
13
+ private
14
+
15
+ def build_options(builder)
16
+ {
17
+ data: builder.push.data,
18
+ notification: {
19
+ title: builder.push.title,
20
+ body: builder.push.body
21
+ },
22
+ priority: builder.options[:priority] || config.push.priority,
23
+ time_to_live: config.push.ttl
24
+ }
25
+ end
26
+
27
+ def ids(to)
28
+ return to if to.is_a?(Array)
29
+ [to]
30
+ end
31
+
32
+ def required_credentials
33
+ %i[fcm_token]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ require 'singleton'
2
+
3
+ module HeyYou
4
+ class Config
5
+ module Configurable
6
+
7
+ def self.extended klass
8
+ klass.class_eval do
9
+ include Singleton
10
+ end
11
+ end
12
+
13
+ def configure(&block)
14
+ # TODO: log warn "Already configured" instead nil
15
+ @configured ? nil : instance_eval(&block)
16
+ @configured = true
17
+ end
18
+
19
+ def config
20
+ @config ||= self.instance
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'conigurable'
2
+ require 'mail'
3
+
4
+ module HeyYou
5
+ class Config
6
+ class Email
7
+ extend Configurable
8
+
9
+ MAIL_DELIVERY_METHOD = :sendmail
10
+ DEFAULT_ACTION_MAILER_METHOD = :send!
11
+ DEFAULT_DELIVERY_METHOD = :deliver_now
12
+
13
+ attr_accessor(
14
+ :from,
15
+ :mail_delivery_method,
16
+ :default_mailing,
17
+ :default_delivery_method,
18
+ :default_mailer_class,
19
+ :default_mailer_method
20
+ )
21
+
22
+ def initialize
23
+ @mail_delivery_method ||= MAIL_DELIVERY_METHOD
24
+ @default_mailing ||= !default_mailer_class.nil?
25
+ @async ||= true
26
+ @default_mailer_method ||= DEFAULT_ACTION_MAILER_METHOD
27
+ @default_delivery_method ||= DEFAULT_DELIVERY_METHOD
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'conigurable'
2
+ require 'fcm'
3
+
4
+ module HeyYou
5
+ class Config
6
+ class Push
7
+ extend Configurable
8
+
9
+ DEFAULT_PRIORITY = 'high'
10
+ DEFAULT_TTL = 60
11
+ DEFAULT_FCM_TIMEOUT = 30
12
+
13
+ attr_accessor :fcm_token, :priority, :ttl, :fcm_timeout
14
+
15
+ def initialize
16
+ @priority = DEFAULT_PRIORITY
17
+ @ttl = DEFAULT_TTL
18
+ @fcm_timeout = DEFAULT_FCM_TIMEOUT
19
+ end
20
+
21
+ def fcm_client
22
+ raise FcmTokenNotExists, 'Can\'t create fcm client: fcm_token not exists' unless fcm_token
23
+ @fcm_client ||= FCM.new(fcm_token, timeout: fcm_timeout)
24
+ end
25
+
26
+ class FcmTokenNotExists < StandardError; end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,111 @@
1
+ require 'yaml'
2
+ require_relative 'config/conigurable'
3
+ require_relative 'config/push'
4
+ require_relative 'config/email'
5
+
6
+ #
7
+ # @config REQUIRED collection_file [String] - File path for general notifications file
8
+ # @config OPTIONAL env_collection_file [String] - File path for environment notifications file
9
+ # @config OPTIONAL splitter [String] - Chars for split notifications keys in builder.
10
+ # For example:
11
+ # if splitter eq `.` you can pass to notification builder 'key_1.nested_key'
12
+ # if splitter eq `/` you can pass to notification builder 'key_1/nested_key'
13
+ # @config OPTIONAL registered_channels - Channels available for service. If your application
14
+ # planning use only push and email just set it as [:push, :email]. Default all channels
15
+ # will available.
16
+ module HeyYou
17
+ class Config
18
+ extend Configurable
19
+
20
+ DEFAULT_REGISTERED_CHANNELS = %i[email push]
21
+ DEFAULT_SPLITTER = '.'
22
+ DEFAULT_GLOBAL_LOG_TAG = 'HeyYou'
23
+ DEFAULT_LOCALIZATION_FLAG = false
24
+
25
+ class CollectionFileNotDefined < StandardError; end
26
+
27
+ attr_reader :collection, :env_collection, :configured, :registered_receivers
28
+ attr_accessor(
29
+ :collection_files, :env_collection_file, :splitter,
30
+ :registered_channels, :localization, :logger, :log_tag
31
+ )
32
+
33
+ def initialize
34
+ @registered_channels ||= DEFAULT_REGISTERED_CHANNELS
35
+ @splitter ||= DEFAULT_SPLITTER
36
+ @registered_receivers = []
37
+ @log_tag ||= DEFAULT_GLOBAL_LOG_TAG
38
+ @localization ||= DEFAULT_LOCALIZATION_FLAG
39
+ define_ch_config_methods
40
+ end
41
+
42
+ def collection_file
43
+ @collection_files || raise(
44
+ CollectionFileNotDefined,
45
+ 'You must define HeyYou::Config.collection_files'
46
+ )
47
+ end
48
+
49
+ def collection
50
+ @collection ||= load_collection
51
+ end
52
+
53
+ def env_collection
54
+ @env_collection ||= load_env_collection
55
+ end
56
+
57
+ def registrate_receiver(receiver_class)
58
+ log("#{receiver_class} registrated as receiver")
59
+ @registered_receivers << receiver_class
60
+ end
61
+
62
+ # Registrate new custom channel.
63
+ # For successful registration, in application must be exists:
64
+ # 1. HeyYou::Channels::<YOUR_CHANNEL_NAME> < HeyYou::Channels::Base
65
+ # 2. HeyYou::Builder::<YOUR_CHANNEL_NAME> < HeyYou::Builder::Base
66
+ # 3. HeyYou::Config::<YOUR_CHANNEL_NAME> extended HeyYou::Config::Configurable
67
+ #
68
+ def registrate_channel(ch)
69
+ registered_channels << ch.to_sym
70
+ define_ch_config_method(ch)
71
+ end
72
+
73
+ def log(msg)
74
+ logger&.info("[#{log_tag}] #{msg} ")
75
+ end
76
+
77
+ private
78
+
79
+ def define_ch_config_methods
80
+ registered_channels.each do |ch|
81
+ define_ch_config_method(ch)
82
+ end
83
+ end
84
+
85
+ # Define method for fetch config of channel.
86
+ #
87
+ # For example, if ch == 'push' will define method #push for class instance.
88
+ # New method will return instance of channel config instance
89
+ def define_ch_config_method(ch)
90
+ method_proc = -> { self.class.const_get(ch.capitalize).config }
91
+ self.class.send(:define_method, ch, method_proc)
92
+ end
93
+
94
+ # Load yaml from collection_file and merge it with yaml from env_collection_file
95
+ def load_collection
96
+ @collection_files = [collection_files] if collection_files.is_a?(String)
97
+ notification_collection = {}
98
+ collection_files.each do |file|
99
+ notification_collection.merge!(YAML.load_file(file))
100
+ end
101
+ notification_collection.merge!(env_collection)
102
+ end
103
+
104
+ def load_env_collection
105
+ if env_collection_file
106
+ return YAML.load_file(env_collection_file) rescue { }
107
+ end
108
+ {}
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,89 @@
1
+ require_relative 'sender'
2
+
3
+ module HeyYou
4
+ module Receiver
5
+ attr_reader :receiver_channels, :receiver_data
6
+
7
+ def self.extended klass
8
+ klass.class_eval do
9
+ # Method will be injected for instances methods of class.
10
+ #
11
+ # This can use something like: user.send_notification('key', options).
12
+ def send_notification(notification_key, **options)
13
+ Sender.send_to(self, notification_key, options)
14
+ end
15
+ end
16
+ end
17
+
18
+ # Registrate class as receiver.
19
+ # In parameters pass hash where keys - channels names, and values -
20
+ # procs with values for receive.
21
+ #
22
+ # For instances of classes will be created methods `#{channel_name}_ch_receive_info`
23
+ # after registrate your class as receiver.
24
+ #
25
+ # Example:
26
+ #
27
+ # class User
28
+ # extend HeyYou::Receiver
29
+ #
30
+ # receive(
31
+ # push: -> { push_tokens.value },
32
+ # email: -> { priority_email }
33
+ # )
34
+ #
35
+ # ## Or you can use receive with options:
36
+ # receive(
37
+ # email: { subject: -> { priority_email }, options: { mailer_class: UserMailer, mailer_method: :notify } }
38
+ # )
39
+ #
40
+ # ...
41
+ # end
42
+ #
43
+ # user = User.new( push_tokens: PushToken.new(value: "456"), priority_email: 'example@mail.com')
44
+ # user.push_ch_receive_info # => 456
45
+ #
46
+ def receive(receiver_data)
47
+ check_channels(receiver_data.keys)
48
+
49
+ @receiver_data = receiver_data
50
+ @receiver_channels = receiver_data.keys
51
+ hey_you_config.registrate_receiver(self)
52
+
53
+ define_receive_info_methods
54
+ end
55
+
56
+ private
57
+
58
+ def check_channels(channels)
59
+ channels.all? do |ch|
60
+ next if hey_you_config.registered_channels.include?(ch.to_sym)
61
+ raise(
62
+ NotRegisteredChannel,
63
+ "Channel #{ch} not registered. Registered channels: #{hey_you_config.registered_channels}"
64
+ )
65
+ end
66
+ @received_channels = channels
67
+ end
68
+
69
+ def define_receive_info_methods
70
+ receiver_channels.each do |ch|
71
+ if receiver_data[ch].is_a?(Hash)
72
+ me = self
73
+ self.send(:define_method, "#{ch}_ch_receive_info", receiver_data[ch].fetch(:subject))
74
+ self.send(:define_method, "#{ch}_ch_receive_options", -> { me.receiver_data[ch].fetch(:options, {}) })
75
+ else
76
+ self.send(:define_method, "#{ch}_ch_receive_info", receiver_data[ch])
77
+ self.send(:define_method, "#{ch}_ch_receive_options", -> { {} })
78
+ end
79
+ end
80
+ end
81
+
82
+ def hey_you_config
83
+ Config.config
84
+ end
85
+
86
+ class NotRegisteredChannel < StandardError;
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,81 @@
1
+ require_relative 'builder'
2
+
3
+ require_relative 'channels/push'
4
+ require_relative 'channels/email'
5
+
6
+ module HeyYou
7
+ class Sender
8
+ class << self
9
+ # Send notifications for receiver
10
+ #
11
+ # @input receiver [Instance of receiver class] - registrated receiver
12
+ # @input notification_key [String] - key for notification builder
13
+ # @input options [Hash]
14
+ # @option only [String/Array[String]] - whitelist for using channels
15
+ #
16
+ def send_to(receiver, notification_key, **options)
17
+ unless receiver_valid?(receiver)
18
+ raise NotRegisteredReceiver, "Class '#{receiver.class}' not registered as receiver"
19
+ end
20
+
21
+ result = send!(notification_key, receiver, **options)
22
+ config.log("Sender result: #{result}")
23
+ result
24
+ end
25
+
26
+ def send!(notification_key, receiver, **options)
27
+ to_hash = {}
28
+ receiver.class.receiver_channels.each do |ch|
29
+ to_hash[ch] = {
30
+ # Fetch receiver's info for sending: phone_number, email, etc
31
+ subject: receiver.public_send("#{ch}_ch_receive_info"),
32
+ # Fetch receiver's options like :mailer_class
33
+ options: receiver.public_send("#{ch}_ch_receive_options") || {}
34
+ }
35
+ end
36
+
37
+ builder = Builder.new(notification_key, options)
38
+ response = {}
39
+ config.registered_channels.each do |ch|
40
+ if channel_allowed?(ch, to_hash, builder, options)
41
+ config.log(
42
+ "Send #{ch} to #{to_hash[ch][:subject]} with data: #{builder.send(ch).data}" \
43
+ " and options: #{to_hash[ch][:options]}"
44
+ )
45
+ response[ch] = Channels.const_get(ch.to_s.capitalize).send!(
46
+ builder, to: to_hash[ch][:subject], **to_hash[ch][:options]
47
+ )
48
+ else
49
+ config.log("Channel #{ch} not allowed.")
50
+ end
51
+ end
52
+ response
53
+ end
54
+
55
+ private
56
+
57
+ def channel_allowed?(ch, to, builder, **options)
58
+ unless to[ch].is_a?(Hash) ? to[ch.to_sym][:subject] || to[ch.to_s][:subject] : to[ch.to_sym] || to[ch.to_s]
59
+ return false
60
+ end
61
+ channel_allowed_by_only?(ch, options[:only]) && !builder.send(ch).nil?
62
+ end
63
+
64
+ def receiver_valid?(receiver)
65
+ config.registered_receivers.include?(receiver.class)
66
+ end
67
+
68
+ def channel_allowed_by_only?(ch, only)
69
+ return true unless only
70
+ return only.map(&:to_sym).include?(ch.to_sym) if only.is_a?(Array)
71
+ only.to_sym == ch.to_sym
72
+ end
73
+
74
+ def config
75
+ Config.config
76
+ end
77
+ end
78
+
79
+ class NotRegisteredReceiver < StandardError; end
80
+ end
81
+ end
@@ -0,0 +1,3 @@
1
+ module HeyYou
2
+ VERSION = "0.1.6"
3
+ end