cnfs-comm 0.0.1.alpha
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 +17 -0
- data/Rakefile +22 -0
- data/app/controllers/campaigns_controller.rb +4 -0
- data/app/controllers/comm/application_controller.rb +6 -0
- data/app/controllers/events_controller.rb +4 -0
- data/app/controllers/messages_controller.rb +19 -0
- data/app/controllers/providers_controller.rb +4 -0
- data/app/controllers/templates_controller.rb +4 -0
- data/app/controllers/whatsapps_controller.rb +73 -0
- data/app/jobs/call_job.rb +8 -0
- data/app/jobs/comm/application_job.rb +6 -0
- data/app/jobs/event_process_job.rb +4 -0
- data/app/jobs/message_process_job.rb +4 -0
- data/app/jobs/message_send_job.rb +4 -0
- data/app/mailers/application_mailer.rb +6 -0
- data/app/models/campaign.rb +17 -0
- data/app/models/comm/application_record.rb +7 -0
- data/app/models/event.rb +81 -0
- data/app/models/message.rb +21 -0
- data/app/models/provider.rb +24 -0
- data/app/models/providers/aws.rb +54 -0
- data/app/models/providers/twilio.rb +37 -0
- data/app/models/template.rb +54 -0
- data/app/models/tenant.rb +7 -0
- data/app/models/whatsapp.rb +4 -0
- data/app/operations/event_process.rb +41 -0
- data/app/operations/message_create.rb +116 -0
- data/app/operations/message_process.rb +17 -0
- data/app/operations/message_send.rb +25 -0
- data/app/policies/campaign_policy.rb +4 -0
- data/app/policies/comm/application_policy.rb +6 -0
- data/app/policies/event_policy.rb +4 -0
- data/app/policies/message_policy.rb +9 -0
- data/app/policies/provider_policy.rb +4 -0
- data/app/policies/template_policy.rb +4 -0
- data/app/resources/campaign_resource.rb +12 -0
- data/app/resources/comm/application_resource.rb +7 -0
- data/app/resources/event_resource.rb +25 -0
- data/app/resources/message_resource.rb +23 -0
- data/app/resources/provider_resource.rb +5 -0
- data/app/resources/template_resource.rb +5 -0
- data/app/resources/twilio_resource.rb +3 -0
- data/app/resources/whatsapp_resource.rb +5 -0
- data/config/environment.rb +0 -0
- data/config/routes.rb +10 -0
- data/config/sidekiq.yml +3 -0
- data/config/spring.rb +3 -0
- data/db/migrate/20190212220055_create_whatsapps.rb +19 -0
- data/db/migrate/20190217113829_create_providers.rb +18 -0
- data/db/migrate/20190317045825_create_campaigns.rb +12 -0
- data/db/migrate/20190317080012_create_templates.rb +12 -0
- data/db/migrate/20190317090002_create_events.rb +19 -0
- data/db/migrate/20190317114527_create_messages.rb +15 -0
- data/db/migrate/20191127070152_add_provider_id_to_message.rb +5 -0
- data/db/seeds/development/data.seeds.rb +35 -0
- data/db/seeds/development/tenants.seeds.rb +8 -0
- data/lib/ros/comm.rb +11 -0
- data/lib/ros/comm/console.rb +8 -0
- data/lib/ros/comm/engine.rb +53 -0
- data/lib/ros/comm/version.rb +7 -0
- data/lib/tasks/ros/comm_tasks.rake +16 -0
- metadata +181 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Providers
|
4
|
+
class Twilio < Provider
|
5
|
+
alias_attribute :account_sid, :credential_1
|
6
|
+
alias_attribute :auth_token, :credential_2
|
7
|
+
|
8
|
+
def self.services; %w[sms call] end
|
9
|
+
|
10
|
+
def client
|
11
|
+
@client ||= ::Twilio::REST::Client.new(x_account_sid, x_auth_token) if x_account_sid && x_auth_token
|
12
|
+
end
|
13
|
+
|
14
|
+
def x_account_sid
|
15
|
+
account_sid || (current_tenant.platform_twilio_enabled ? ENV['TWILIO_ACCOUNT_SID'] : nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
def x_auth_token
|
19
|
+
auth_token || (current_tenant.platform_twilio_enabled ? ENV['TWILIO_AUTH_TOKEN'] : nil)
|
20
|
+
end
|
21
|
+
|
22
|
+
def sms(from, to, body)
|
23
|
+
sender = from || provider_from
|
24
|
+
# Rails.logger.warn('No Twilio client configured for tenant.account_id') and return unless client
|
25
|
+
# message.update(from: from)
|
26
|
+
# binding.pry
|
27
|
+
# TODO: toggle sending on and off
|
28
|
+
client.messages.create(from: sender, to: to, body: body)
|
29
|
+
Rails.logger.debug message
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(_message)
|
33
|
+
# to = whatup.From.gsub('whatsapp:', '')
|
34
|
+
client.calls.create(from: from, to: to, url: 'http://demo.twilio.com/docs/voice.xml')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Template < Comm::ApplicationRecord
|
4
|
+
attr_accessor :properties
|
5
|
+
|
6
|
+
after_initialize :initialize_properties
|
7
|
+
|
8
|
+
def initialize_properties
|
9
|
+
self.properties = OpenStruct.new
|
10
|
+
end
|
11
|
+
|
12
|
+
# See: https://www.stuartellis.name/articles/erb/
|
13
|
+
def render(user:, campaign:)
|
14
|
+
properties.user = user
|
15
|
+
properties.campaign = campaign
|
16
|
+
|
17
|
+
final_content = content.dup
|
18
|
+
keys_to_replace = content.scan(/\[[A-z0-9]+\]/)
|
19
|
+
keys_to_replace.each do |key|
|
20
|
+
final_content.gsub!(key, value_for(key))
|
21
|
+
end
|
22
|
+
final_content
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def key_map
|
28
|
+
{ 'salutation' => { property: :user, value: :title },
|
29
|
+
'userFirstName' => { property: :user, value: :first_name },
|
30
|
+
'userLastName' => { property: :user, value: :last_name },
|
31
|
+
'userId' => { property: :user, value: :primary_identifier },
|
32
|
+
'campaignUrl' => { property: :campaign, value: :final_url } }
|
33
|
+
end
|
34
|
+
|
35
|
+
def value_for(key)
|
36
|
+
sanitized_key = key.delete '[]'
|
37
|
+
mapped_key = key_map[sanitized_key]
|
38
|
+
return key if mapped_key.nil? || properties[mapped_key[:property]].nil?
|
39
|
+
|
40
|
+
if sanitized_key != 'campaignUrl'
|
41
|
+
properties[mapped_key[:property]].send(mapped_key[:value])
|
42
|
+
else
|
43
|
+
"#{properties[mapped_key[:property]].send(mapped_key[:value])}?#{campaign_query_params}&#{owner_query_params}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def campaign_query_params
|
48
|
+
"cid=#{properties.campaign.owner_id}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def owner_query_params
|
52
|
+
"pi=#{properties.user.primary_identifier}"
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class EventProcess < Ros::ActivityBase
|
4
|
+
step :find_event
|
5
|
+
failed :event_not_found
|
6
|
+
step :create_messages_for_pool
|
7
|
+
|
8
|
+
def find_event(ctx, id:, **)
|
9
|
+
event = ::Event.find(id)
|
10
|
+
# TODO: What if campaign is not set? This returns nil and fails
|
11
|
+
ctx[:event] = event
|
12
|
+
ctx[:template] = event.template
|
13
|
+
ctx[:campaign] = event.campaign
|
14
|
+
rescue ActiveRecord::RecordNotFound
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
def event_not_found(_ctx, id:, errors:, **)
|
19
|
+
errors.add(:event, "not found for tenant (id: #{id})")
|
20
|
+
end
|
21
|
+
|
22
|
+
def create_messages_for_pool(_ctx, event:, template:, campaign:, **)
|
23
|
+
event.process!
|
24
|
+
# TODO: We need to somehow paginate these users otherwise its going
|
25
|
+
# to burst the server. A pool (event target, can have millions of users)
|
26
|
+
# which will be all returned here (or not if the server goes down)
|
27
|
+
event.users.each do |user|
|
28
|
+
content = template.render(user: user, campaign: campaign)
|
29
|
+
MessageProcessJob.perform_later(
|
30
|
+
params: {
|
31
|
+
to: user.phone_number,
|
32
|
+
provider: event.provider,
|
33
|
+
channel: event.channel,
|
34
|
+
body: content,
|
35
|
+
owner: event
|
36
|
+
}
|
37
|
+
)
|
38
|
+
end
|
39
|
+
event.publish!
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MessageCreate < Ros::ActivityBase
|
4
|
+
# step :check_permission
|
5
|
+
# failed :not_permitted, Output(:success) => End(:failure)
|
6
|
+
step :valid_recipient_and_phone_number
|
7
|
+
failed :invalid_recipient_and_phone_number, Output(:success) => End(:failure)
|
8
|
+
step :set_recipient
|
9
|
+
failed :recipient_not_found, Output(:success) => End(:failure)
|
10
|
+
step :match_recipient_and_phone_number
|
11
|
+
failed :mismatched_recipient_and_phone_number, Output(:success) => End(:failure)
|
12
|
+
step :set_final_to
|
13
|
+
step :valid_send_at
|
14
|
+
failed :invalid_send_at, Output(:success) => End(:failure)
|
15
|
+
step :setup_message
|
16
|
+
failed :invalid_message
|
17
|
+
step :save_sms
|
18
|
+
step :send_to_provider
|
19
|
+
|
20
|
+
# NOTE: Ensures that if send_at was sent then it is a valid date/datetime
|
21
|
+
# If send_at is sent and valid, we store it in context, else we jump
|
22
|
+
# to the error track.
|
23
|
+
# If send_at is not sent we will not delay the sms sending and instead send
|
24
|
+
# it immediately
|
25
|
+
|
26
|
+
def check_permission(_ctx, user:, **)
|
27
|
+
MessagePolicy.new(user, Message.new).create?
|
28
|
+
end
|
29
|
+
|
30
|
+
def not_permitted(ctx, **)
|
31
|
+
ctx[:errors].add(:user, 'not permitted to send message')
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid_recipient_and_phone_number(_ctx, params:, **)
|
35
|
+
params[:recipient_id].present? || params[:to].present?
|
36
|
+
end
|
37
|
+
|
38
|
+
def invalid_recipient_and_phone_number(ctx, **)
|
39
|
+
ctx[:errors].add(:recipient, 'is missing')
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_recipient(ctx, params:, **)
|
43
|
+
recipient_id = params[:recipient_id]
|
44
|
+
return true unless recipient_id
|
45
|
+
|
46
|
+
begin
|
47
|
+
ctx[:recipient] = Ros::Cognito::User.find(recipient_id).first
|
48
|
+
ctx[:recipient].errors.blank?
|
49
|
+
rescue JsonApiClient::Errors::NotFound
|
50
|
+
false
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def recipient_not_found(ctx, params:, **)
|
55
|
+
ctx[:errors].add(:recipient, "#{params[:recipient_id]} cannot be found")
|
56
|
+
end
|
57
|
+
|
58
|
+
def match_recipient_and_phone_number(ctx, params:, **)
|
59
|
+
recipient_id = params[:recipient_id]
|
60
|
+
to = params[:to]
|
61
|
+
return true unless recipient_id.present? && to.present?
|
62
|
+
|
63
|
+
ctx[:recipient].phone_number == to
|
64
|
+
end
|
65
|
+
|
66
|
+
def mismatched_recipient_and_phone_number(ctx, **)
|
67
|
+
ctx[:errors].add(:recipient, 'mismatch')
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_final_to(ctx, params:, **)
|
71
|
+
params[:to] ||= ctx[:recipient].phone_number
|
72
|
+
end
|
73
|
+
|
74
|
+
def valid_send_at(ctx, **)
|
75
|
+
return true if ctx[:send_at].blank?
|
76
|
+
|
77
|
+
begin
|
78
|
+
parsed_send_at = Time.zone.parse(ctx[:send_at])
|
79
|
+
return false if parsed_send_at.nil?
|
80
|
+
|
81
|
+
ctx[:send_at] = parsed_send_at
|
82
|
+
rescue ArgumentError
|
83
|
+
return false
|
84
|
+
end
|
85
|
+
|
86
|
+
!ctx[:send_at].nil?
|
87
|
+
end
|
88
|
+
|
89
|
+
def invalid_send_at(ctx, **)
|
90
|
+
ctx[:errors].add(:send_at, "is not a valid date format (send_at: #{ctx[:send_at]})")
|
91
|
+
end
|
92
|
+
|
93
|
+
def setup_message(ctx, params:, **)
|
94
|
+
# TODO: If the tenant has a provider set, use the tenant's provider
|
95
|
+
# else default to platform default provider
|
96
|
+
ctx[:model] = Message.new(params)
|
97
|
+
ctx[:model].provider_id = Providers::Aws.first.id
|
98
|
+
ctx[:model].valid?
|
99
|
+
end
|
100
|
+
|
101
|
+
def invalid_message(ctx, model:, **)
|
102
|
+
ctx[:errors] = model.errors
|
103
|
+
end
|
104
|
+
|
105
|
+
def save_sms(_ctx, model:, **)
|
106
|
+
model.save
|
107
|
+
end
|
108
|
+
|
109
|
+
def send_to_provider(ctx, model:, **)
|
110
|
+
if ctx[:send_at].present?
|
111
|
+
MessageSendJob.set(wait_until: ctx[:send_at]).perform_later(id: model.id)
|
112
|
+
else
|
113
|
+
MessageSendJob.perform_later(id: model.id)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MessageProcess < Ros::ActivityBase
|
4
|
+
step :create_message
|
5
|
+
failed :cannot_create_message
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def create_message(ctx, params:, **)
|
10
|
+
ctx[:op_result] = MessageCreate.call(params: params)
|
11
|
+
ctx[:op_result].success?
|
12
|
+
end
|
13
|
+
|
14
|
+
def cannot_create_message(_ctx, op_result:, errors:, **)
|
15
|
+
errors.add(:message, op_result.errors.full_messages.join(', '))
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MessageSend < Ros::ActivityBase
|
4
|
+
step :retrieve_message
|
5
|
+
failed :message_not_found
|
6
|
+
step :send_message
|
7
|
+
step :update_message_provider_id
|
8
|
+
|
9
|
+
def retrieve_message(ctx, id:, **)
|
10
|
+
ctx[:message] = Message.find_by(id: id)
|
11
|
+
end
|
12
|
+
|
13
|
+
def message_not_found(_ctx, errors:, id:, **)
|
14
|
+
errors.add(:message, "with #{id} not found")
|
15
|
+
end
|
16
|
+
|
17
|
+
def send_message(ctx, message:, **)
|
18
|
+
ctx[:msg_id] = message.provider.send(message.channel, message.from, message.to, message.body)
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_message_provider_id(ctx, message:, **)
|
22
|
+
message.provider_msg_id = ctx[:msg_id]
|
23
|
+
message.save
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CampaignResource < Comm::ApplicationResource
|
4
|
+
# Serialize the polymorphic :owner association using the specific fields
|
5
|
+
# rather than just :owner since if it is :owner JR will look for the class
|
6
|
+
# which could be in a separate service and therefore fails
|
7
|
+
attributes :name, :description, :owner_type, :owner_id, :base_url
|
8
|
+
has_many :events
|
9
|
+
has_many :templates
|
10
|
+
|
11
|
+
filters :owner_type, :owner_id
|
12
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class EventResource < Comm::ApplicationResource
|
4
|
+
attributes :name, :send_at, :provider_id, :campaign_id, :template_id, :channel, :pool_id
|
5
|
+
attributes :target_type, :target_id, :owner_type, :owner_id
|
6
|
+
|
7
|
+
has_one :provider
|
8
|
+
has_one :template
|
9
|
+
|
10
|
+
filters :owner_type, :owner_id
|
11
|
+
|
12
|
+
def fetchable_fields
|
13
|
+
super - %i[pool_id]
|
14
|
+
end
|
15
|
+
|
16
|
+
def pool_id=(pool_id)
|
17
|
+
@model.target_type = 'Ros::Cognito::Pool'
|
18
|
+
@model.target_id = pool_id
|
19
|
+
end
|
20
|
+
|
21
|
+
before_save do
|
22
|
+
# TODO: Make this a valid provider
|
23
|
+
@model.provider_id ||= 1
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MessageResource < Comm::ApplicationResource
|
4
|
+
attributes :from, :to, :body, :provider_id, :owner_id, :recipient_id, :owner_type, :channel
|
5
|
+
|
6
|
+
filters :owner_id, :owner_type
|
7
|
+
|
8
|
+
filter :to, apply: lambda { |records, value, _options|
|
9
|
+
records.sent_to(value[0])
|
10
|
+
}
|
11
|
+
|
12
|
+
filter :user_id, apply: lambda { |records, value, _options|
|
13
|
+
user = Ros::Cognito::User.where(id: value[0]).first
|
14
|
+
|
15
|
+
return records.none if user.nil?
|
16
|
+
|
17
|
+
records.sent_to(user.phone_number)
|
18
|
+
}
|
19
|
+
|
20
|
+
def fetchable_fields
|
21
|
+
super + [:provider_msg_id]
|
22
|
+
end
|
23
|
+
end
|