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