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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/README.md +642 -86
  4. data/examples/initializer.rb +31 -0
  5. data/examples/media_prompts_examples.rb +28 -0
  6. data/examples/multi_tenant_whatsapp_controller.rb +244 -0
  7. data/examples/ussd_controller.rb +264 -0
  8. data/examples/whatsapp_controller.rb +140 -0
  9. data/examples/whatsapp_media_examples.rb +406 -0
  10. data/examples/whatsapp_message_job.rb +111 -0
  11. data/lib/flow_chat/base_processor.rb +67 -0
  12. data/lib/flow_chat/config.rb +36 -0
  13. data/lib/flow_chat/session/cache_session_store.rb +84 -0
  14. data/lib/flow_chat/session/middleware.rb +14 -6
  15. data/lib/flow_chat/simulator/controller.rb +78 -0
  16. data/lib/flow_chat/simulator/views/simulator.html.erb +1707 -0
  17. data/lib/flow_chat/ussd/app.rb +25 -0
  18. data/lib/flow_chat/ussd/gateway/nalo.rb +2 -0
  19. data/lib/flow_chat/ussd/gateway/nsano.rb +6 -0
  20. data/lib/flow_chat/ussd/middleware/resumable_session.rb +1 -1
  21. data/lib/flow_chat/ussd/processor.rb +14 -42
  22. data/lib/flow_chat/ussd/prompt.rb +39 -5
  23. data/lib/flow_chat/version.rb +1 -1
  24. data/lib/flow_chat/whatsapp/app.rb +64 -0
  25. data/lib/flow_chat/whatsapp/client.rb +439 -0
  26. data/lib/flow_chat/whatsapp/configuration.rb +113 -0
  27. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +213 -0
  28. data/lib/flow_chat/whatsapp/middleware/executor.rb +30 -0
  29. data/lib/flow_chat/whatsapp/processor.rb +26 -0
  30. data/lib/flow_chat/whatsapp/prompt.rb +251 -0
  31. data/lib/flow_chat/whatsapp/send_job_support.rb +79 -0
  32. data/lib/flow_chat/whatsapp/template_manager.rb +162 -0
  33. data/lib/flow_chat.rb +1 -0
  34. metadata +21 -3
  35. data/lib/flow_chat/ussd/simulator/controller.rb +0 -51
  36. 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
@@ -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.id"]
18
- # File.join(
19
- # context["PATH_INFO"],
20
- # (Config.resumable_sessions_enabled && Config.resumable_sessions_global) ? "global" : context["ussd.request"][:provider].to_s,
21
- # context["ussd.request"][:msisdn]
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