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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e83faabad39fa0034ef10af4b89f3f19d9070da73bd319edbcdd08763f516756
4
+ data.tar.gz: 2975ef6f8729dee40fab0a4524ba2fb30d91e31ecc10dd7c571177e6b3983e7c
5
+ SHA512:
6
+ metadata.gz: 233b5c4d2200924ec43fb1f678ba93114a77573e1bb88d698a31f40cd3aa0c1fe15cd053878a993196e457abee47bae711ff3dc6ecbb962fa67b06f87ed98b57
7
+ data.tar.gz: 21a3f71ee15d2fcd1c1ebbcb1706e9fa9173bf202f7e934f43c5a67cb2d7910dc88513f756475125d24344a572be9b82e5da408549ce9653593518c32189e580
@@ -0,0 +1,20 @@
1
+ Copyright 2019 Robert Roach
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,17 @@
1
+ ## Summary
2
+
3
+ Created by rails-templates
4
+
5
+
6
+ def tcall(from: '+12565308753', to: '+6581132988')
7
+ @twilio_client.calls.create(to: to, from: from, url: 'http://demo.twilio.com/docs/voice.xml')
8
+ end
9
+
10
+ def tsms(from: '+12565308753', to: '+6581132988', body: 'Hey friend!')
11
+ @twilio_client.messages.create(
12
+ from: from,
13
+ to: to,
14
+ body: body
15
+ )
16
+ end
17
+
@@ -0,0 +1,22 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Ros::Comm'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+ load 'rails/tasks/statistics.rake'
21
+
22
+ require 'bundler/gem_tasks'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CampaignsController < Comm::ApplicationController
4
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Comm
4
+ class ApplicationController < ::ApplicationController
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EventsController < Comm::ApplicationController
4
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MessagesController < Comm::ApplicationController
4
+ def create
5
+ res = MessageCreate.call(params: assign_params, user: context[:user])
6
+ if res.success?
7
+ render json: json_resource(resource_class: MessageResource, record: res.model), status: :created
8
+ else
9
+ resource = ApplicationResource.new(res, nil)
10
+ handle_exceptions JSONAPI::Exceptions::ValidationErrors.new(resource)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def assign_params
17
+ jsonapi_params.permit(MessageResource.creatable_fields(context))
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ProvidersController < Comm::ApplicationController
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TemplatesController < Comm::ApplicationController
4
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # module ServiceTwilio
4
+ class WhatsappsController < Comm::ApplicationController
5
+ # skip_before_action :authenticate_user!
6
+ # before_action :set_whatsapp, only: [:show, :update, :destroy]
7
+
8
+ # GET /whatsapps
9
+ def index
10
+ @whatsapps = Whatsapp.all
11
+
12
+ render json: @whatsapps
13
+ end
14
+
15
+ # GET /whatsapps/1
16
+ def show
17
+ render json: @whatsapp
18
+ end
19
+
20
+ # POST /whatsapps
21
+ # NOTE: This is an endpoint that response to a Twilio notification
22
+ # when a whatsapp message is sent to a registered number
23
+ def create
24
+ @whatsapp = Whatsapp.new(whatsapp_params)
25
+
26
+ who = 'Blob'
27
+ who = 'Narayani' if @whatsapp.from.ends_with? '26'
28
+
29
+ if @whatsapp.save
30
+ # Put a message on a bus (rabbitMQ)
31
+ # Channels are named in a standardized way including service and tenant
32
+ # TODO:
33
+ # UserSpace, e.g. perx, truewards, LoyatlyCampaign listens on bus and sends message back
34
+ # Twilio service listens on bus and sends message
35
+ # Something like this
36
+ twiml = Twilio::TwiML::MessagingResponse.new do |r|
37
+ r.message(body: "Ahoy #{who}! Thanks so much for your message that said: '#{@whatsapp.Body}'")
38
+ end
39
+ render xml: twiml
40
+ else
41
+ # render json: @whatsapp, status: :created, location: @whatsapp
42
+ render json: @whatsapp.errors, status: :unprocessable_entity
43
+ end
44
+ end
45
+
46
+ # PATCH/PUT /whatsapps/1
47
+ def update
48
+ if @whatsapp.update(whatsapp_params)
49
+ render json: @whatsapp
50
+ else
51
+ render json: @whatsapp.errors, status: :unprocessable_entity
52
+ end
53
+ end
54
+
55
+ # DELETE /whatsapps/1
56
+ def destroy
57
+ @whatsapp.destroy
58
+ end
59
+
60
+ private
61
+
62
+ # Use callbacks to share common setup or constraints between actions.
63
+ def set_whatsapp
64
+ @whatsapp = Whatsapp.find(params[:id])
65
+ end
66
+
67
+ # Only allow a trusted parameter "white list" through.
68
+ # NOTE: Twilio sends params as SmsStatus
69
+ def whatsapp_params
70
+ request.request_parameters.deep_transform_keys!(&:underscore).except(:controller, :action)
71
+ end
72
+ end
73
+ # end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CallJob < Comm::ApplicationJob
4
+ def perform(call:)
5
+ # 'http://demo.twilio.com/docs/voice.xml')
6
+ @tenant.twilio_client.calls.create(from: call.from, to: call.to, url: call.url)
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Comm
4
+ class ApplicationJob < Ros::ApplicationJob
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EventProcessJob < Comm::ApplicationJob
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MessageProcessJob < Comm::ApplicationJob
4
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MessageSendJob < Comm::ApplicationJob
4
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: 'from@example.com'
5
+ layout 'mailer'
6
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Campaign < Comm::ApplicationRecord
4
+ has_many :events
5
+ has_many :templates
6
+
7
+ before_save :set_base_url
8
+
9
+ def set_base_url
10
+ # https://{{tenant}}-blackcomb-sales.uat.whistler.perxtech.io/
11
+ self.base_url ||= current_tenant.properties.fetch(:campaign_base_url, '')
12
+ end
13
+
14
+ def final_url
15
+ "#{base_url}loading/"
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Comm
4
+ class ApplicationRecord < ::ApplicationRecord
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Event < Comm::ApplicationRecord
4
+ # - includes/extends
5
+ include AASM
6
+
7
+ # - constants
8
+
9
+ # - gems and related
10
+ # TODO: we should have an extra status that informs us that all the messages
11
+ # for this event have been scheduled
12
+ aasm whiny_transitions: true, column: :status do
13
+ state :pending, initial: true
14
+ state :processing, :published
15
+
16
+ after_all_transitions :log_status_change
17
+
18
+ event :process do
19
+ transitions from: :pending, to: :processing
20
+ end
21
+
22
+ event :publish do
23
+ transitions from: :processing, to: :published
24
+ end
25
+ end
26
+
27
+ # - serialized attributes
28
+
29
+ # - associations
30
+ belongs_to :template, optional: true
31
+ belongs_to :provider
32
+ belongs_to :campaign, optional: true
33
+ # maybe target should be cognito_pool_id
34
+ belongs_to_resource :target, polymorphic: true
35
+ belongs_to_resource :owner, polymorphic: true
36
+
37
+ has_many :messages, as: :owner
38
+ # api_has_many :users, through: :target
39
+
40
+ # - attr_accessible
41
+
42
+ # - scopes
43
+
44
+ # - class methods
45
+
46
+ # - validations
47
+ validate :provider_channel
48
+ # NOTE: if there channel is not weblink, then target is mandatory
49
+ validates :target, presence: true, if: -> { channel != 'weblink' }
50
+
51
+ # - callbacks
52
+ after_commit :queue_job, on: :create
53
+
54
+ # - other methods
55
+
56
+ # TODO: Decide if the target is always a Pool or not
57
+ # TODO: Implement as api_has_many :users, through: :target
58
+ def users
59
+ query_resource(:target) { |query| query.includes(:users) }.users
60
+ end
61
+
62
+ def log_status_change
63
+ Rails.logger.info("changing from #{aasm.from_state} to #{aasm.to_state} (event: #{aasm.current_event})")
64
+ end
65
+
66
+ # - private
67
+ private
68
+
69
+ def provider_channel
70
+ return unless provider
71
+
72
+ channels = provider.class.services + ['weblink']
73
+ return if channel.in? channels
74
+
75
+ errors.add(:channel, "must be one of: #{channels.join(' ')}")
76
+ end
77
+
78
+ def queue_job
79
+ EventProcessJob.set(wait_until: send_at).perform_later(id: id)
80
+ end
81
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Message < Comm::ApplicationRecord
4
+ belongs_to :provider
5
+ belongs_to :owner, polymorphic: true, optional: true
6
+
7
+ attr_accessor :recipient_id
8
+
9
+ def self.sent_to(phone_number)
10
+ search_number = phone_number.tr('^0-9', '%')
11
+ where('messages.to LIKE ?', search_number)
12
+ end
13
+
14
+ validate :provider_channel, if: :provider
15
+
16
+ def provider_channel
17
+ return if channel.in? provider.class.services
18
+
19
+ errors.add(:channel, "must be one of: #{provider.class.services.join(' ')}")
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Provider < Comm::ApplicationRecord
4
+ attr_reader :client
5
+
6
+ attr_encrypted_options.merge!(key: Settings.encryption_key, encode: true, encode_iv: true)
7
+ attr_encrypted :credential_1
8
+ attr_encrypted :credential_2
9
+ attr_encrypted :credential_3
10
+
11
+ validates :type, presence: true
12
+
13
+ def self.services
14
+ []
15
+ end
16
+
17
+ def provider_from
18
+ current_tenant.properties.dig(:from) || 'Perx'
19
+ end
20
+
21
+ def sms
22
+ raise NotImplementedError
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Providers
4
+ class Aws < Provider
5
+ alias_attribute :access_key_id, :credential_1
6
+ alias_attribute :secret_access_key, :credential_2
7
+
8
+ def self.services
9
+ %w[sms]
10
+ end
11
+
12
+ def client
13
+ return unless x_access_key_id && x_secret_access_key
14
+
15
+ @client ||= ::Aws::SNS::Client.new(client_params)
16
+ end
17
+
18
+ def x_access_key_id
19
+ access_key_id || (current_tenant.platform_aws_enabled ? ENV['AWS_ACCESS_KEY_ID'] : nil)
20
+ end
21
+
22
+ def x_secret_access_key
23
+ secret_access_key || (current_tenant.platform_aws_enabled ? ENV['AWS_SECRET_ACCESS_KEY'] : nil)
24
+ end
25
+
26
+ def provider_from
27
+ current_tenant.properties.dig(:from) || 'Perx'
28
+ end
29
+
30
+ def sms(from, to, body)
31
+ sender = from || provider_from
32
+ client.set_sms_attributes(attributes: { 'DefaultSenderID' => sender })
33
+ client.publish(phone_number: to, message: body)
34
+ rescue ::Aws::SNS::Errors::ServiceError => e
35
+ Rails.logger.warn("No AWS client configured for tenant.account_id. #{e.inspect}")
36
+ end
37
+
38
+ private
39
+
40
+ # TODO: Cleanup this logic. This should probably live in an initializer.
41
+ # The problem might be that a tenant uses Perx SNS credentials for sending
42
+ # the sms but then wants to use his own credentials for storing the assets
43
+ # in the S3. We should have the configuration being picked up from
44
+ # 1. settings, env variables, our defaults
45
+ def client_params
46
+ params = { region: 'ap-southeast-1',
47
+ access_key_id: x_access_key_id,
48
+ secret_access_key: x_secret_access_key }
49
+
50
+ params[:endpoint] = ENV['AWS_ENDPOINT'] unless ENV['AWS_ENDPOINT'].nil?
51
+ params
52
+ end
53
+ end
54
+ end