flow_chat 0.6.1 → 0.8.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/.github/workflows/ci.yml +44 -0
- data/.gitignore +2 -1
- data/README.md +85 -1229
- data/docs/configuration.md +360 -0
- data/docs/flows.md +320 -0
- data/docs/images/simulator.png +0 -0
- data/docs/instrumentation.md +216 -0
- data/docs/media.md +153 -0
- data/docs/sessions.md +433 -0
- data/docs/testing.md +475 -0
- data/docs/ussd-setup.md +322 -0
- data/docs/whatsapp-setup.md +162 -0
- data/examples/multi_tenant_whatsapp_controller.rb +9 -37
- data/examples/simulator_controller.rb +13 -22
- data/examples/ussd_controller.rb +41 -41
- data/examples/whatsapp_controller.rb +32 -125
- data/examples/whatsapp_media_examples.rb +68 -336
- data/examples/whatsapp_message_job.rb +5 -3
- data/flow_chat.gemspec +6 -2
- data/lib/flow_chat/base_processor.rb +79 -2
- data/lib/flow_chat/config.rb +31 -5
- data/lib/flow_chat/context.rb +13 -1
- data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
- data/lib/flow_chat/instrumentation/setup.rb +155 -0
- data/lib/flow_chat/instrumentation.rb +70 -0
- data/lib/flow_chat/prompt.rb +20 -20
- data/lib/flow_chat/session/cache_session_store.rb +73 -7
- data/lib/flow_chat/session/middleware.rb +130 -12
- data/lib/flow_chat/session/rails_session_store.rb +36 -1
- data/lib/flow_chat/simulator/controller.rb +8 -8
- data/lib/flow_chat/simulator/views/simulator.html.erb +5 -5
- data/lib/flow_chat/ussd/gateway/nalo.rb +31 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +36 -2
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
- data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
- data/lib/flow_chat/ussd/processor.rb +16 -4
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +99 -12
- data/lib/flow_chat/whatsapp/configuration.rb +35 -4
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +121 -34
- data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
- data/lib/flow_chat/whatsapp/processor.rb +7 -1
- data/lib/flow_chat/whatsapp/renderer.rb +4 -9
- data/lib/flow_chat.rb +23 -0
- metadata +23 -12
- data/.travis.yml +0 -6
- data/app/controllers/demo_controller.rb +0 -101
- data/app/flow_chat/demo_restaurant_flow.rb +0 -889
- data/config/routes_demo.rb +0 -59
- data/examples/initializer.rb +0 -86
- data/examples/media_prompts_examples.rb +0 -27
- data/images/ussd_simulator.png +0 -0
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +0 -39
@@ -4,11 +4,16 @@ module FlowChat
|
|
4
4
|
module Ussd
|
5
5
|
module Gateway
|
6
6
|
class Nsano
|
7
|
+
include FlowChat::Instrumentation
|
8
|
+
|
9
|
+
attr_reader :context
|
10
|
+
|
7
11
|
def initialize(app)
|
8
12
|
@app = app
|
9
13
|
end
|
10
14
|
|
11
15
|
def call(context)
|
16
|
+
@context = context
|
12
17
|
controller = context["controller"]
|
13
18
|
controller.request
|
14
19
|
|
@@ -17,6 +22,35 @@ module FlowChat
|
|
17
22
|
|
18
23
|
# Set a basic message_id (can be enhanced based on actual Nsano implementation)
|
19
24
|
context["request.message_id"] = SecureRandom.uuid
|
25
|
+
context["request.platform"] = :ussd
|
26
|
+
|
27
|
+
# TODO: Implement Nsano-specific parameter parsing
|
28
|
+
# For now, add basic instrumentation structure for when this is implemented
|
29
|
+
|
30
|
+
# Placeholder instrumentation - indicates Nsano implementation is needed
|
31
|
+
instrument(Events::MESSAGE_RECEIVED, {
|
32
|
+
from: "TODO", # Would be parsed from Nsano params
|
33
|
+
message: "TODO", # Would be actual user input
|
34
|
+
session_id: "TODO", # Would be Nsano session ID
|
35
|
+
gateway: :nsano,
|
36
|
+
platform: :ussd,
|
37
|
+
timestamp: context["request.timestamp"]
|
38
|
+
})
|
39
|
+
|
40
|
+
# Process request with placeholder app call
|
41
|
+
_, _, _, _ = @app.call(context) if @app
|
42
|
+
|
43
|
+
# Placeholder response instrumentation
|
44
|
+
instrument(Events::MESSAGE_SENT, {
|
45
|
+
to: "TODO", # Would be actual phone number
|
46
|
+
session_id: "TODO", # Would be Nsano session ID
|
47
|
+
message: "TODO", # Would be actual response message
|
48
|
+
message_type: "prompt", # Would depend on actual response type
|
49
|
+
gateway: :nsano,
|
50
|
+
platform: :ussd,
|
51
|
+
content_length: 0, # Would be actual content length
|
52
|
+
timestamp: context["request.timestamp"]
|
53
|
+
})
|
20
54
|
|
21
55
|
# input = context["rack.input"].read
|
22
56
|
# context["rack.input"].rewind
|
@@ -25,7 +59,7 @@ module FlowChat
|
|
25
59
|
# if params["network"].present? && params["UserSessionID"].present?
|
26
60
|
# request_id = "nsano::request_id::#{params["UserSessionID"]}"
|
27
61
|
# context["ussd.request"] = {
|
28
|
-
#
|
62
|
+
# gateway: :nsano,
|
29
63
|
# network: params["network"].to_sym,
|
30
64
|
# msisdn: Phonelib.parse(params["msisdn"]).e164,
|
31
65
|
# type: Config.cache&.read(request_id).present? ? :response : :initial,
|
@@ -37,7 +71,7 @@ module FlowChat
|
|
37
71
|
|
38
72
|
# status, headers, response = @app.call(context)
|
39
73
|
|
40
|
-
# if context["ussd.response"].present? && context["ussd.request"][:
|
74
|
+
# if context["ussd.response"].present? && context["ussd.request"][:gateway] == :nsano
|
41
75
|
# if context["ussd.response"][:type] == :terminal
|
42
76
|
# Config.cache&.write(request_id, nil)
|
43
77
|
# else
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Ussd
|
3
|
+
module Middleware
|
4
|
+
class ChoiceMapper
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
FlowChat.logger.debug { "Ussd::ChoiceMapper: Initialized USSD choice mapping middleware" }
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(context)
|
11
|
+
@context = context
|
12
|
+
@session = context.session
|
13
|
+
|
14
|
+
session_id = context["session.id"]
|
15
|
+
FlowChat.logger.debug { "Ussd::ChoiceMapper: Processing request for session #{session_id}" }
|
16
|
+
|
17
|
+
if intercept?
|
18
|
+
FlowChat.logger.info { "Ussd::ChoiceMapper: Intercepting request for choice resolution - session #{session_id}" }
|
19
|
+
handle_choice_input
|
20
|
+
end
|
21
|
+
|
22
|
+
# Clear choice mapping state for new flows
|
23
|
+
clear_choice_state_if_needed
|
24
|
+
type, prompt, choices, media = @app.call(context)
|
25
|
+
|
26
|
+
if choices.present?
|
27
|
+
FlowChat.logger.debug { "Ussd::ChoiceMapper: Found choices, creating number mapping" }
|
28
|
+
choices = create_numbered_mapping(choices)
|
29
|
+
end
|
30
|
+
|
31
|
+
[type, prompt, choices, media]
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def intercept?
|
37
|
+
# Intercept if we have choice mapping state and user input is a number that maps to a choice
|
38
|
+
choice_mapping = get_choice_mapping
|
39
|
+
should_intercept = choice_mapping.present? &&
|
40
|
+
@context.input.present? &&
|
41
|
+
choice_mapping.key?(@context.input.to_s)
|
42
|
+
|
43
|
+
if should_intercept
|
44
|
+
FlowChat.logger.debug { "Ussd::ChoiceMapper: Intercepting - input: #{@context.input}, mapped to: #{choice_mapping[@context.input.to_s]}" }
|
45
|
+
end
|
46
|
+
|
47
|
+
should_intercept
|
48
|
+
end
|
49
|
+
|
50
|
+
def handle_choice_input
|
51
|
+
choice_mapping = get_choice_mapping
|
52
|
+
original_choice = choice_mapping[@context.input.to_s]
|
53
|
+
|
54
|
+
FlowChat.logger.info { "Ussd::ChoiceMapper: Resolving choice input #{@context.input} to #{original_choice}" }
|
55
|
+
|
56
|
+
# Replace the numeric input with the original choice
|
57
|
+
@context.input = original_choice
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_numbered_mapping(choices)
|
61
|
+
# Choices are always a hash after normalize_choices
|
62
|
+
numbered_choices = {}
|
63
|
+
choice_mapping = {}
|
64
|
+
|
65
|
+
choices.each_with_index do |(key, value), index|
|
66
|
+
number = (index + 1).to_s
|
67
|
+
numbered_choices[number] = value
|
68
|
+
choice_mapping[number] = key.to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
store_choice_mapping(choice_mapping)
|
72
|
+
FlowChat.logger.debug { "Ussd::ChoiceMapper: Created mapping: #{choice_mapping}" }
|
73
|
+
numbered_choices
|
74
|
+
end
|
75
|
+
|
76
|
+
def store_choice_mapping(mapping)
|
77
|
+
@session.set("ussd.choice_mapping", mapping)
|
78
|
+
FlowChat.logger.debug { "Ussd::ChoiceMapper: Stored choice mapping: #{mapping}" }
|
79
|
+
end
|
80
|
+
|
81
|
+
def get_choice_mapping
|
82
|
+
@session.get("ussd.choice_mapping") || {}
|
83
|
+
end
|
84
|
+
|
85
|
+
def clear_choice_mapping
|
86
|
+
@session.delete("ussd.choice_mapping")
|
87
|
+
FlowChat.logger.debug { "Ussd::ChoiceMapper: Cleared choice mapping" }
|
88
|
+
end
|
89
|
+
|
90
|
+
def clear_choice_state_if_needed
|
91
|
+
# Clear choice mapping if this is a new flow (no input or fresh start)
|
92
|
+
if @context.input.blank? || should_clear_for_new_flow?
|
93
|
+
clear_choice_mapping
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def should_clear_for_new_flow?
|
98
|
+
# Clear mapping if this input doesn't match any stored mapping
|
99
|
+
# This indicates we're in a new flow step
|
100
|
+
choice_mapping = get_choice_mapping
|
101
|
+
return false if choice_mapping.empty?
|
102
|
+
|
103
|
+
# If input is present but doesn't match any mapping, we're in a new flow
|
104
|
+
@context.input.present? && !choice_mapping.key?(@context.input.to_s)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -4,22 +4,44 @@ module FlowChat
|
|
4
4
|
class Executor
|
5
5
|
def initialize(app)
|
6
6
|
@app = app
|
7
|
+
FlowChat.logger.debug { "Ussd::Executor: Initialized USSD executor middleware" }
|
7
8
|
end
|
8
9
|
|
9
10
|
def call(context)
|
11
|
+
flow_class = context.flow
|
12
|
+
action = context["flow.action"]
|
13
|
+
session_id = context["session.id"]
|
14
|
+
|
15
|
+
FlowChat.logger.info { "Ussd::Executor: Executing flow #{flow_class.name}##{action} for session #{session_id}" }
|
16
|
+
|
10
17
|
ussd_app = build_ussd_app context
|
11
|
-
|
12
|
-
|
18
|
+
FlowChat.logger.debug { "Ussd::Executor: USSD app built for flow execution" }
|
19
|
+
|
20
|
+
flow = flow_class.new ussd_app
|
21
|
+
FlowChat.logger.debug { "Ussd::Executor: Flow instance created, invoking #{action} method" }
|
22
|
+
|
23
|
+
flow.send action
|
24
|
+
FlowChat.logger.warn { "Ussd::Executor: Flow execution failed to interact with user for #{flow_class.name}##{action}" }
|
25
|
+
raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
|
13
26
|
rescue FlowChat::Interrupt::Prompt => e
|
27
|
+
FlowChat.logger.info { "Ussd::Executor: Flow prompted user - Session: #{session_id}, Prompt: '#{e.prompt.truncate(100)}'" }
|
28
|
+
FlowChat.logger.debug { "Ussd::Executor: Prompt details - Choices: #{e.choices&.size || 0}, Has media: #{!e.media.nil?}" }
|
14
29
|
[:prompt, e.prompt, e.choices, e.media]
|
15
30
|
rescue FlowChat::Interrupt::Terminate => e
|
31
|
+
FlowChat.logger.info { "Ussd::Executor: Flow terminated - Session: #{session_id}, Message: '#{e.prompt.truncate(100)}'" }
|
32
|
+
FlowChat.logger.debug { "Ussd::Executor: Destroying session #{session_id}" }
|
16
33
|
context.session.destroy
|
17
34
|
[:terminate, e.prompt, nil, e.media]
|
35
|
+
rescue => error
|
36
|
+
FlowChat.logger.error { "Ussd::Executor: Flow execution failed - #{flow_class.name}##{action}, Session: #{session_id}, Error: #{error.class.name}: #{error.message}" }
|
37
|
+
FlowChat.logger.debug { "Ussd::Executor: Stack trace: #{error.backtrace.join("\n")}" }
|
38
|
+
raise
|
18
39
|
end
|
19
40
|
|
20
41
|
private
|
21
42
|
|
22
43
|
def build_ussd_app(context)
|
44
|
+
FlowChat.logger.debug { "Ussd::Executor: Building USSD app instance" }
|
23
45
|
FlowChat::Ussd::App.new(context)
|
24
46
|
end
|
25
47
|
end
|
@@ -2,23 +2,44 @@ module FlowChat
|
|
2
2
|
module Ussd
|
3
3
|
module Middleware
|
4
4
|
class Pagination
|
5
|
+
include FlowChat::Instrumentation
|
6
|
+
|
7
|
+
attr_reader :context
|
8
|
+
|
5
9
|
def initialize(app)
|
6
10
|
@app = app
|
11
|
+
FlowChat.logger.debug { "Ussd::Pagination: Initialized USSD pagination middleware" }
|
7
12
|
end
|
8
13
|
|
9
14
|
def call(context)
|
10
15
|
@context = context
|
11
16
|
@session = context.session
|
12
17
|
|
18
|
+
session_id = context["session.id"]
|
19
|
+
FlowChat.logger.debug { "Ussd::Pagination: Processing request for session #{session_id}" }
|
20
|
+
|
13
21
|
if intercept?
|
22
|
+
FlowChat.logger.info { "Ussd::Pagination: Intercepting request for pagination handling - session #{session_id}" }
|
14
23
|
type, prompt = handle_intercepted_request
|
15
24
|
[type, prompt, []]
|
16
25
|
else
|
26
|
+
# Clear pagination state for new flows
|
27
|
+
if pagination_state.present?
|
28
|
+
FlowChat.logger.debug { "Ussd::Pagination: Clearing pagination state for new flow - session #{session_id}" }
|
29
|
+
end
|
17
30
|
@session.delete "ussd.pagination"
|
31
|
+
|
18
32
|
type, prompt, choices, media = @app.call(context)
|
19
33
|
|
20
34
|
prompt = FlowChat::Ussd::Renderer.new(prompt, choices: choices, media: media).render
|
21
|
-
|
35
|
+
|
36
|
+
if prompt.present?
|
37
|
+
original_length = prompt.length
|
38
|
+
type, prompt = maybe_paginate(type, prompt)
|
39
|
+
if prompt.length != original_length
|
40
|
+
FlowChat.logger.info { "Ussd::Pagination: Content paginated - original: #{original_length} chars, paginated: #{prompt.length} chars" }
|
41
|
+
end
|
42
|
+
end
|
22
43
|
|
23
44
|
[type, prompt, []]
|
24
45
|
end
|
@@ -27,53 +48,91 @@ module FlowChat
|
|
27
48
|
private
|
28
49
|
|
29
50
|
def intercept?
|
30
|
-
pagination_state.present? &&
|
51
|
+
should_intercept = pagination_state.present? &&
|
31
52
|
(pagination_state["type"].to_sym == :terminal ||
|
32
53
|
([FlowChat::Config.ussd.pagination_next_option, FlowChat::Config.ussd.pagination_back_option].include? @context.input))
|
54
|
+
|
55
|
+
if should_intercept
|
56
|
+
FlowChat.logger.debug { "Ussd::Pagination: Intercepting - input: #{@context.input}, pagination type: #{pagination_state["type"]}" }
|
57
|
+
end
|
58
|
+
|
59
|
+
should_intercept
|
33
60
|
end
|
34
61
|
|
35
62
|
def handle_intercepted_request
|
36
|
-
FlowChat
|
63
|
+
FlowChat.logger.info { "Ussd::Pagination: Handling paginated request" }
|
37
64
|
start, finish, has_more = calculate_offsets
|
38
65
|
type = (pagination_state["type"].to_sym == :terminal && !has_more) ? :terminal : :prompt
|
39
66
|
prompt = pagination_state["prompt"][start..finish] + build_pagination_options(type, has_more)
|
40
67
|
set_pagination_state(current_page, start, finish)
|
41
68
|
|
69
|
+
# Instrument pagination navigation
|
70
|
+
instrument(Events::PAGINATION_TRIGGERED, {
|
71
|
+
session_id: @context["session.id"],
|
72
|
+
current_page: current_page,
|
73
|
+
total_pages: calculate_total_pages,
|
74
|
+
content_length: pagination_state["prompt"].length,
|
75
|
+
page_limit: FlowChat::Config.ussd.pagination_page_size,
|
76
|
+
navigation_action: (@context.input == FlowChat::Config.ussd.pagination_next_option) ? "next" : "back"
|
77
|
+
})
|
78
|
+
|
79
|
+
FlowChat.logger.debug { "Ussd::Pagination: Serving page content - start: #{start}, finish: #{finish}, has_more: #{has_more}, type: #{type}" }
|
42
80
|
[type, prompt]
|
43
81
|
end
|
44
82
|
|
45
83
|
def maybe_paginate(type, prompt)
|
46
84
|
if prompt.length > FlowChat::Config.ussd.pagination_page_size
|
47
85
|
original_prompt = prompt
|
48
|
-
FlowChat
|
86
|
+
FlowChat.logger.info { "Ussd::Pagination: Content exceeds page size (#{prompt.length} > #{FlowChat::Config.ussd.pagination_page_size}), initiating pagination" }
|
87
|
+
|
49
88
|
slice_end = single_option_slice_size
|
50
89
|
# Ensure we do not cut words and options off in the middle.
|
51
90
|
current_pagebreak = original_prompt[slice_end + 1].blank? ? slice_end : original_prompt[0..slice_end].rindex("\n") || original_prompt[0..slice_end].rindex(" ") || slice_end
|
91
|
+
|
92
|
+
FlowChat.logger.debug { "Ussd::Pagination: First page break at position #{current_pagebreak}" }
|
93
|
+
|
52
94
|
set_pagination_state(1, 0, current_pagebreak, original_prompt, type)
|
53
95
|
prompt = original_prompt[0..current_pagebreak] + "\n\n" + next_option
|
54
96
|
type = :prompt
|
97
|
+
|
98
|
+
# Instrument initial pagination setup
|
99
|
+
total_pages = calculate_total_pages(original_prompt)
|
100
|
+
instrument(Events::PAGINATION_TRIGGERED, {
|
101
|
+
session_id: @context["session.id"],
|
102
|
+
current_page: 1,
|
103
|
+
total_pages: total_pages,
|
104
|
+
content_length: original_prompt.length,
|
105
|
+
page_limit: FlowChat::Config.ussd.pagination_page_size,
|
106
|
+
navigation_action: "initial"
|
107
|
+
})
|
108
|
+
|
109
|
+
FlowChat.logger.debug { "Ussd::Pagination: First page prepared with #{prompt.length} characters" }
|
55
110
|
end
|
56
111
|
[type, prompt]
|
57
112
|
end
|
58
113
|
|
59
114
|
def calculate_offsets
|
60
115
|
page = current_page
|
116
|
+
|
117
|
+
FlowChat.logger.debug { "Ussd::Pagination: Calculating offsets for page #{page}" }
|
118
|
+
|
61
119
|
offset = pagination_state["offsets"][page.to_s]
|
62
120
|
if offset.present?
|
63
|
-
FlowChat
|
121
|
+
FlowChat.logger.debug { "Ussd::Pagination: Using cached offset for page #{page}" }
|
64
122
|
start = offset["start"]
|
65
123
|
finish = offset["finish"]
|
66
124
|
has_more = pagination_state["prompt"].length > finish
|
67
125
|
else
|
68
|
-
FlowChat
|
126
|
+
FlowChat.logger.debug { "Ussd::Pagination: Computing new offset for page #{page}" }
|
69
127
|
# We are guaranteed a previous offset because it was set in maybe_paginate
|
70
128
|
previous_page = page - 1
|
71
129
|
previous_offset = pagination_state["offsets"][previous_page.to_s]
|
72
130
|
start = previous_offset["finish"] + 1
|
73
131
|
has_more, len = (pagination_state["prompt"].length > start + single_option_slice_size) ? [true, dual_options_slice_size] : [false, single_option_slice_size]
|
74
132
|
finish = start + len
|
133
|
+
|
75
134
|
if start > pagination_state["prompt"].length
|
76
|
-
FlowChat
|
135
|
+
FlowChat.logger.warn { "Ussd::Pagination: No content for page #{page}, reverting to page #{page - 1}" }
|
77
136
|
page -= 1
|
78
137
|
has_more = false
|
79
138
|
start = previous_offset["start"]
|
@@ -90,19 +149,26 @@ module FlowChat
|
|
90
149
|
# We're in the middle of a word, find the last word boundary
|
91
150
|
boundary_pos = slice_text.rindex("\n") || slice_text.rindex(" ")
|
92
151
|
if boundary_pos
|
152
|
+
old_finish = finish
|
93
153
|
finish = start + boundary_pos
|
154
|
+
FlowChat.logger.debug { "Ussd::Pagination: Adjusted finish for word boundary - #{old_finish} -> #{finish}" }
|
94
155
|
end
|
95
156
|
# If no boundary found, we'll have to break mid-word (fallback)
|
96
157
|
end
|
97
158
|
end
|
98
159
|
end
|
99
160
|
end
|
161
|
+
|
162
|
+
FlowChat.logger.debug { "Ussd::Pagination: Page #{page} offsets - start: #{start}, finish: #{finish}, has_more: #{has_more}" }
|
100
163
|
[start, finish, has_more]
|
101
164
|
end
|
102
165
|
|
103
166
|
def build_pagination_options(type, has_more)
|
104
167
|
options_str = ""
|
105
168
|
has_less = current_page > 1
|
169
|
+
|
170
|
+
FlowChat.logger.debug { "Ussd::Pagination: Building pagination options - type: #{type}, has_more: #{has_more}, has_less: #{has_less}" }
|
171
|
+
|
106
172
|
if type.to_sym == :prompt
|
107
173
|
options_str += "\n\n"
|
108
174
|
next_opt = has_more ? next_option : ""
|
@@ -126,6 +192,7 @@ module FlowChat
|
|
126
192
|
# We accomodate the 2 newlines and the longest of the options
|
127
193
|
# We subtract an additional 1 to normalize it for slicing
|
128
194
|
@single_option_slice_size = FlowChat::Config.ussd.pagination_page_size - 2 - [next_option.length, back_option.length].max - 1
|
195
|
+
FlowChat.logger.debug { "Ussd::Pagination: Calculated single option slice size: #{@single_option_slice_size}" }
|
129
196
|
end
|
130
197
|
@single_option_slice_size
|
131
198
|
end
|
@@ -135,6 +202,7 @@ module FlowChat
|
|
135
202
|
# To display both back and next options
|
136
203
|
# We accomodate the 3 newlines and both of the options
|
137
204
|
@dual_options_slice_size = FlowChat::Config.ussd.pagination_page_size - 3 - [next_option.length, back_option.length].sum - 1
|
205
|
+
FlowChat.logger.debug { "Ussd::Pagination: Calculated dual options slice size: #{@dual_options_slice_size}" }
|
138
206
|
end
|
139
207
|
@dual_options_slice_size
|
140
208
|
end
|
@@ -143,8 +211,10 @@ module FlowChat
|
|
143
211
|
page = pagination_state["page"]
|
144
212
|
if @context.input == FlowChat::Config.ussd.pagination_back_option
|
145
213
|
page -= 1
|
214
|
+
FlowChat.logger.debug { "Ussd::Pagination: Moving to previous page: #{page}" }
|
146
215
|
elsif @context.input == FlowChat::Config.ussd.pagination_next_option
|
147
216
|
page += 1
|
217
|
+
FlowChat.logger.debug { "Ussd::Pagination: Moving to next page: #{page}" }
|
148
218
|
end
|
149
219
|
[page, 1].max
|
150
220
|
end
|
@@ -165,8 +235,18 @@ module FlowChat
|
|
165
235
|
"prompt" => prompt,
|
166
236
|
"type" => type.to_s
|
167
237
|
}
|
238
|
+
|
239
|
+
FlowChat.logger.debug { "Ussd::Pagination: Saving pagination state - page: #{page}, total_content: #{prompt&.length || 0} chars" }
|
168
240
|
@session.set "ussd.pagination", new_state
|
169
241
|
end
|
242
|
+
|
243
|
+
def calculate_total_pages(content = nil)
|
244
|
+
content ||= pagination_state["prompt"]
|
245
|
+
return 1 unless content&.length&.> FlowChat::Config.ussd.pagination_page_size
|
246
|
+
|
247
|
+
# Rough estimation - actual pages may vary due to word boundaries
|
248
|
+
(content.length.to_f / single_option_slice_size).ceil
|
249
|
+
end
|
170
250
|
end
|
171
251
|
end
|
172
252
|
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
module FlowChat
|
2
2
|
module Ussd
|
3
3
|
class Processor < FlowChat::BaseProcessor
|
4
|
-
def
|
5
|
-
|
6
|
-
|
4
|
+
def use_durable_sessions(cross_gateway: false)
|
5
|
+
FlowChat.logger.debug { "Ussd::Processor: Enabling durable sessions via session configuration" }
|
6
|
+
use_session_config(
|
7
|
+
identifier: :msisdn # Use MSISDN for durable sessions
|
8
|
+
)
|
7
9
|
end
|
8
10
|
|
9
11
|
protected
|
@@ -13,14 +15,24 @@ module FlowChat
|
|
13
15
|
end
|
14
16
|
|
15
17
|
def build_middleware_stack
|
18
|
+
FlowChat.logger.debug { "Ussd::Processor: Building USSD middleware stack" }
|
16
19
|
create_middleware_stack("ussd")
|
17
20
|
end
|
18
21
|
|
19
22
|
def configure_middleware_stack(builder)
|
20
|
-
|
23
|
+
FlowChat.logger.debug { "Ussd::Processor: Configuring USSD middleware stack" }
|
24
|
+
|
21
25
|
builder.use FlowChat::Ussd::Middleware::Pagination
|
26
|
+
FlowChat.logger.debug { "Ussd::Processor: Added Ussd::Middleware::Pagination" }
|
27
|
+
|
22
28
|
builder.use middleware
|
29
|
+
FlowChat.logger.debug { "Ussd::Processor: Added custom middleware" }
|
30
|
+
|
31
|
+
builder.use FlowChat::Ussd::Middleware::ChoiceMapper
|
32
|
+
FlowChat.logger.debug { "Ussd::Processor: Added Ussd::Middleware::ChoiceMapper" }
|
33
|
+
|
23
34
|
builder.use FlowChat::Ussd::Middleware::Executor
|
35
|
+
FlowChat.logger.debug { "Ussd::Processor: Added Ussd::Middleware::Executor" }
|
24
36
|
end
|
25
37
|
end
|
26
38
|
end
|
data/lib/flow_chat/version.rb
CHANGED