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
@@ -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,6 +12,8 @@ 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
@@ -12,6 +12,12 @@ module FlowChat
12
12
  controller = context["controller"]
13
13
  controller.request
14
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
20
+
15
21
  # input = context["rack.input"].read
16
22
  # context["rack.input"].rewind
17
23
  # if input.present?
@@ -29,7 +29,7 @@ module FlowChat
29
29
  return true unless FlowChat::Config.ussd.resumable_sessions_timeout_seconds
30
30
 
31
31
  last_active_at = Time.parse session.dig("context", "last_active_at")
32
- (Time.now - FlowChat::Config.ussd.resumable_sessions_timeout_seconds) < last_active_at
32
+ (Time.current - FlowChat::Config.ussd.resumable_sessions_timeout_seconds) < last_active_at
33
33
  rescue
34
34
  false
35
35
  end
@@ -1,55 +1,27 @@
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
- def run(flow_class, action)
37
- @context["flow.name"] = flow_class.name.underscore
38
- @context["flow.class"] = flow_class
39
- @context["flow.action"] = action
9
+ protected
40
10
 
41
- stack = ::Middleware::Builder.new name: "ussd" do |b|
42
- b.use gateway
43
- b.use FlowChat::Session::Middleware
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
- yield stack if block_given?
15
+ def build_middleware_stack
16
+ create_middleware_stack("ussd")
17
+ end
50
18
 
51
- stack.call(@context)
19
+ def configure_middleware_stack(builder)
20
+ builder.use FlowChat::Session::Middleware
21
+ builder.use FlowChat::Ussd::Middleware::Pagination
22
+ builder.use middleware
23
+ builder.use FlowChat::Ussd::Middleware::Executor
52
24
  end
53
25
  end
54
26
  end
55
- end
27
+ end
@@ -7,23 +7,31 @@ module FlowChat
7
7
  @user_input = input
8
8
  end
9
9
 
10
- def ask(msg, choices: nil, convert: nil, validate: nil, transform: nil)
10
+ def ask(msg, choices: nil, convert: nil, validate: nil, transform: nil, media: nil)
11
11
  if user_input.present?
12
12
  input = user_input
13
13
  input = convert.call(input) if convert.present?
14
14
  validation_error = validate.call(input) if validate.present?
15
15
 
16
- prompt!([validation_error, msg].join("\n\n"), choices:) if validation_error.present?
16
+ if validation_error.present?
17
+ # Include media URL in validation error message
18
+ original_message_with_media = build_message_with_media(msg, media)
19
+ prompt!([validation_error, original_message_with_media].join("\n\n"), choices:)
20
+ end
17
21
 
18
22
  input = transform.call(input) if transform.present?
19
23
  return input
20
24
  end
21
25
 
22
- prompt! msg, choices:
26
+ # Include media URL in the message for USSD
27
+ final_message = build_message_with_media(msg, media)
28
+ prompt! final_message, choices:
23
29
  end
24
30
 
25
- def say(message)
26
- terminate! message
31
+ def say(message, media: nil)
32
+ # Include media URL in the message for USSD
33
+ final_message = build_message_with_media(message, media)
34
+ terminate! final_message
27
35
  end
28
36
 
29
37
  def select(msg, choices)
@@ -43,6 +51,32 @@ module FlowChat
43
51
 
44
52
  private
45
53
 
54
+ def build_message_with_media(message, media)
55
+ return message unless media
56
+
57
+ media_url = media[:url] || media[:path]
58
+ media_type = media[:type] || :image
59
+
60
+ # For USSD, we append the media URL to the message
61
+ media_text = case media_type.to_sym
62
+ when :image
63
+ "📷 Image: #{media_url}"
64
+ when :document
65
+ "📄 Document: #{media_url}"
66
+ when :audio
67
+ "🎵 Audio: #{media_url}"
68
+ when :video
69
+ "🎥 Video: #{media_url}"
70
+ when :sticker
71
+ "😊 Sticker: #{media_url}"
72
+ else
73
+ "📎 Media: #{media_url}"
74
+ end
75
+
76
+ # Combine message with media information
77
+ "#{message}\n\n#{media_text}"
78
+ end
79
+
46
80
  def build_select_choices(choices)
47
81
  case choices
48
82
  when Array
@@ -1,3 +1,3 @@
1
1
  module FlowChat
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.1"
3
3
  end
@@ -0,0 +1,64 @@
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
+ user_input = input
21
+ if session.get("$started_at$").nil?
22
+ session.set("$started_at$", Time.current.iso8601)
23
+ user_input = nil
24
+ end
25
+
26
+ prompt = FlowChat::Whatsapp::Prompt.new user_input
27
+ @input = nil # input is being submitted to prompt so we clear it
28
+
29
+ value = yield prompt
30
+ session.set(key, value)
31
+ value
32
+ end
33
+
34
+ def say(msg)
35
+ raise FlowChat::Interrupt::Terminate.new([:text, msg, {}])
36
+ end
37
+
38
+ # WhatsApp-specific data accessors (read-only)
39
+ def contact_name
40
+ context["request.contact_name"]
41
+ end
42
+
43
+ def message_id
44
+ context["request.message_id"]
45
+ end
46
+
47
+ def timestamp
48
+ context["request.timestamp"]
49
+ end
50
+
51
+ def location
52
+ context["request.location"]
53
+ end
54
+
55
+ def media
56
+ context["request.media"]
57
+ end
58
+
59
+ def phone_number
60
+ context["request.msisdn"]
61
+ end
62
+ end
63
+ end
64
+ end