flow_chat 0.8.0 → 0.8.2
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/docs/configuration.md +2 -2
- data/docs/http-gateway-protocol.md +432 -0
- data/docs/sessions.md +7 -7
- data/docs/ussd-setup.md +1 -1
- data/examples/http_controller.rb +154 -0
- data/examples/simulator_controller.rb +21 -1
- data/examples/ussd_controller.rb +1 -1
- data/lib/flow_chat/base_app.rb +86 -0
- data/lib/flow_chat/base_executor.rb +57 -0
- data/lib/flow_chat/base_processor.rb +7 -6
- data/lib/flow_chat/config.rb +17 -2
- data/lib/flow_chat/http/app.rb +6 -0
- data/lib/flow_chat/http/gateway/simple.rb +77 -0
- data/lib/flow_chat/http/middleware/executor.rb +24 -0
- data/lib/flow_chat/http/processor.rb +33 -0
- data/lib/flow_chat/http/renderer.rb +41 -0
- data/lib/flow_chat/instrumentation/setup.rb +0 -2
- data/lib/flow_chat/instrumentation.rb +2 -0
- data/lib/flow_chat/interrupt.rb +6 -0
- data/lib/flow_chat/phone_number_util.rb +47 -0
- data/lib/flow_chat/session/cache_session_store.rb +1 -17
- data/lib/flow_chat/session/middleware.rb +19 -18
- data/lib/flow_chat/simulator/controller.rb +17 -5
- data/lib/flow_chat/simulator/views/simulator.html.erb +220 -8
- data/lib/flow_chat/ussd/app.rb +1 -53
- data/lib/flow_chat/ussd/gateway/nalo.rb +3 -7
- data/lib/flow_chat/ussd/gateway/nsano.rb +0 -2
- data/lib/flow_chat/ussd/middleware/executor.rb +11 -37
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +11 -46
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +16 -14
- data/lib/flow_chat/whatsapp/middleware/executor.rb +11 -39
- data/lib/flow_chat.rb +1 -11
- metadata +12 -2
@@ -31,6 +31,15 @@ class SimulatorController < ApplicationController
|
|
31
31
|
icon: "💬",
|
32
32
|
color: "#25D366"
|
33
33
|
},
|
34
|
+
http_main: {
|
35
|
+
name: "Main HTTP API",
|
36
|
+
description: "JSON HTTP API endpoint",
|
37
|
+
processor_type: "http",
|
38
|
+
gateway: "http_simple",
|
39
|
+
endpoint: "/http/webhook",
|
40
|
+
icon: "🌐",
|
41
|
+
color: "#0066cc"
|
42
|
+
},
|
34
43
|
whatsapp_tenant_a: {
|
35
44
|
name: "Tenant A WhatsApp",
|
36
45
|
description: "Multi-tenant endpoint for Tenant A",
|
@@ -40,6 +49,15 @@ class SimulatorController < ApplicationController
|
|
40
49
|
icon: "🏢",
|
41
50
|
color: "#fd7e14"
|
42
51
|
},
|
52
|
+
http_external: {
|
53
|
+
name: "External HTTP Test",
|
54
|
+
description: "Test with external HTTP server",
|
55
|
+
processor_type: "http",
|
56
|
+
gateway: "http_simple",
|
57
|
+
endpoint: "http://localhost:4567/http/webhook",
|
58
|
+
icon: "🔗",
|
59
|
+
color: "#17a2b8"
|
60
|
+
},
|
43
61
|
whatsapp_legacy: {
|
44
62
|
name: "Legacy WhatsApp",
|
45
63
|
description: "Legacy endpoint for compatibility",
|
@@ -54,7 +72,7 @@ class SimulatorController < ApplicationController
|
|
54
72
|
|
55
73
|
# Default configuration to start with
|
56
74
|
def default_config_key
|
57
|
-
:
|
75
|
+
:http_main
|
58
76
|
end
|
59
77
|
|
60
78
|
# Default test phone number
|
@@ -79,8 +97,10 @@ end
|
|
79
97
|
# 5. View request/response logs in real-time
|
80
98
|
|
81
99
|
# This allows you to test:
|
100
|
+
# - Different protocol types (USSD, WhatsApp, HTTP)
|
82
101
|
# - Different controller implementations on the same server
|
83
102
|
# - Different API versions (v1, v2, etc.)
|
84
103
|
# - Multi-tenant endpoints with different configurations
|
85
104
|
# - Legacy endpoints alongside new ones
|
105
|
+
# - External HTTP servers (run examples/http_simulator_test.rb)
|
86
106
|
# - Different flow implementations for different endpoints
|
data/examples/ussd_controller.rb
CHANGED
@@ -237,7 +237,7 @@ class UssdController < ApplicationController
|
|
237
237
|
# Or configure session boundaries explicitly:
|
238
238
|
# config.use_session_config(
|
239
239
|
# boundaries: [:flow, :platform], # which boundaries to enforce
|
240
|
-
#
|
240
|
+
# hash_identifiers: true # hash phone numbers for privacy
|
241
241
|
# )
|
242
242
|
end
|
243
243
|
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module FlowChat
|
2
|
+
class BaseApp
|
3
|
+
attr_reader :session, :input, :context, :navigation_stack
|
4
|
+
|
5
|
+
def initialize(context)
|
6
|
+
@context = context
|
7
|
+
@session = context.session
|
8
|
+
@input = context.input
|
9
|
+
@navigation_stack = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def screen(key)
|
13
|
+
raise ArgumentError, "a block is expected" unless block_given?
|
14
|
+
raise ArgumentError, "screen has already been presented" if navigation_stack.include?(key)
|
15
|
+
|
16
|
+
navigation_stack << key
|
17
|
+
return session.get(key) if session.get(key).present?
|
18
|
+
|
19
|
+
user_input = prepare_user_input
|
20
|
+
prompt = FlowChat::Prompt.new user_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 go_back
|
29
|
+
return false if navigation_stack.empty?
|
30
|
+
|
31
|
+
@context.input = nil
|
32
|
+
current_screen = navigation_stack.last
|
33
|
+
session.delete(current_screen)
|
34
|
+
|
35
|
+
# Restart the flow from the beginning
|
36
|
+
raise FlowChat::Interrupt::RestartFlow.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def say(msg, media: nil)
|
40
|
+
raise FlowChat::Interrupt::Terminate.new(msg, media: media)
|
41
|
+
end
|
42
|
+
|
43
|
+
def platform
|
44
|
+
context["request.platform"]
|
45
|
+
end
|
46
|
+
|
47
|
+
def gateway
|
48
|
+
context["request.gateway"]
|
49
|
+
end
|
50
|
+
|
51
|
+
def user_id
|
52
|
+
context["request.user_id"]
|
53
|
+
end
|
54
|
+
|
55
|
+
def msisdn
|
56
|
+
context["request.msisdn"]
|
57
|
+
end
|
58
|
+
|
59
|
+
def message_id
|
60
|
+
context["request.message_id"]
|
61
|
+
end
|
62
|
+
|
63
|
+
def timestamp
|
64
|
+
context["request.timestamp"]
|
65
|
+
end
|
66
|
+
|
67
|
+
def contact_name
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def location
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
|
75
|
+
def media
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
|
81
|
+
# Platform-specific methods to be overridden
|
82
|
+
def prepare_user_input
|
83
|
+
input
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module FlowChat
|
2
|
+
class BaseExecutor
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
FlowChat.logger.debug { "#{log_prefix}: Initialized #{platform_name} executor middleware" }
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(context)
|
9
|
+
flow_class = context.flow
|
10
|
+
action = context["flow.action"]
|
11
|
+
session_id = context["session.id"]
|
12
|
+
|
13
|
+
FlowChat.logger.info { "#{log_prefix}: Executing flow #{flow_class.name}##{action} for session #{session_id}" }
|
14
|
+
|
15
|
+
platform_app = build_platform_app(context)
|
16
|
+
FlowChat.logger.debug { "#{log_prefix}: #{platform_name} app built for flow execution" }
|
17
|
+
|
18
|
+
flow = flow_class.new platform_app
|
19
|
+
FlowChat.logger.debug { "#{log_prefix}: Flow instance created, invoking #{action} method" }
|
20
|
+
|
21
|
+
flow.send action
|
22
|
+
FlowChat.logger.warn { "#{log_prefix}: Flow execution failed to interact with user for #{flow_class.name}##{action}" }
|
23
|
+
raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
|
24
|
+
rescue FlowChat::Interrupt::RestartFlow => e
|
25
|
+
FlowChat.logger.info { "#{log_prefix}: Flow restart requested - Session: #{session_id}, restarting #{action}" }
|
26
|
+
retry
|
27
|
+
rescue FlowChat::Interrupt::Prompt => e
|
28
|
+
FlowChat.logger.info { "#{log_prefix}: Flow prompted user - Session: #{session_id}, Prompt: '#{e.prompt&.truncate(100)}'" }
|
29
|
+
FlowChat.logger.debug { "#{log_prefix}: Prompt details - Choices: #{e.choices&.size || 0}, Has media: #{!e.media.nil?}" }
|
30
|
+
[:prompt, e.prompt, e.choices, e.media]
|
31
|
+
rescue FlowChat::Interrupt::Terminate => e
|
32
|
+
FlowChat.logger.info { "#{log_prefix}: Flow terminated - Session: #{session_id}, Message: '#{e.prompt&.truncate(100)}'" }
|
33
|
+
FlowChat.logger.debug { "#{log_prefix}: Destroying session #{session_id}" }
|
34
|
+
context.session.destroy
|
35
|
+
[:terminal, e.prompt, nil, e.media]
|
36
|
+
rescue => error
|
37
|
+
FlowChat.logger.error { "#{log_prefix}: Flow execution failed - #{flow_class.name}##{action}, Session: #{session_id}, Error: #{error.class.name}: #{error.message}" }
|
38
|
+
FlowChat.logger.debug { "#{log_prefix}: Stack trace: #{error.backtrace.join("\n")}" }
|
39
|
+
raise
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
# Subclasses must implement these methods
|
45
|
+
def platform_name
|
46
|
+
raise NotImplementedError, "Subclasses must implement platform_name"
|
47
|
+
end
|
48
|
+
|
49
|
+
def log_prefix
|
50
|
+
raise NotImplementedError, "Subclasses must implement log_prefix"
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_platform_app(context)
|
54
|
+
raise NotImplementedError, "Subclasses must implement build_platform_app"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -36,14 +36,14 @@ module FlowChat
|
|
36
36
|
self
|
37
37
|
end
|
38
38
|
|
39
|
-
def use_session_config(boundaries: nil,
|
40
|
-
FlowChat.logger.debug { "BaseProcessor: Configuring session config: boundaries=#{boundaries.inspect},
|
39
|
+
def use_session_config(boundaries: nil, hash_identifiers: nil, identifier: nil)
|
40
|
+
FlowChat.logger.debug { "BaseProcessor: Configuring session config: boundaries=#{boundaries.inspect}, hash_identifiers=#{hash_identifiers}, identifier=#{identifier}" }
|
41
41
|
|
42
42
|
# Update the session options directly
|
43
43
|
@session_options = @session_options.dup
|
44
|
-
@session_options.boundaries = Array(boundaries) if boundaries
|
45
|
-
@session_options.
|
46
|
-
@session_options.identifier = identifier if identifier
|
44
|
+
@session_options.boundaries = Array(boundaries) if boundaries != nil
|
45
|
+
@session_options.hash_identifiers = hash_identifiers if hash_identifiers != nil
|
46
|
+
@session_options.identifier = identifier if identifier != nil
|
47
47
|
|
48
48
|
self
|
49
49
|
end
|
@@ -70,7 +70,7 @@ module FlowChat
|
|
70
70
|
use_session_config(boundaries: current_boundaries)
|
71
71
|
end
|
72
72
|
|
73
|
-
def run(flow_class, action)
|
73
|
+
def run(flow_class, action, **options)
|
74
74
|
# Instrument flow execution (this will log via LogSubscriber)
|
75
75
|
instrument(Events::FLOW_EXECUTION_START, {
|
76
76
|
flow_name: flow_class.name.underscore,
|
@@ -81,6 +81,7 @@ module FlowChat
|
|
81
81
|
@context["flow.name"] = flow_class.name.underscore
|
82
82
|
@context["flow.class"] = flow_class
|
83
83
|
@context["flow.action"] = action
|
84
|
+
@context["flow.options"] = options
|
84
85
|
|
85
86
|
FlowChat.logger.debug { "BaseProcessor: Context prepared for flow #{flow_class.name}" }
|
86
87
|
|
data/lib/flow_chat/config.rb
CHANGED
@@ -23,8 +23,13 @@ module FlowChat
|
|
23
23
|
@whatsapp ||= WhatsappConfig.new
|
24
24
|
end
|
25
25
|
|
26
|
+
# HTTP-specific configuration object
|
27
|
+
def self.http
|
28
|
+
@http ||= HttpConfig.new
|
29
|
+
end
|
30
|
+
|
26
31
|
class SessionConfig
|
27
|
-
attr_accessor :boundaries, :
|
32
|
+
attr_accessor :boundaries, :hash_identifiers, :identifier
|
28
33
|
|
29
34
|
def initialize
|
30
35
|
# Session boundaries control how session IDs are constructed
|
@@ -34,7 +39,7 @@ module FlowChat
|
|
34
39
|
@boundaries = [:flow, :gateway, :platform]
|
35
40
|
|
36
41
|
# Always hash phone numbers for privacy
|
37
|
-
@
|
42
|
+
@hash_identifiers = true
|
38
43
|
|
39
44
|
# Session identifier type (nil = let platforms choose their default)
|
40
45
|
# :msisdn = durable sessions (durable across timeouts)
|
@@ -88,6 +93,16 @@ module FlowChat
|
|
88
93
|
@message_handling_mode == :simulator
|
89
94
|
end
|
90
95
|
end
|
96
|
+
|
97
|
+
class HttpConfig
|
98
|
+
attr_accessor :default_gateway, :request_timeout, :response_format
|
99
|
+
|
100
|
+
def initialize
|
101
|
+
@default_gateway = :simple
|
102
|
+
@request_timeout = 30
|
103
|
+
@response_format = :json
|
104
|
+
end
|
105
|
+
end
|
91
106
|
end
|
92
107
|
|
93
108
|
# Shorthand for accessing the logger throughout the application
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Http
|
3
|
+
module Gateway
|
4
|
+
class Simple
|
5
|
+
include FlowChat::Instrumentation
|
6
|
+
|
7
|
+
attr_reader :context
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(context)
|
14
|
+
@context = context
|
15
|
+
params = context.controller.request.params
|
16
|
+
request = context.controller.request
|
17
|
+
|
18
|
+
# Extract basic request information
|
19
|
+
context["request.id"] = params["session_id"] || SecureRandom.uuid
|
20
|
+
context["request.msisdn"] = FlowChat::PhoneNumberUtil.to_e164(params["msisdn"])
|
21
|
+
context["request.user_id"] = params["user_id"] || context["request.msisdn"] || context["request.id"]
|
22
|
+
context["request.message_id"] = params["message_id"] || SecureRandom.uuid
|
23
|
+
context["request.timestamp"] = Time.current.iso8601
|
24
|
+
context["request.gateway"] = :http_simple
|
25
|
+
context["request.platform"] = :http
|
26
|
+
context["request.network"] = nil
|
27
|
+
context["request.method"] = request.method
|
28
|
+
context["request.path"] = request.path
|
29
|
+
context["request.user_agent"] = request.user_agent
|
30
|
+
context.input = params["input"] || params["message"]
|
31
|
+
|
32
|
+
# Instrument message received when user provides input
|
33
|
+
if context.input.present?
|
34
|
+
instrument(Events::MESSAGE_RECEIVED, {
|
35
|
+
from: context["request.user_id"],
|
36
|
+
message: context.input,
|
37
|
+
timestamp: context["request.timestamp"]
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
41
|
+
# Process the request
|
42
|
+
type, prompt, choices, media = @app.call(context)
|
43
|
+
|
44
|
+
# Instrument message sent
|
45
|
+
instrument(Events::MESSAGE_SENT, {
|
46
|
+
to: context["request.user_id"],
|
47
|
+
session_id: context["request.id"],
|
48
|
+
message: context.input || "",
|
49
|
+
message_type: (type == :prompt) ? "prompt" : "terminal",
|
50
|
+
gateway: :http_simple,
|
51
|
+
platform: :http,
|
52
|
+
content_length: prompt.to_s.length,
|
53
|
+
timestamp: context["request.timestamp"]
|
54
|
+
})
|
55
|
+
|
56
|
+
# Render response as JSON
|
57
|
+
response_data = render_response(type, prompt, choices, media)
|
58
|
+
context.controller.render json: response_data
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def render_response(type, prompt, choices, media)
|
64
|
+
rendered = FlowChat::Http::Renderer.new(prompt, choices: choices, media: media).render
|
65
|
+
|
66
|
+
{
|
67
|
+
type: type,
|
68
|
+
session_id: context["request.id"],
|
69
|
+
user_id: context["request.user_id"],
|
70
|
+
timestamp: context["request.timestamp"],
|
71
|
+
**rendered
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative "../../base_executor"
|
2
|
+
|
3
|
+
module FlowChat
|
4
|
+
module Http
|
5
|
+
module Middleware
|
6
|
+
class Executor < FlowChat::BaseExecutor
|
7
|
+
protected
|
8
|
+
|
9
|
+
def platform_name
|
10
|
+
"HTTP"
|
11
|
+
end
|
12
|
+
|
13
|
+
def log_prefix
|
14
|
+
"Http::Executor"
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_platform_app(context)
|
18
|
+
FlowChat.logger.debug { "#{log_prefix}: Building HTTP app instance" }
|
19
|
+
FlowChat::Http::App.new(context)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Http
|
3
|
+
class Processor < FlowChat::BaseProcessor
|
4
|
+
def use_durable_sessions(cross_gateway: false)
|
5
|
+
FlowChat.logger.debug { "Http::Processor: Enabling durable sessions via session configuration" }
|
6
|
+
use_session_config(
|
7
|
+
identifier: :user_id
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def middleware_name
|
14
|
+
"http.middleware"
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_middleware_stack
|
18
|
+
FlowChat.logger.debug { "Http::Processor: Building HTTP middleware stack" }
|
19
|
+
create_middleware_stack("http")
|
20
|
+
end
|
21
|
+
|
22
|
+
def configure_middleware_stack(builder)
|
23
|
+
FlowChat.logger.debug { "Http::Processor: Configuring HTTP middleware stack" }
|
24
|
+
|
25
|
+
builder.use middleware
|
26
|
+
FlowChat.logger.debug { "Http::Processor: Added custom middleware" }
|
27
|
+
|
28
|
+
builder.use FlowChat::Http::Middleware::Executor
|
29
|
+
FlowChat.logger.debug { "Http::Processor: Added Http::Middleware::Executor" }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Http
|
3
|
+
class Renderer
|
4
|
+
attr_reader :prompt, :choices, :media
|
5
|
+
|
6
|
+
def initialize(prompt, choices: nil, media: nil)
|
7
|
+
@prompt = prompt
|
8
|
+
@choices = choices
|
9
|
+
@media = media
|
10
|
+
end
|
11
|
+
|
12
|
+
def render = build_response
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_response
|
17
|
+
{
|
18
|
+
message: prompt,
|
19
|
+
choices: format_choices,
|
20
|
+
media: format_media
|
21
|
+
}.compact
|
22
|
+
end
|
23
|
+
|
24
|
+
def format_choices
|
25
|
+
return unless choices.present?
|
26
|
+
|
27
|
+
choices.map { |key, value| { key: key, value: value } }
|
28
|
+
end
|
29
|
+
|
30
|
+
def format_media
|
31
|
+
return unless media.present?
|
32
|
+
|
33
|
+
{
|
34
|
+
url: media[:url] || media[:path],
|
35
|
+
type: media[:type] || :image,
|
36
|
+
caption: media[:caption]
|
37
|
+
}.compact
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -22,7 +22,6 @@ module FlowChat
|
|
22
22
|
def setup_logging!(options = {})
|
23
23
|
return if @log_subscriber_setup
|
24
24
|
|
25
|
-
require_relative "log_subscriber"
|
26
25
|
setup_log_subscriber(options)
|
27
26
|
@log_subscriber_setup = true
|
28
27
|
end
|
@@ -31,7 +30,6 @@ module FlowChat
|
|
31
30
|
def setup_metrics!(options = {})
|
32
31
|
return if @metrics_collector_setup
|
33
32
|
|
34
|
-
require_relative "metrics_collector"
|
35
33
|
setup_metrics_collector(options)
|
36
34
|
@metrics_collector_setup = true
|
37
35
|
end
|
@@ -8,9 +8,11 @@ module FlowChat
|
|
8
8
|
def instrument(event_name, payload = {}, &block)
|
9
9
|
enriched_payload = payload&.dup || {}
|
10
10
|
if respond_to?(:context) && context
|
11
|
+
enriched_payload[:request_id] = context["request.id"] if context["request.id"]
|
11
12
|
enriched_payload[:session_id] = context["session.id"] if context["session.id"]
|
12
13
|
enriched_payload[:flow_name] = context["flow.name"] if context["flow.name"]
|
13
14
|
enriched_payload[:gateway] = context["request.gateway"] if context["request.gateway"]
|
15
|
+
enriched_payload[:platform] = context["request.platform"] if context["request.platform"]
|
14
16
|
end
|
15
17
|
|
16
18
|
self.class.instrument(event_name, enriched_payload, &block)
|
data/lib/flow_chat/interrupt.rb
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module PhoneNumberUtil
|
3
|
+
def self.to_e164(phone_number)
|
4
|
+
return phone_number if phone_number.nil? || phone_number.empty?
|
5
|
+
|
6
|
+
begin
|
7
|
+
# Try to load phonelib without Rails dependency
|
8
|
+
require_phonelib_safely
|
9
|
+
Phonelib.parse(phone_number).e164
|
10
|
+
rescue => e
|
11
|
+
FlowChat.logger.warn { "PhoneNumberUtil: Failed to parse phone number '#{phone_number}': #{e.message}" }
|
12
|
+
# Fallback to simple formatting if phonelib fails
|
13
|
+
fallback_e164_format(phone_number)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def self.require_phonelib_safely
|
20
|
+
return if defined?(Phonelib)
|
21
|
+
|
22
|
+
# Temporarily stub Rails if it doesn't exist
|
23
|
+
unless defined?(Rails)
|
24
|
+
stub_rails = Module.new do
|
25
|
+
def self.const_missing(name)
|
26
|
+
if name == :Railtie
|
27
|
+
Class.new
|
28
|
+
else
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
Object.const_set(:Rails, stub_rails)
|
34
|
+
require "phonelib"
|
35
|
+
Object.send(:remove_const, :Rails)
|
36
|
+
else
|
37
|
+
require "phonelib"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.fallback_e164_format(phone_number)
|
42
|
+
# Simple fallback - ensure it starts with + and looks like a phone number
|
43
|
+
cleaned = phone_number.to_s.gsub(/[^\d+]/, '')
|
44
|
+
cleaned.start_with?('+') ? cleaned : "+#{cleaned}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -111,23 +111,7 @@ module FlowChat
|
|
111
111
|
private
|
112
112
|
|
113
113
|
def session_key
|
114
|
-
|
115
|
-
|
116
|
-
gateway = @context["request.gateway"]
|
117
|
-
msisdn = @context["request.msisdn"]
|
118
|
-
|
119
|
-
key = case gateway
|
120
|
-
when :whatsapp_cloud_api
|
121
|
-
"flow_chat:session:whatsapp:#{msisdn}"
|
122
|
-
when :nalo, :nsano
|
123
|
-
session_id = @context["request.id"]
|
124
|
-
"flow_chat:session:ussd:#{session_id}:#{msisdn}"
|
125
|
-
else
|
126
|
-
"flow_chat:session:unknown:#{msisdn}"
|
127
|
-
end
|
128
|
-
|
129
|
-
FlowChat.logger.debug { "CacheSessionStore: Generated session key: #{key}" }
|
130
|
-
key
|
114
|
+
"flow_chat:cached_session:#{@context["session.id"]}"
|
131
115
|
end
|
132
116
|
|
133
117
|
def session_ttl
|
@@ -64,32 +64,33 @@ module FlowChat
|
|
64
64
|
end
|
65
65
|
|
66
66
|
def get_session_identifier(context)
|
67
|
-
identifier_type = @session_options.identifier
|
68
|
-
|
69
|
-
# If no identifier specified, use platform defaults
|
70
|
-
if identifier_type.nil?
|
71
|
-
platform = context["request.platform"]
|
72
|
-
identifier_type = case platform
|
73
|
-
when :ussd
|
74
|
-
:request_id # USSD defaults to ephemeral sessions
|
75
|
-
when :whatsapp
|
76
|
-
:msisdn # WhatsApp defaults to durable sessions
|
77
|
-
else
|
78
|
-
:msisdn # Default fallback to durable
|
79
|
-
end
|
80
|
-
end
|
67
|
+
identifier_type = @session_options.identifier || platform_default_identifier(context)
|
81
68
|
|
82
69
|
case identifier_type
|
83
70
|
when :request_id
|
84
71
|
context["request.id"]
|
72
|
+
when :user_id
|
73
|
+
user_id = context["request.user_id"]
|
74
|
+
@session_options.hash_identifiers ? hash_identifier(user_id) : user_id
|
85
75
|
when :msisdn
|
86
|
-
|
87
|
-
@session_options.
|
76
|
+
msisdn = context["request.msisdn"]
|
77
|
+
@session_options.hash_identifiers ? hash_identifier(msisdn) : msisdn
|
88
78
|
else
|
89
79
|
raise "Invalid session identifier type: #{identifier_type}"
|
90
80
|
end
|
91
81
|
end
|
92
82
|
|
83
|
+
def platform_default_identifier(context)
|
84
|
+
platform = context["request.platform"]
|
85
|
+
|
86
|
+
case platform
|
87
|
+
when :whatsapp
|
88
|
+
:msisdn
|
89
|
+
else
|
90
|
+
:request_id
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
93
94
|
def build_session_id(flow_name, platform, gateway, identifier)
|
94
95
|
parts = []
|
95
96
|
|
@@ -142,10 +143,10 @@ module FlowChat
|
|
142
143
|
url_identifier
|
143
144
|
end
|
144
145
|
|
145
|
-
def
|
146
|
+
def hash_identifier(identifier)
|
146
147
|
# Use SHA256 but only take first 8 characters for reasonable session IDs
|
147
148
|
require 'digest'
|
148
|
-
Digest::SHA256.hexdigest(
|
149
|
+
Digest::SHA256.hexdigest(identifier.to_s)[0, 8]
|
149
150
|
end
|
150
151
|
end
|
151
152
|
end
|