cnfs-comm 0.0.1.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +17 -0
  4. data/Rakefile +22 -0
  5. data/app/controllers/campaigns_controller.rb +4 -0
  6. data/app/controllers/comm/application_controller.rb +6 -0
  7. data/app/controllers/events_controller.rb +4 -0
  8. data/app/controllers/messages_controller.rb +19 -0
  9. data/app/controllers/providers_controller.rb +4 -0
  10. data/app/controllers/templates_controller.rb +4 -0
  11. data/app/controllers/whatsapps_controller.rb +73 -0
  12. data/app/jobs/call_job.rb +8 -0
  13. data/app/jobs/comm/application_job.rb +6 -0
  14. data/app/jobs/event_process_job.rb +4 -0
  15. data/app/jobs/message_process_job.rb +4 -0
  16. data/app/jobs/message_send_job.rb +4 -0
  17. data/app/mailers/application_mailer.rb +6 -0
  18. data/app/models/campaign.rb +17 -0
  19. data/app/models/comm/application_record.rb +7 -0
  20. data/app/models/event.rb +81 -0
  21. data/app/models/message.rb +21 -0
  22. data/app/models/provider.rb +24 -0
  23. data/app/models/providers/aws.rb +54 -0
  24. data/app/models/providers/twilio.rb +37 -0
  25. data/app/models/template.rb +54 -0
  26. data/app/models/tenant.rb +7 -0
  27. data/app/models/whatsapp.rb +4 -0
  28. data/app/operations/event_process.rb +41 -0
  29. data/app/operations/message_create.rb +116 -0
  30. data/app/operations/message_process.rb +17 -0
  31. data/app/operations/message_send.rb +25 -0
  32. data/app/policies/campaign_policy.rb +4 -0
  33. data/app/policies/comm/application_policy.rb +6 -0
  34. data/app/policies/event_policy.rb +4 -0
  35. data/app/policies/message_policy.rb +9 -0
  36. data/app/policies/provider_policy.rb +4 -0
  37. data/app/policies/template_policy.rb +4 -0
  38. data/app/resources/campaign_resource.rb +12 -0
  39. data/app/resources/comm/application_resource.rb +7 -0
  40. data/app/resources/event_resource.rb +25 -0
  41. data/app/resources/message_resource.rb +23 -0
  42. data/app/resources/provider_resource.rb +5 -0
  43. data/app/resources/template_resource.rb +5 -0
  44. data/app/resources/twilio_resource.rb +3 -0
  45. data/app/resources/whatsapp_resource.rb +5 -0
  46. data/config/environment.rb +0 -0
  47. data/config/routes.rb +10 -0
  48. data/config/sidekiq.yml +3 -0
  49. data/config/spring.rb +3 -0
  50. data/db/migrate/20190212220055_create_whatsapps.rb +19 -0
  51. data/db/migrate/20190217113829_create_providers.rb +18 -0
  52. data/db/migrate/20190317045825_create_campaigns.rb +12 -0
  53. data/db/migrate/20190317080012_create_templates.rb +12 -0
  54. data/db/migrate/20190317090002_create_events.rb +19 -0
  55. data/db/migrate/20190317114527_create_messages.rb +15 -0
  56. data/db/migrate/20191127070152_add_provider_id_to_message.rb +5 -0
  57. data/db/seeds/development/data.seeds.rb +35 -0
  58. data/db/seeds/development/tenants.seeds.rb +8 -0
  59. data/lib/ros/comm.rb +11 -0
  60. data/lib/ros/comm/console.rb +8 -0
  61. data/lib/ros/comm/engine.rb +53 -0
  62. data/lib/ros/comm/version.rb +7 -0
  63. data/lib/tasks/ros/comm_tasks.rake +16 -0
  64. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Tenant < Comm::ApplicationRecord
4
+ include Ros::TenantConcern
5
+
6
+ store_accessor :platform_properties, :platform_twilio_enabled, :platform_aws_enabled
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Whatsapp < Comm::ApplicationRecord
4
+ 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,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CampaignPolicy < Comm::ApplicationPolicy
4
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Comm
4
+ class ApplicationPolicy < ::ApplicationPolicy
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EventPolicy < Comm::ApplicationPolicy
4
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MessagePolicy < Comm::ApplicationPolicy
4
+ def create?
5
+ return false if user.cognito_user_id
6
+
7
+ super
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ProviderPolicy < Comm::ApplicationPolicy
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TemplatePolicy < Comm::ApplicationPolicy
4
+ 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Comm
4
+ class ApplicationResource < ::ApplicationResource
5
+ abstract
6
+ end
7
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ProviderResource < Comm::ApplicationResource
4
+ attributes :name
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TemplateResource < Comm::ApplicationResource
4
+ attributes(:name, :description, :content, :status)
5
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TwilioResource < ProviderResource; end