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