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.
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