hey-you 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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