flow_chat 0.8.1 → 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.
@@ -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,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,3 +1,3 @@
1
1
  module FlowChat
2
- VERSION = "0.8.1"
2
+ VERSION = "0.8.2"
3
3
  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
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flow_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-06-14 00:00:00.000000000 Z
11
+ date: 2025-06-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -104,6 +104,7 @@ files:
104
104
  - bin/setup
105
105
  - docs/configuration.md
106
106
  - docs/flows.md
107
+ - docs/http-gateway-protocol.md
107
108
  - docs/images/simulator.png
108
109
  - docs/instrumentation.md
109
110
  - docs/media.md
@@ -111,6 +112,7 @@ files:
111
112
  - docs/testing.md
112
113
  - docs/ussd-setup.md
113
114
  - docs/whatsapp-setup.md
115
+ - examples/http_controller.rb
114
116
  - examples/multi_tenant_whatsapp_controller.rb
115
117
  - examples/simulator_controller.rb
116
118
  - examples/ussd_controller.rb
@@ -125,11 +127,17 @@ files:
125
127
  - lib/flow_chat/config.rb
126
128
  - lib/flow_chat/context.rb
127
129
  - lib/flow_chat/flow.rb
130
+ - lib/flow_chat/http/app.rb
131
+ - lib/flow_chat/http/gateway/simple.rb
132
+ - lib/flow_chat/http/middleware/executor.rb
133
+ - lib/flow_chat/http/processor.rb
134
+ - lib/flow_chat/http/renderer.rb
128
135
  - lib/flow_chat/instrumentation.rb
129
136
  - lib/flow_chat/instrumentation/log_subscriber.rb
130
137
  - lib/flow_chat/instrumentation/metrics_collector.rb
131
138
  - lib/flow_chat/instrumentation/setup.rb
132
139
  - lib/flow_chat/interrupt.rb
140
+ - lib/flow_chat/phone_number_util.rb
133
141
  - lib/flow_chat/prompt.rb
134
142
  - lib/flow_chat/session/cache_session_store.rb
135
143
  - lib/flow_chat/session/middleware.rb