flow_chat 0.2.1 → 0.4.0
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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/Gemfile +3 -1
- data/README.md +784 -63
- data/Rakefile +7 -3
- data/examples/initializer.rb +31 -0
- data/examples/multi_tenant_whatsapp_controller.rb +248 -0
- data/examples/ussd_controller.rb +264 -0
- data/examples/whatsapp_controller.rb +141 -0
- data/lib/flow_chat/base_processor.rb +63 -0
- data/lib/flow_chat/config.rb +21 -8
- data/lib/flow_chat/context.rb +8 -0
- data/lib/flow_chat/interrupt.rb +2 -0
- data/lib/flow_chat/session/cache_session_store.rb +84 -0
- data/lib/flow_chat/session/middleware.rb +15 -7
- data/lib/flow_chat/ussd/app.rb +25 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +3 -1
- data/lib/flow_chat/ussd/gateway/nsano.rb +7 -1
- data/lib/flow_chat/ussd/middleware/pagination.rb +14 -17
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +18 -31
- data/lib/flow_chat/ussd/processor.rb +15 -42
- data/lib/flow_chat/ussd/prompt.rb +1 -1
- data/lib/flow_chat/ussd/simulator/controller.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +58 -0
- data/lib/flow_chat/whatsapp/configuration.rb +75 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +213 -0
- data/lib/flow_chat/whatsapp/middleware/executor.rb +30 -0
- data/lib/flow_chat/whatsapp/processor.rb +36 -0
- data/lib/flow_chat/whatsapp/prompt.rb +206 -0
- data/lib/flow_chat/whatsapp/template_manager.rb +162 -0
- data/lib/flow_chat.rb +1 -0
- metadata +17 -4
- data/.rspec +0 -3
@@ -0,0 +1,63 @@
|
|
1
|
+
require "middleware"
|
2
|
+
|
3
|
+
module FlowChat
|
4
|
+
class BaseProcessor
|
5
|
+
attr_reader :middleware, :gateway
|
6
|
+
|
7
|
+
def initialize(controller)
|
8
|
+
@context = FlowChat::Context.new
|
9
|
+
@context["controller"] = controller
|
10
|
+
@middleware = ::Middleware::Builder.new(name: middleware_name)
|
11
|
+
|
12
|
+
yield self if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
def use_gateway(gateway)
|
16
|
+
@gateway = gateway
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def use_session_store(session_store)
|
21
|
+
@context["session.store"] = session_store
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
def use_middleware(middleware)
|
26
|
+
@middleware.use middleware
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
def run(flow_class, action)
|
31
|
+
@context["flow.name"] = flow_class.name.underscore
|
32
|
+
@context["flow.class"] = flow_class
|
33
|
+
@context["flow.action"] = action
|
34
|
+
|
35
|
+
stack = build_middleware_stack
|
36
|
+
yield stack if block_given?
|
37
|
+
|
38
|
+
stack.call(@context)
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
|
43
|
+
# Subclasses should override these methods
|
44
|
+
def middleware_name
|
45
|
+
raise NotImplementedError, "Subclasses must implement middleware_name"
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_middleware_stack
|
49
|
+
raise NotImplementedError, "Subclasses must implement build_middleware_stack"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Helper method for building stacks
|
53
|
+
def create_middleware_stack(name)
|
54
|
+
::Middleware::Builder.new(name: name) do |b|
|
55
|
+
configure_middleware_stack(b)
|
56
|
+
end.inject_logger(Rails.logger)
|
57
|
+
end
|
58
|
+
|
59
|
+
def configure_middleware_stack(builder)
|
60
|
+
raise NotImplementedError, "Subclasses must implement configure_middleware_stack"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/flow_chat/config.rb
CHANGED
@@ -1,16 +1,29 @@
|
|
1
1
|
module FlowChat
|
2
2
|
module Config
|
3
|
+
# General framework configuration
|
3
4
|
mattr_accessor :logger, default: Logger.new($stdout)
|
4
5
|
mattr_accessor :cache, default: nil
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
mattr_accessor :pagination_next_text, default: "More"
|
7
|
+
# USSD-specific configuration object
|
8
|
+
def self.ussd
|
9
|
+
@ussd ||= UssdConfig.new
|
10
|
+
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
class UssdConfig
|
13
|
+
attr_accessor :pagination_page_size, :pagination_back_option, :pagination_back_text,
|
14
|
+
:pagination_next_option, :pagination_next_text,
|
15
|
+
:resumable_sessions_enabled, :resumable_sessions_global, :resumable_sessions_timeout_seconds
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@pagination_page_size = 140
|
19
|
+
@pagination_back_option = "0"
|
20
|
+
@pagination_back_text = "Back"
|
21
|
+
@pagination_next_option = "#"
|
22
|
+
@pagination_next_text = "More"
|
23
|
+
@resumable_sessions_enabled = false
|
24
|
+
@resumable_sessions_global = true
|
25
|
+
@resumable_sessions_timeout_seconds = 300
|
26
|
+
end
|
27
|
+
end
|
15
28
|
end
|
16
29
|
end
|
data/lib/flow_chat/context.rb
CHANGED
@@ -14,8 +14,16 @@ module FlowChat
|
|
14
14
|
|
15
15
|
def input = @data["request.input"]
|
16
16
|
|
17
|
+
def input=(value)
|
18
|
+
@data["request.input"] = value
|
19
|
+
end
|
20
|
+
|
17
21
|
def session = @data["session"]
|
18
22
|
|
23
|
+
def session=(value)
|
24
|
+
@data["session"] = value
|
25
|
+
end
|
26
|
+
|
19
27
|
def controller = @data["controller"]
|
20
28
|
|
21
29
|
# def request = controller.request
|
data/lib/flow_chat/interrupt.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module FlowChat
|
2
2
|
module Interrupt
|
3
|
+
# standard:disable Lint/InheritException
|
3
4
|
class Base < Exception
|
4
5
|
attr_reader :prompt
|
5
6
|
|
@@ -8,6 +9,7 @@ module FlowChat
|
|
8
9
|
super
|
9
10
|
end
|
10
11
|
end
|
12
|
+
# standard:enable Lint/InheritException
|
11
13
|
|
12
14
|
class Prompt < Base
|
13
15
|
attr_reader :choices
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Session
|
3
|
+
class CacheSessionStore
|
4
|
+
def initialize(context, cache = nil)
|
5
|
+
@context = context
|
6
|
+
@cache = cache || FlowChat::Config.cache
|
7
|
+
|
8
|
+
raise ArgumentError, "Cache is required. Set FlowChat::Config.cache or pass a cache instance." unless @cache
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(key)
|
12
|
+
return nil unless @context
|
13
|
+
|
14
|
+
data = @cache.read(session_key)
|
15
|
+
return nil unless data
|
16
|
+
|
17
|
+
data[key.to_s]
|
18
|
+
end
|
19
|
+
|
20
|
+
def set(key, value)
|
21
|
+
return unless @context
|
22
|
+
|
23
|
+
data = @cache.read(session_key) || {}
|
24
|
+
data[key.to_s] = value
|
25
|
+
|
26
|
+
@cache.write(session_key, data, expires_in: session_ttl)
|
27
|
+
value
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(key)
|
31
|
+
return unless @context
|
32
|
+
|
33
|
+
data = @cache.read(session_key)
|
34
|
+
return unless data
|
35
|
+
|
36
|
+
data.delete(key.to_s)
|
37
|
+
@cache.write(session_key, data, expires_in: session_ttl)
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
return unless @context
|
42
|
+
|
43
|
+
@cache.delete(session_key)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Alias for compatibility
|
47
|
+
alias_method :destroy, :clear
|
48
|
+
|
49
|
+
def exists?
|
50
|
+
@cache.exist?(session_key)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def session_key
|
56
|
+
gateway = @context["request.gateway"]
|
57
|
+
msisdn = @context["request.msisdn"]
|
58
|
+
|
59
|
+
case gateway
|
60
|
+
when :whatsapp_cloud_api
|
61
|
+
"flow_chat:session:whatsapp:#{msisdn}"
|
62
|
+
when :nalo, :nsano
|
63
|
+
session_id = @context["request.id"]
|
64
|
+
"flow_chat:session:ussd:#{session_id}:#{msisdn}"
|
65
|
+
else
|
66
|
+
"flow_chat:session:unknown:#{msisdn}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def session_ttl
|
71
|
+
gateway = @context["request.gateway"]
|
72
|
+
|
73
|
+
case gateway
|
74
|
+
when :whatsapp_cloud_api
|
75
|
+
7.days # WhatsApp conversations can be long-lived
|
76
|
+
when :nalo, :nsano
|
77
|
+
1.hour # USSD sessions are typically short
|
78
|
+
else
|
79
|
+
1.day # Default
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -7,19 +7,27 @@ module FlowChat
|
|
7
7
|
|
8
8
|
def call(context)
|
9
9
|
context["session.id"] = session_id context
|
10
|
-
context
|
10
|
+
context.session = context["session.store"].new(context)
|
11
11
|
@app.call(context)
|
12
12
|
end
|
13
13
|
|
14
14
|
private
|
15
15
|
|
16
16
|
def session_id(context)
|
17
|
-
context["request.
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
gateway = context["request.gateway"]
|
18
|
+
flow_name = context["flow.name"]
|
19
|
+
case gateway
|
20
|
+
when :whatsapp_cloud_api
|
21
|
+
# For WhatsApp, use phone number + flow name for consistent sessions
|
22
|
+
phone = context["request.msisdn"]
|
23
|
+
"#{gateway}:#{flow_name}:#{phone}"
|
24
|
+
# when :nalo, :nsano
|
25
|
+
# # For USSD, use the request ID from the gateway
|
26
|
+
# "#{gateway}:#{flow_name}:#{context["request.id"]}"
|
27
|
+
else
|
28
|
+
# Fallback to request ID
|
29
|
+
"#{gateway}:#{flow_name}:#{context["request.id"]}"
|
30
|
+
end
|
23
31
|
end
|
24
32
|
end
|
25
33
|
end
|
data/lib/flow_chat/ussd/app.rb
CHANGED
@@ -28,6 +28,31 @@ module FlowChat
|
|
28
28
|
def say(msg)
|
29
29
|
raise FlowChat::Interrupt::Terminate.new(msg)
|
30
30
|
end
|
31
|
+
|
32
|
+
# WhatsApp-specific data accessors (not supported in USSD)
|
33
|
+
def contact_name
|
34
|
+
nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def message_id
|
38
|
+
context["request.message_id"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def timestamp
|
42
|
+
context["request.timestamp"]
|
43
|
+
end
|
44
|
+
|
45
|
+
def location
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def media
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def phone_number
|
54
|
+
context["request.msisdn"]
|
55
|
+
end
|
31
56
|
end
|
32
57
|
end
|
33
58
|
end
|
@@ -12,11 +12,13 @@ module FlowChat
|
|
12
12
|
params = context.controller.request.params
|
13
13
|
|
14
14
|
context["request.id"] = params["USERID"]
|
15
|
+
context["request.message_id"] = SecureRandom.uuid
|
16
|
+
context["request.timestamp"] = Time.current.iso8601
|
15
17
|
context["request.gateway"] = :nalo
|
16
18
|
context["request.network"] = nil
|
17
19
|
context["request.msisdn"] = Phonelib.parse(params["MSISDN"]).e164
|
18
20
|
# context["request.type"] = params["MSGTYPE"] ? :initial : :response
|
19
|
-
context
|
21
|
+
context.input = params["USERDATA"].presence
|
20
22
|
|
21
23
|
type, prompt, choices = @app.call(context)
|
22
24
|
|
@@ -10,7 +10,13 @@ module FlowChat
|
|
10
10
|
|
11
11
|
def call(context)
|
12
12
|
controller = context["controller"]
|
13
|
-
|
13
|
+
controller.request
|
14
|
+
|
15
|
+
# Add timestamp for all requests
|
16
|
+
context["request.timestamp"] = Time.current.iso8601
|
17
|
+
|
18
|
+
# Set a basic message_id (can be enhanced based on actual Nsano implementation)
|
19
|
+
context["request.message_id"] = SecureRandom.uuid
|
14
20
|
|
15
21
|
# input = context["rack.input"].read
|
16
22
|
# context["rack.input"].rewind
|
@@ -12,16 +12,14 @@ module FlowChat
|
|
12
12
|
|
13
13
|
if intercept?
|
14
14
|
type, prompt = handle_intercepted_request
|
15
|
-
[type, prompt, []]
|
16
15
|
else
|
17
16
|
@session.delete "ussd.pagination"
|
18
17
|
type, prompt, choices = @app.call(context)
|
19
18
|
|
20
19
|
prompt = FlowChat::Ussd::Renderer.new(prompt, choices).render
|
21
20
|
type, prompt = maybe_paginate(type, prompt) if prompt.present?
|
22
|
-
|
23
|
-
[type, prompt, []]
|
24
21
|
end
|
22
|
+
[type, prompt, []]
|
25
23
|
end
|
26
24
|
|
27
25
|
private
|
@@ -29,11 +27,11 @@ module FlowChat
|
|
29
27
|
def intercept?
|
30
28
|
pagination_state.present? &&
|
31
29
|
(pagination_state["type"].to_sym == :terminal ||
|
32
|
-
([Config.pagination_next_option, Config.pagination_back_option].include? @context.input))
|
30
|
+
([FlowChat::Config.ussd.pagination_next_option, FlowChat::Config.ussd.pagination_back_option].include? @context.input))
|
33
31
|
end
|
34
32
|
|
35
33
|
def handle_intercepted_request
|
36
|
-
Config.logger&.info "FlowChat::Middleware::Pagination :: Intercepted to handle pagination"
|
34
|
+
FlowChat::Config.logger&.info "FlowChat::Middleware::Pagination :: Intercepted to handle pagination"
|
37
35
|
start, finish, has_more = calculate_offsets
|
38
36
|
type = (pagination_state["type"].to_sym == :terminal && !has_more) ? :terminal : :prompt
|
39
37
|
prompt = pagination_state["prompt"][start..finish].strip + build_pagination_options(type, has_more)
|
@@ -43,9 +41,9 @@ module FlowChat
|
|
43
41
|
end
|
44
42
|
|
45
43
|
def maybe_paginate(type, prompt)
|
46
|
-
if prompt.length > Config.pagination_page_size
|
44
|
+
if prompt.length > FlowChat::Config.ussd.pagination_page_size
|
47
45
|
original_prompt = prompt
|
48
|
-
Config.logger&.info "FlowChat::Middleware::Pagination :: Response length (#{prompt.length}) exceeds page size (#{Config.pagination_page_size}). Paginating."
|
46
|
+
FlowChat::Config.logger&.info "FlowChat::Middleware::Pagination :: Response length (#{prompt.length}) exceeds page size (#{FlowChat::Config.ussd.pagination_page_size}). Paginating."
|
49
47
|
prompt = prompt[0..single_option_slice_size]
|
50
48
|
# Ensure we do not cut words and options off in the middle.
|
51
49
|
current_pagebreak = prompt[single_option_slice_size + 1].blank? ? single_option_slice_size : prompt.rindex("\n") || prompt.rindex(" ") || single_option_slice_size
|
@@ -60,12 +58,12 @@ module FlowChat
|
|
60
58
|
page = current_page
|
61
59
|
offset = pagination_state["offsets"][page.to_s]
|
62
60
|
if offset.present?
|
63
|
-
Config.logger&.debug "FlowChat::Middleware::Pagination :: Reusing cached offset for page: #{page}"
|
61
|
+
FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: Reusing cached offset for page: #{page}"
|
64
62
|
start = offset["start"]
|
65
63
|
finish = offset["finish"]
|
66
64
|
has_more = pagination_state["prompt"].length > finish
|
67
65
|
else
|
68
|
-
Config.logger&.debug "FlowChat::Middleware::Pagination :: Calculating offset for page: #{page}"
|
66
|
+
FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: Calculating offset for page: #{page}"
|
69
67
|
# We are guaranteed a previous offset because it was set in maybe_paginate
|
70
68
|
previous_page = page - 1
|
71
69
|
previous_offset = pagination_state["offsets"][previous_page.to_s]
|
@@ -73,8 +71,7 @@ module FlowChat
|
|
73
71
|
has_more, len = (pagination_state["prompt"].length > start + single_option_slice_size) ? [true, dual_options_slice_size] : [false, single_option_slice_size]
|
74
72
|
finish = start + len
|
75
73
|
if start > pagination_state["prompt"].length
|
76
|
-
Config.logger&.debug "FlowChat::Middleware::Pagination :: No content exists for page: #{page}. Reverting to page: #{page - 1}"
|
77
|
-
page -= 1
|
74
|
+
FlowChat::Config.logger&.debug "FlowChat::Middleware::Pagination :: No content exists for page: #{page}. Reverting to page: #{page - 1}"
|
78
75
|
has_more = false
|
79
76
|
start = previous_offset["start"]
|
80
77
|
finish = previous_offset["finish"]
|
@@ -100,11 +97,11 @@ module FlowChat
|
|
100
97
|
end
|
101
98
|
|
102
99
|
def next_option
|
103
|
-
"#{Config.pagination_next_option} #{Config.pagination_next_text}"
|
100
|
+
"#{FlowChat::Config.ussd.pagination_next_option} #{FlowChat::Config.ussd.pagination_next_text}"
|
104
101
|
end
|
105
102
|
|
106
103
|
def back_option
|
107
|
-
"#{Config.pagination_back_option} #{Config.pagination_back_text}"
|
104
|
+
"#{FlowChat::Config.ussd.pagination_back_option} #{FlowChat::Config.ussd.pagination_back_text}"
|
108
105
|
end
|
109
106
|
|
110
107
|
def single_option_slice_size
|
@@ -112,7 +109,7 @@ module FlowChat
|
|
112
109
|
# To display a single back or next option
|
113
110
|
# We accomodate the 2 newlines and the longest of the options
|
114
111
|
# We subtract an additional 1 to normalize it for slicing
|
115
|
-
@single_option_slice_size = Config.pagination_page_size - 2 - [next_option.length, back_option.length].max - 1
|
112
|
+
@single_option_slice_size = FlowChat::Config.ussd.pagination_page_size - 2 - [next_option.length, back_option.length].max - 1
|
116
113
|
end
|
117
114
|
@single_option_slice_size
|
118
115
|
end
|
@@ -121,16 +118,16 @@ module FlowChat
|
|
121
118
|
unless @dual_options_slice_size.present?
|
122
119
|
# To display both back and next options
|
123
120
|
# We accomodate the 3 newlines and both of the options
|
124
|
-
@dual_options_slice_size = Config.pagination_page_size - 3 - [next_option.length, back_option.length].sum - 1
|
121
|
+
@dual_options_slice_size = FlowChat::Config.ussd.pagination_page_size - 3 - [next_option.length, back_option.length].sum - 1
|
125
122
|
end
|
126
123
|
@dual_options_slice_size
|
127
124
|
end
|
128
125
|
|
129
126
|
def current_page
|
130
127
|
page = pagination_state["page"]
|
131
|
-
if @context.input == Config.pagination_back_option
|
128
|
+
if @context.input == FlowChat::Config.ussd.pagination_back_option
|
132
129
|
page -= 1
|
133
|
-
elsif @context.input == Config.pagination_next_option
|
130
|
+
elsif @context.input == FlowChat::Config.ussd.pagination_next_option
|
134
131
|
page += 1
|
135
132
|
end
|
136
133
|
[page, 1].max
|
@@ -7,44 +7,31 @@ module FlowChat
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def call(context)
|
10
|
-
if Config.resumable_sessions_enabled && context["ussd.request"].present?
|
11
|
-
|
12
|
-
session
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
context["ussd.request"][:type] = :response
|
21
|
-
context["ussd.resumable_sessions"][:resumed] = true
|
10
|
+
if FlowChat::Config.ussd.resumable_sessions_enabled && context["ussd.request"].present?
|
11
|
+
# First, try to find any interruption session.
|
12
|
+
# The session key can be:
|
13
|
+
# - a global session (key: "global")
|
14
|
+
# - a provider-specific session (key: <provider>)
|
15
|
+
session_key = self.class.session_key(context)
|
16
|
+
resumable_session = context["session.store"].get(session_key)
|
17
|
+
|
18
|
+
if resumable_session.present? && valid?(resumable_session)
|
19
|
+
context.merge! resumable_session
|
22
20
|
end
|
23
|
-
|
24
|
-
res = @app.call(context)
|
25
|
-
|
26
|
-
if context["ussd.response"].present?
|
27
|
-
if context["ussd.response"][:type] == :terminal || context["ussd.resumable_sessions"][:disable]
|
28
|
-
session.delete "ussd.resumable_sessions"
|
29
|
-
else
|
30
|
-
session["ussd.resumable_sessions"] = Time.now.to_i
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
res
|
35
|
-
else
|
36
|
-
@app.call(context)
|
37
21
|
end
|
22
|
+
|
23
|
+
@app.call(context)
|
38
24
|
end
|
39
25
|
|
40
26
|
private
|
41
27
|
|
42
|
-
def
|
43
|
-
return unless
|
44
|
-
return true unless Config.resumable_sessions_timeout_seconds
|
28
|
+
def valid?(session)
|
29
|
+
return true unless FlowChat::Config.ussd.resumable_sessions_timeout_seconds
|
45
30
|
|
46
|
-
last_active_at = Time.
|
47
|
-
(Time.
|
31
|
+
last_active_at = Time.parse session.dig("context", "last_active_at")
|
32
|
+
(Time.current - FlowChat::Config.ussd.resumable_sessions_timeout_seconds) < last_active_at
|
33
|
+
rescue
|
34
|
+
false
|
48
35
|
end
|
49
36
|
end
|
50
37
|
end
|
@@ -1,55 +1,28 @@
|
|
1
|
-
require "middleware"
|
2
|
-
|
3
1
|
module FlowChat
|
4
2
|
module Ussd
|
5
|
-
class Processor
|
6
|
-
attr_reader :middleware, :gateway
|
7
|
-
|
8
|
-
def initialize(controller)
|
9
|
-
@context = FlowChat::Context.new
|
10
|
-
@context["controller"] = controller
|
11
|
-
@middleware = ::Middleware::Builder.new(name: "ussd.middleware")
|
12
|
-
|
13
|
-
yield self if block_given?
|
14
|
-
end
|
15
|
-
|
16
|
-
def use_gateway(gateway)
|
17
|
-
@gateway = gateway
|
18
|
-
self
|
19
|
-
end
|
20
|
-
|
21
|
-
def use_session_store(session_store)
|
22
|
-
@context["session.store"] = session_store
|
23
|
-
self
|
24
|
-
end
|
25
|
-
|
26
|
-
def use_middleware(middleware)
|
27
|
-
@middleware.use middleware
|
28
|
-
self
|
29
|
-
end
|
30
|
-
|
3
|
+
class Processor < FlowChat::BaseProcessor
|
31
4
|
def use_resumable_sessions
|
32
5
|
middleware.insert_before 0, FlowChat::Ussd::Middleware::ResumableSession
|
33
6
|
self
|
34
7
|
end
|
35
8
|
|
36
|
-
|
37
|
-
@context["flow.name"] = flow_class.name.underscore
|
38
|
-
@context["flow.class"] = flow_class
|
39
|
-
@context["flow.action"] = action
|
9
|
+
protected
|
40
10
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
b.use FlowChat::Ussd::Middleware::Pagination
|
45
|
-
b.use middleware
|
46
|
-
b.use FlowChat::Ussd::Middleware::Executor
|
47
|
-
end.inject_logger(Rails.logger)
|
11
|
+
def middleware_name
|
12
|
+
"ussd.middleware"
|
13
|
+
end
|
48
14
|
|
49
|
-
|
15
|
+
def build_middleware_stack
|
16
|
+
create_middleware_stack("ussd")
|
17
|
+
end
|
50
18
|
|
51
|
-
|
19
|
+
def configure_middleware_stack(builder)
|
20
|
+
builder.use gateway
|
21
|
+
builder.use FlowChat::Session::Middleware
|
22
|
+
builder.use FlowChat::Ussd::Middleware::Pagination
|
23
|
+
builder.use middleware
|
24
|
+
builder.use FlowChat::Ussd::Middleware::Executor
|
52
25
|
end
|
53
26
|
end
|
54
27
|
end
|
55
|
-
end
|
28
|
+
end
|
@@ -32,7 +32,7 @@ module FlowChat
|
|
32
32
|
msg,
|
33
33
|
choices: choices_prompt,
|
34
34
|
convert: lambda { |choice| choice.to_i },
|
35
|
-
validate: lambda { |choice| "Invalid selection:" unless (1..choices.size).
|
35
|
+
validate: lambda { |choice| "Invalid selection:" unless (1..choices.size).cover?(choice) },
|
36
36
|
transform: lambda { |choice| choices[choice - 1] }
|
37
37
|
)
|
38
38
|
end
|
data/lib/flow_chat/version.rb
CHANGED
@@ -0,0 +1,58 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Whatsapp
|
3
|
+
class App
|
4
|
+
attr_reader :session, :input, :context, :navigation_stack
|
5
|
+
|
6
|
+
def initialize(context)
|
7
|
+
@context = context
|
8
|
+
@session = context.session
|
9
|
+
@input = context.input
|
10
|
+
@navigation_stack = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def screen(key)
|
14
|
+
raise ArgumentError, "a block is expected" unless block_given?
|
15
|
+
raise ArgumentError, "screen has been presented" if navigation_stack.include?(key)
|
16
|
+
|
17
|
+
navigation_stack << key
|
18
|
+
return session.get(key) if session.get(key).present?
|
19
|
+
|
20
|
+
prompt = FlowChat::Whatsapp::Prompt.new input
|
21
|
+
@input = nil # input is being submitted to prompt so we clear it
|
22
|
+
|
23
|
+
value = yield prompt
|
24
|
+
session.set(key, value)
|
25
|
+
value
|
26
|
+
end
|
27
|
+
|
28
|
+
def say(msg)
|
29
|
+
raise FlowChat::Interrupt::Terminate.new([:text, msg, {}])
|
30
|
+
end
|
31
|
+
|
32
|
+
# WhatsApp-specific data accessors (read-only)
|
33
|
+
def contact_name
|
34
|
+
context["request.contact_name"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def message_id
|
38
|
+
context["request.message_id"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def timestamp
|
42
|
+
context["request.timestamp"]
|
43
|
+
end
|
44
|
+
|
45
|
+
def location
|
46
|
+
context["request.location"]
|
47
|
+
end
|
48
|
+
|
49
|
+
def media
|
50
|
+
context["request.media"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def phone_number
|
54
|
+
context["request.msisdn"]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|