flow_chat 0.3.0 → 0.4.1
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/Gemfile +1 -0
- data/README.md +642 -86
- data/examples/initializer.rb +31 -0
- data/examples/media_prompts_examples.rb +28 -0
- data/examples/multi_tenant_whatsapp_controller.rb +244 -0
- data/examples/ussd_controller.rb +264 -0
- data/examples/whatsapp_controller.rb +140 -0
- data/examples/whatsapp_media_examples.rb +406 -0
- data/examples/whatsapp_message_job.rb +111 -0
- data/lib/flow_chat/base_processor.rb +67 -0
- data/lib/flow_chat/config.rb +36 -0
- data/lib/flow_chat/session/cache_session_store.rb +84 -0
- data/lib/flow_chat/session/middleware.rb +14 -6
- data/lib/flow_chat/simulator/controller.rb +78 -0
- data/lib/flow_chat/simulator/views/simulator.html.erb +1707 -0
- data/lib/flow_chat/ussd/app.rb +25 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +2 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +6 -0
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +1 -1
- data/lib/flow_chat/ussd/processor.rb +14 -42
- data/lib/flow_chat/ussd/prompt.rb +39 -5
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +64 -0
- data/lib/flow_chat/whatsapp/client.rb +439 -0
- data/lib/flow_chat/whatsapp/configuration.rb +113 -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 +26 -0
- data/lib/flow_chat/whatsapp/prompt.rb +251 -0
- data/lib/flow_chat/whatsapp/send_job_support.rb +79 -0
- data/lib/flow_chat/whatsapp/template_manager.rb +162 -0
- data/lib/flow_chat.rb +1 -0
- metadata +21 -3
- data/lib/flow_chat/ussd/simulator/controller.rb +0 -51
- data/lib/flow_chat/ussd/simulator/views/simulator.html.erb +0 -239
@@ -0,0 +1,67 @@
|
|
1
|
+
require "middleware"
|
2
|
+
|
3
|
+
module FlowChat
|
4
|
+
class BaseProcessor
|
5
|
+
attr_reader :middleware
|
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_class, *args)
|
16
|
+
@gateway_class = gateway_class
|
17
|
+
@gateway_args = args
|
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
|
+
|
31
|
+
def run(flow_class, action)
|
32
|
+
@context["flow.name"] = flow_class.name.underscore
|
33
|
+
@context["flow.class"] = flow_class
|
34
|
+
@context["flow.action"] = action
|
35
|
+
|
36
|
+
stack = build_middleware_stack
|
37
|
+
yield stack if block_given?
|
38
|
+
|
39
|
+
stack.call(@context)
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
# Subclasses should override these methods
|
45
|
+
def middleware_name
|
46
|
+
raise NotImplementedError, "Subclasses must implement middleware_name"
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_middleware_stack
|
50
|
+
raise NotImplementedError, "Subclasses must implement build_middleware_stack"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Helper method for building stacks
|
54
|
+
def create_middleware_stack(name)
|
55
|
+
raise ArgumentError, "Gateway is required. Call use_gateway(gateway_class, *args) before running." unless @gateway_class
|
56
|
+
|
57
|
+
::Middleware::Builder.new(name: name) do |b|
|
58
|
+
b.use @gateway_class, *@gateway_args
|
59
|
+
configure_middleware_stack(b)
|
60
|
+
end.inject_logger(Rails.logger)
|
61
|
+
end
|
62
|
+
|
63
|
+
def configure_middleware_stack(builder)
|
64
|
+
raise NotImplementedError, "Subclasses must implement configure_middleware_stack"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/flow_chat/config.rb
CHANGED
@@ -9,6 +9,11 @@ module FlowChat
|
|
9
9
|
@ussd ||= UssdConfig.new
|
10
10
|
end
|
11
11
|
|
12
|
+
# WhatsApp-specific configuration object
|
13
|
+
def self.whatsapp
|
14
|
+
@whatsapp ||= WhatsappConfig.new
|
15
|
+
end
|
16
|
+
|
12
17
|
class UssdConfig
|
13
18
|
attr_accessor :pagination_page_size, :pagination_back_option, :pagination_back_text,
|
14
19
|
:pagination_next_option, :pagination_next_text,
|
@@ -25,5 +30,36 @@ module FlowChat
|
|
25
30
|
@resumable_sessions_timeout_seconds = 300
|
26
31
|
end
|
27
32
|
end
|
33
|
+
|
34
|
+
class WhatsappConfig
|
35
|
+
attr_accessor :message_handling_mode, :background_job_class
|
36
|
+
|
37
|
+
def initialize
|
38
|
+
@message_handling_mode = :inline
|
39
|
+
@background_job_class = 'WhatsappMessageJob'
|
40
|
+
end
|
41
|
+
|
42
|
+
# Validate message handling mode
|
43
|
+
def message_handling_mode=(mode)
|
44
|
+
valid_modes = [:inline, :background, :simulator]
|
45
|
+
unless valid_modes.include?(mode.to_sym)
|
46
|
+
raise ArgumentError, "Invalid message handling mode: #{mode}. Valid modes: #{valid_modes.join(', ')}"
|
47
|
+
end
|
48
|
+
@message_handling_mode = mode.to_sym
|
49
|
+
end
|
50
|
+
|
51
|
+
# Helper methods for mode checking
|
52
|
+
def inline_mode?
|
53
|
+
@message_handling_mode == :inline
|
54
|
+
end
|
55
|
+
|
56
|
+
def background_mode?
|
57
|
+
@message_handling_mode == :background
|
58
|
+
end
|
59
|
+
|
60
|
+
def simulator_mode?
|
61
|
+
@message_handling_mode == :simulator
|
62
|
+
end
|
63
|
+
end
|
28
64
|
end
|
29
65
|
end
|
@@ -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
|
@@ -14,12 +14,20 @@ module FlowChat
|
|
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
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module FlowChat
|
2
|
+
module Simulator
|
3
|
+
module Controller
|
4
|
+
def flowchat_simulator
|
5
|
+
respond_to do |format|
|
6
|
+
format.html do
|
7
|
+
render inline: simulator_view_template, layout: false, locals: simulator_locals
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def default_phone_number
|
15
|
+
"+233244123456"
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_contact_name
|
19
|
+
"John Doe"
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_config_key
|
23
|
+
"ussd"
|
24
|
+
end
|
25
|
+
|
26
|
+
def simulator_configurations
|
27
|
+
{
|
28
|
+
"ussd" => {
|
29
|
+
name: "USSD (Nalo)",
|
30
|
+
description: "Local development USSD testing",
|
31
|
+
processor_type: "ussd",
|
32
|
+
provider: "nalo",
|
33
|
+
endpoint: "/ussd",
|
34
|
+
icon: "📱",
|
35
|
+
color: "#28a745",
|
36
|
+
settings: {
|
37
|
+
phone_number: default_phone_number,
|
38
|
+
session_timeout: 300
|
39
|
+
}
|
40
|
+
},
|
41
|
+
"whatsapp" => {
|
42
|
+
name: "WhatsApp",
|
43
|
+
description: "Local development WhatsApp testing",
|
44
|
+
processor_type: "whatsapp",
|
45
|
+
provider: "cloud_api",
|
46
|
+
endpoint: "/whatsapp/webhook",
|
47
|
+
icon: "💬",
|
48
|
+
color: "#25D366",
|
49
|
+
settings: {
|
50
|
+
phone_number: default_phone_number,
|
51
|
+
contact_name: default_contact_name,
|
52
|
+
verify_token: "local_verify_token",
|
53
|
+
webhook_url: "http://localhost:3000/whatsapp/webhook"
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
def simulator_view_template
|
60
|
+
File.read simulator_view_path
|
61
|
+
end
|
62
|
+
|
63
|
+
def simulator_view_path
|
64
|
+
File.join FlowChat.root.join("flow_chat", "simulator", "views", "simulator.html.erb")
|
65
|
+
end
|
66
|
+
|
67
|
+
def simulator_locals
|
68
|
+
{
|
69
|
+
pagesize: FlowChat::Config.ussd.pagination_page_size,
|
70
|
+
default_phone_number: default_phone_number,
|
71
|
+
default_contact_name: default_contact_name,
|
72
|
+
default_config_key: default_config_key,
|
73
|
+
configurations: simulator_configurations
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|