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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/docs/configuration.md +2 -2
  3. data/docs/http-gateway-protocol.md +432 -0
  4. data/docs/sessions.md +7 -7
  5. data/docs/ussd-setup.md +1 -1
  6. data/examples/http_controller.rb +154 -0
  7. data/examples/simulator_controller.rb +21 -1
  8. data/examples/ussd_controller.rb +1 -1
  9. data/lib/flow_chat/base_app.rb +86 -0
  10. data/lib/flow_chat/base_executor.rb +57 -0
  11. data/lib/flow_chat/base_processor.rb +7 -6
  12. data/lib/flow_chat/config.rb +17 -2
  13. data/lib/flow_chat/http/app.rb +6 -0
  14. data/lib/flow_chat/http/gateway/simple.rb +77 -0
  15. data/lib/flow_chat/http/middleware/executor.rb +24 -0
  16. data/lib/flow_chat/http/processor.rb +33 -0
  17. data/lib/flow_chat/http/renderer.rb +41 -0
  18. data/lib/flow_chat/instrumentation/setup.rb +0 -2
  19. data/lib/flow_chat/instrumentation.rb +2 -0
  20. data/lib/flow_chat/interrupt.rb +6 -0
  21. data/lib/flow_chat/phone_number_util.rb +47 -0
  22. data/lib/flow_chat/session/cache_session_store.rb +1 -17
  23. data/lib/flow_chat/session/middleware.rb +19 -18
  24. data/lib/flow_chat/simulator/controller.rb +17 -5
  25. data/lib/flow_chat/simulator/views/simulator.html.erb +220 -8
  26. data/lib/flow_chat/ussd/app.rb +1 -53
  27. data/lib/flow_chat/ussd/gateway/nalo.rb +3 -7
  28. data/lib/flow_chat/ussd/gateway/nsano.rb +0 -2
  29. data/lib/flow_chat/ussd/middleware/executor.rb +11 -37
  30. data/lib/flow_chat/version.rb +1 -1
  31. data/lib/flow_chat/whatsapp/app.rb +11 -46
  32. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +16 -14
  33. data/lib/flow_chat/whatsapp/middleware/executor.rb +11 -39
  34. data/lib/flow_chat.rb +1 -11
  35. metadata +12 -2
@@ -23,12 +23,12 @@ module FlowChat
23
23
  end
24
24
 
25
25
  def default_config_key
26
- "ussd"
26
+ :ussd
27
27
  end
28
28
 
29
- def simulator_configurations
29
+ def configurations
30
30
  {
31
- "ussd" => {
31
+ ussd: {
32
32
  name: "USSD (Nalo)",
33
33
  description: "USSD integration using Nalo",
34
34
  processor_type: "ussd",
@@ -41,7 +41,7 @@ module FlowChat
41
41
  session_timeout: 300
42
42
  }
43
43
  },
44
- "whatsapp" => {
44
+ whatsapp: {
45
45
  name: "WhatsApp (Cloud API)",
46
46
  description: "WhatsApp integration using Cloud API",
47
47
  processor_type: "whatsapp",
@@ -53,6 +53,18 @@ module FlowChat
53
53
  phone_number: default_phone_number,
54
54
  contact_name: default_contact_name
55
55
  }
56
+ },
57
+ http: {
58
+ name: "HTTP API",
59
+ description: "HTTP integration with JSON request/response",
60
+ processor_type: "http",
61
+ gateway: "http_simple",
62
+ endpoint: "/http/webhook",
63
+ icon: "🌐",
64
+ color: "#0066cc",
65
+ settings: {
66
+ user_id: default_phone_number
67
+ }
56
68
  }
57
69
  }
58
70
  end
@@ -71,7 +83,7 @@ module FlowChat
71
83
  default_phone_number: default_phone_number,
72
84
  default_contact_name: default_contact_name,
73
85
  default_config_key: default_config_key,
74
- configurations: simulator_configurations
86
+ configurations: configurations
75
87
  }
76
88
  end
77
89
 
@@ -314,6 +314,15 @@
314
314
  height: 100%;
315
315
  }
316
316
 
317
+ .http-screen {
318
+ background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
319
+ flex: 1;
320
+ display: flex;
321
+ flex-direction: column;
322
+ min-height: 0;
323
+ height: 100%;
324
+ }
325
+
317
326
  .whatsapp-header {
318
327
  background: #075e54;
319
328
  color: white;
@@ -324,6 +333,16 @@
324
333
  flex-shrink: 0;
325
334
  }
326
335
 
336
+ .http-header {
337
+ background: #0066cc;
338
+ color: white;
339
+ padding: 15px 20px;
340
+ display: flex;
341
+ align-items: center;
342
+ gap: 12px;
343
+ flex-shrink: 0;
344
+ }
345
+
327
346
  .contact-avatar {
328
347
  width: 40px;
329
348
  height: 40px;
@@ -931,6 +950,20 @@
931
950
  <!-- Messages will be displayed here -->
932
951
  </div>
933
952
  </div>
953
+
954
+ <!-- HTTP Screen -->
955
+ <div id="http-screen" class="http-screen hidden">
956
+ <div class="http-header">
957
+ <div class="config-icon" style="background: #0066cc;">🌐</div>
958
+ <div class="contact-info">
959
+ <h4>HTTP API</h4>
960
+ <p>JSON Request/Response</p>
961
+ </div>
962
+ </div>
963
+ <div class="messages-area" id="http-messages-area">
964
+ <!-- HTTP messages will be displayed here -->
965
+ </div>
966
+ </div>
934
967
  </div>
935
968
  </div>
936
969
 
@@ -1000,7 +1033,9 @@
1000
1033
 
1001
1034
  ussdScreen: document.getElementById('ussd-screen'),
1002
1035
  whatsappScreen: document.getElementById('whatsapp-screen'),
1036
+ httpScreen: document.getElementById('http-screen'),
1003
1037
  messagesArea: document.getElementById('messages-area'),
1038
+ httpMessagesArea: document.getElementById('http-messages-area'),
1004
1039
  contactAvatar: document.getElementById('contact-avatar'),
1005
1040
  headerContactName: document.getElementById('header-contact-name'),
1006
1041
 
@@ -1091,17 +1126,24 @@
1091
1126
  function updateUI() {
1092
1127
  if (!state.currentConfig) return
1093
1128
 
1094
- const isWhatsApp = state.currentConfig.processor_type === 'whatsapp'
1129
+ const processorType = state.currentConfig.processor_type
1130
+ const isWhatsApp = processorType === 'whatsapp'
1131
+ const isHttp = processorType === 'http'
1095
1132
 
1096
1133
  // Show/hide platform-specific elements
1097
1134
  elements.contactNameGroup.style.display = isWhatsApp ? 'block' : 'none'
1098
- elements.ussdScreen.classList.toggle('hidden', isWhatsApp)
1135
+ elements.ussdScreen.classList.toggle('hidden', isWhatsApp || isHttp)
1099
1136
  elements.whatsappScreen.classList.toggle('hidden', !isWhatsApp)
1137
+ elements.httpScreen.classList.toggle('hidden', !isHttp)
1100
1138
 
1101
1139
  // Update input placeholder
1102
- elements.messageInput.placeholder = isWhatsApp ?
1103
- 'Type your WhatsApp message...' :
1104
- 'Enter USSD input...'
1140
+ let placeholder = 'Enter USSD input...'
1141
+ if (isWhatsApp) {
1142
+ placeholder = 'Type your WhatsApp message...'
1143
+ } else if (isHttp) {
1144
+ placeholder = 'Type your HTTP message...'
1145
+ }
1146
+ elements.messageInput.placeholder = placeholder
1105
1147
 
1106
1148
  // Update button states
1107
1149
  const canStart = state.currentConfig && !state.isRunning
@@ -1144,8 +1186,10 @@
1144
1186
 
1145
1187
  if (state.currentConfig.processor_type === 'ussd') {
1146
1188
  await makeUSSDRequest()
1147
- } else {
1189
+ } else if (state.currentConfig.processor_type === 'whatsapp') {
1148
1190
  await makeWhatsAppRequest()
1191
+ } else if (state.currentConfig.processor_type === 'http') {
1192
+ await makeHTTPRequest()
1149
1193
  }
1150
1194
 
1151
1195
  updateStatus('Connected', 'connected')
@@ -1164,9 +1208,11 @@
1164
1208
  try {
1165
1209
  updateStatus('Sending...', 'connecting')
1166
1210
 
1167
- // Add outgoing message to WhatsApp chat
1211
+ // Add outgoing message to appropriate chat
1168
1212
  if (state.currentConfig.processor_type === 'whatsapp') {
1169
1213
  addMessage(message, true)
1214
+ } else if (state.currentConfig.processor_type === 'http') {
1215
+ addHttpMessage(message, true)
1170
1216
  }
1171
1217
 
1172
1218
  elements.messageInput.value = ''
@@ -1174,8 +1220,10 @@
1174
1220
 
1175
1221
  if (state.currentConfig.processor_type === 'ussd') {
1176
1222
  await makeUSSDRequest(message)
1177
- } else {
1223
+ } else if (state.currentConfig.processor_type === 'whatsapp') {
1178
1224
  await makeWhatsAppRequest(message)
1225
+ } else if (state.currentConfig.processor_type === 'http') {
1226
+ await makeHTTPRequest(message)
1179
1227
  }
1180
1228
 
1181
1229
  updateStatus('Connected', 'connected')
@@ -1192,6 +1240,7 @@
1192
1240
  elements.messageInput.value = ''
1193
1241
  elements.ussdScreen.textContent = ''
1194
1242
  elements.messagesArea.innerHTML = ''
1243
+ elements.httpMessagesArea.innerHTML = ''
1195
1244
 
1196
1245
  updateCharCount()
1197
1246
  updateStatus('Ready', 'disconnected')
@@ -1516,6 +1565,75 @@
1516
1565
  }
1517
1566
  }
1518
1567
 
1568
+ // HTTP API request handler
1569
+ async function makeHTTPRequest(userInput = null) {
1570
+ const config = state.currentConfig
1571
+ const userId = elements.phoneNumber.value
1572
+
1573
+ // Build HTTP request payload
1574
+ const requestData = {
1575
+ session_id: state.sessionId,
1576
+ user_id: userId,
1577
+ input: userInput || '',
1578
+ simulator_mode: true
1579
+ }
1580
+
1581
+ try {
1582
+ const response = await fetch(config.endpoint, {
1583
+ method: 'POST',
1584
+ headers: { 'Content-Type': 'application/json' },
1585
+ body: JSON.stringify(requestData),
1586
+ credentials: 'include'
1587
+ })
1588
+
1589
+ // Read and parse response
1590
+ const responseText = await response.text()
1591
+ let responseData = null
1592
+
1593
+ // Try to parse as JSON
1594
+ if (response.headers.get('content-type')?.includes('application/json')) {
1595
+ try {
1596
+ responseData = JSON.parse(responseText)
1597
+
1598
+ // Display the HTTP response
1599
+ displayHTTPResponse(responseData)
1600
+ addRequestLog('POST', config.endpoint, requestData, responseData, response.status)
1601
+
1602
+ } catch (jsonError) {
1603
+ console.warn('Failed to parse JSON response:', jsonError)
1604
+ responseData = responseText
1605
+ displayHTTPResponse({ message: responseText, type: 'text' })
1606
+ addRequestLog('POST', config.endpoint, requestData, responseData, response.status)
1607
+ }
1608
+ } else {
1609
+ responseData = responseText
1610
+ displayHTTPResponse({ message: responseText, type: 'text' })
1611
+ addRequestLog('POST', config.endpoint, requestData, responseData, response.status)
1612
+ }
1613
+
1614
+ if (!response.ok) {
1615
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
1616
+ }
1617
+
1618
+ } catch (error) {
1619
+ // Log the error
1620
+ addRequestLog('POST', config.endpoint, requestData, null, 0, error.message)
1621
+
1622
+ // Show error in HTTP chat
1623
+ setTimeout(() => {
1624
+ addHttpMessage(
1625
+ `❌ Request Failed: ${error.message}\n\n` +
1626
+ `💡 Ensure your HTTP endpoint:\n` +
1627
+ `• Accepts POST requests\n` +
1628
+ `• Returns JSON with message, type, choices (optional), media (optional)\n` +
1629
+ `• Handles CORS if cross-origin`
1630
+ , false, 'error')
1631
+ }, 500)
1632
+
1633
+ throw error
1634
+ }
1635
+ }
1636
+
1519
1637
  // Display Functions
1520
1638
  function displayUSSDResponse(content) {
1521
1639
  elements.ussdScreen.textContent = content
@@ -1889,6 +2007,100 @@
1889
2007
  elements.messagesArea.scrollTop = elements.messagesArea.scrollHeight
1890
2008
  }
1891
2009
 
2010
+ function displayHTTPResponse(responseData) {
2011
+ // Handle the HTTP response data
2012
+ let message = responseData.message || 'Empty response'
2013
+ let messageType = responseData.type || 'text'
2014
+ let choices = responseData.choices || null
2015
+ let media = responseData.media || null
2016
+
2017
+ // Log choices for debugging if needed
2018
+ if (choices && console.log) {
2019
+ console.log('HTTP Response choices:', choices)
2020
+ }
2021
+
2022
+ // Add incoming HTTP message
2023
+ addHttpMessage(message, false, messageType, choices, media)
2024
+
2025
+ // Update session state based on response type
2026
+ state.isRunning = (messageType === 'prompt' || messageType === 'text')
2027
+ }
2028
+
2029
+ function addHttpMessage(content, isOutgoing = false, type = 'text', choices = null, media = null) {
2030
+ const messageDiv = document.createElement('div')
2031
+ messageDiv.className = `message ${isOutgoing ? 'outgoing' : 'incoming'}`
2032
+
2033
+ const bubbleDiv = document.createElement('div')
2034
+ bubbleDiv.className = 'message-bubble'
2035
+
2036
+ // Style differently for different message types
2037
+ if (type === 'error') {
2038
+ bubbleDiv.style.background = '#ffebee'
2039
+ bubbleDiv.style.borderLeft = '4px solid #f44336'
2040
+ bubbleDiv.style.whiteSpace = 'pre-line'
2041
+ } else if (type === 'terminal') {
2042
+ bubbleDiv.style.background = '#f3e5f5'
2043
+ bubbleDiv.style.borderLeft = '4px solid #9c27b0'
2044
+ }
2045
+
2046
+ // Handle media content if present
2047
+ if (media && !isOutgoing) {
2048
+ const mediaContainer = document.createElement('div')
2049
+ mediaContainer.className = 'media-container'
2050
+ mediaContainer.style.marginBottom = content ? '8px' : '0'
2051
+
2052
+ if (media.type === 'image' && media.url) {
2053
+ const img = document.createElement('img')
2054
+ img.src = media.url
2055
+ img.style.maxWidth = '100%'
2056
+ img.style.height = 'auto'
2057
+ img.style.borderRadius = '8px'
2058
+ img.style.display = 'block'
2059
+ img.alt = media.caption || 'Image'
2060
+ mediaContainer.appendChild(img)
2061
+ }
2062
+
2063
+ bubbleDiv.appendChild(mediaContainer)
2064
+ }
2065
+
2066
+ // Add text content
2067
+ if (content) {
2068
+ const textDiv = document.createElement('div')
2069
+ textDiv.textContent = content
2070
+ bubbleDiv.appendChild(textDiv)
2071
+ }
2072
+
2073
+ messageDiv.appendChild(bubbleDiv)
2074
+
2075
+ // Add choice buttons for incoming messages
2076
+ if (!isOutgoing && choices && Object.keys(choices).length > 0) {
2077
+ const buttonsDiv = document.createElement('div')
2078
+ buttonsDiv.className = 'interactive-buttons'
2079
+
2080
+ Object.entries(choices).forEach(([key, value]) => {
2081
+ const btn = document.createElement('div')
2082
+ btn.className = 'interactive-button'
2083
+
2084
+ const choiceKey = value.key || key
2085
+ const displayValue = value.value || 'Option'
2086
+
2087
+ btn.textContent = displayValue
2088
+ btn.onclick = () => selectHttpOption(choiceKey)
2089
+ buttonsDiv.appendChild(btn)
2090
+ })
2091
+
2092
+ bubbleDiv.appendChild(buttonsDiv)
2093
+ }
2094
+
2095
+ elements.httpMessagesArea.appendChild(messageDiv)
2096
+ elements.httpMessagesArea.scrollTop = elements.httpMessagesArea.scrollHeight
2097
+ }
2098
+
2099
+ function selectHttpOption(optionKey) {
2100
+ elements.messageInput.value = optionKey
2101
+ sendMessage()
2102
+ }
2103
+
1892
2104
  function getDocumentIcon(filename) {
1893
2105
  if (!filename) return '📄'
1894
2106
 
@@ -1,58 +1,6 @@
1
1
  module FlowChat
2
2
  module Ussd
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 already been presented" if navigation_stack.include?(key)
16
-
17
- navigation_stack << key
18
- return session.get(key) if session.get(key).present?
19
-
20
- prompt = FlowChat::Prompt.new 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 say(msg, media: nil)
29
- raise FlowChat::Interrupt::Terminate.new(msg)
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
3
+ class App < FlowChat::BaseApp
56
4
  end
57
5
  end
58
6
  end
@@ -1,5 +1,3 @@
1
- require "phonelib"
2
-
3
1
  module FlowChat
4
2
  module Ussd
5
3
  module Gateway
@@ -17,23 +15,21 @@ module FlowChat
17
15
  params = context.controller.request.params
18
16
 
19
17
  context["request.id"] = params["USERID"]
18
+ context["request.msisdn"] = FlowChat::PhoneNumberUtil.to_e164(params["MSISDN"])
19
+ context["request.user_id"] = context["request.msisdn"]
20
20
  context["request.message_id"] = SecureRandom.uuid
21
21
  context["request.timestamp"] = Time.current.iso8601
22
22
  context["request.gateway"] = :nalo
23
23
  context["request.platform"] = :ussd
24
24
  context["request.network"] = nil
25
- context["request.msisdn"] = Phonelib.parse(params["MSISDN"]).e164
26
25
  # context["request.type"] = params["MSGTYPE"] ? :initial : :response
27
26
  context.input = params["USERDATA"].presence
28
27
 
29
28
  # Instrument message received when user provides input using new scalable approach
30
29
  if context.input.present?
31
30
  instrument(Events::MESSAGE_RECEIVED, {
32
- from: context["request.msisdn"],
31
+ from: context["request.user_id"],
33
32
  message: context.input,
34
- session_id: context["request.id"],
35
- gateway: :nalo,
36
- platform: :ussd,
37
33
  timestamp: context["request.timestamp"]
38
34
  })
39
35
  end
@@ -1,5 +1,3 @@
1
- require "phonelib"
2
-
3
1
  module FlowChat
4
2
  module Ussd
5
3
  module Gateway
@@ -1,47 +1,21 @@
1
+ require_relative "../../base_executor"
2
+
1
3
  module FlowChat
2
4
  module Ussd
3
5
  module Middleware
4
- class Executor
5
- def initialize(app)
6
- @app = app
7
- FlowChat.logger.debug { "Ussd::Executor: Initialized USSD executor middleware" }
8
- end
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}" }
6
+ class Executor < FlowChat::BaseExecutor
7
+ protected
16
8
 
17
- ussd_app = build_ussd_app context
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."
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?}" }
29
- [:prompt, e.prompt, e.choices, e.media]
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}" }
33
- context.session.destroy
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
9
+ def platform_name
10
+ "USSD"
39
11
  end
40
12
 
41
- private
13
+ def log_prefix
14
+ "Ussd::Executor"
15
+ end
42
16
 
43
- def build_ussd_app(context)
44
- FlowChat.logger.debug { "Ussd::Executor: Building USSD app instance" }
17
+ def build_platform_app(context)
18
+ FlowChat.logger.debug { "#{log_prefix}: Building USSD app instance" }
45
19
  FlowChat::Ussd::App.new(context)
46
20
  end
47
21
  end
@@ -1,3 +1,3 @@
1
1
  module FlowChat
2
- VERSION = "0.8.0"
2
+ VERSION = "0.8.2"
3
3
  end
@@ -1,53 +1,10 @@
1
1
  module FlowChat
2
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 already 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::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, media: nil)
35
- raise FlowChat::Interrupt::Terminate.new(msg, media: media)
36
- end
37
-
38
- # WhatsApp-specific data accessors (read-only)
3
+ class App < FlowChat::BaseApp
39
4
  def contact_name
40
5
  context["request.contact_name"]
41
6
  end
42
7
 
43
- def message_id
44
- context["request.message_id"]
45
- end
46
-
47
- def timestamp
48
- context["request.timestamp"]
49
- end
50
-
51
8
  def location
52
9
  context["request.location"]
53
10
  end
@@ -56,8 +13,16 @@ module FlowChat
56
13
  context["request.media"]
57
14
  end
58
15
 
59
- def phone_number
60
- context["request.msisdn"]
16
+ protected
17
+
18
+ # WhatsApp has special startup logic and supports media
19
+ def prepare_user_input
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
+ user_input
61
26
  end
62
27
  end
63
28
  end
@@ -1,6 +1,5 @@
1
1
  require "net/http"
2
2
  require "json"
3
- require "phonelib"
4
3
  require "openssl"
5
4
 
6
5
  module FlowChat
@@ -145,29 +144,32 @@ module FlowChat
145
144
  message = value["messages"].first
146
145
  contact = value["contacts"]&.first
147
146
 
148
- phone_number = message["from"]
147
+ phone_number = FlowChat::PhoneNumberUtil.to_e164(message["from"])
149
148
  message_id = message["id"]
150
149
  contact_name = contact&.dig("profile", "name")
151
150
 
152
- # Use instrumentation for message received
153
- instrument(Events::MESSAGE_RECEIVED, {
154
- from: phone_number,
155
- message: context.input,
156
- message_type: message["type"],
157
- message_id: message_id,
158
- platform: :whatsapp
159
- })
160
-
161
151
  context["request.id"] = phone_number
152
+ context["request.msisdn"] = phone_number
153
+ context["request.user_id"] = context["request.msisdn"]
162
154
  context["request.gateway"] = :whatsapp_cloud_api
163
155
  context["request.platform"] = :whatsapp
164
156
  context["request.message_id"] = message_id
165
- context["request.msisdn"] = Phonelib.parse(phone_number).e164
166
157
  context["request.contact_name"] = contact_name
167
158
  context["request.timestamp"] = message["timestamp"]
168
159
 
169
160
  # Extract message content based on type
170
- extract_message_content(message, context)
161
+ extract_message_content!(message, context)
162
+
163
+ if context.input.present?
164
+ # Use instrumentation for message received
165
+ instrument(Events::MESSAGE_RECEIVED, {
166
+ from: phone_number,
167
+ message: context.input,
168
+ message_type: message["type"],
169
+ message_id: message_id,
170
+ })
171
+ end
172
+
171
173
 
172
174
  FlowChat.logger.debug { "CloudApi: Message content extracted - Type: #{message["type"]}, Input: '#{context.input}'" }
173
175
 
@@ -260,7 +262,7 @@ module FlowChat
260
262
  res == 0
261
263
  end
262
264
 
263
- def extract_message_content(message, context)
265
+ def extract_message_content!(message, context)
264
266
  message_type = message["type"]
265
267
  FlowChat.logger.debug { "CloudApi: Extracting content from #{message_type} message" }
266
268