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
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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,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,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,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
|
data/app/models/event.rb
ADDED
@@ -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
|