flow_chat 0.8.1 → 0.9.0
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/.cliff.toml +74 -0
- data/.github/workflows/ci.yml +2 -3
- data/.github/workflows/release.yml +56 -0
- data/.standard.yml +4 -0
- data/CHANGELOG.md +22 -0
- data/CLAUDE.md +327 -0
- data/CONTRIBUTING.md +134 -0
- data/Gemfile +1 -0
- data/README.md +290 -105
- data/Rakefile +5 -1
- data/SECURITY.md +42 -349
- data/docs/architecture.md +510 -0
- data/docs/async-background-processing.md +298 -0
- data/docs/configuration.md +556 -226
- data/docs/factory-pattern.md +355 -0
- data/docs/gateway-context-variables.md +171 -0
- data/docs/gateway-development.md +723 -0
- data/docs/getting-started.md +429 -0
- data/docs/instrumentation.md +264 -153
- data/docs/platforms/telegram.md +1013 -0
- data/docs/platforms/ussd.md +693 -0
- data/docs/platforms/whatsapp.md +1395 -0
- data/docs/testing.md +243 -365
- data/examples/custom_session_id_example.rb +119 -0
- data/examples/http_controller.rb +154 -0
- data/examples/intercom_configuration_example.rb +118 -0
- data/examples/intercom_controller.rb +194 -0
- data/examples/multi_tenant_whatsapp_controller.rb +4 -4
- data/examples/simulator_controller.rb +21 -1
- data/examples/ussd_controller.rb +10 -10
- data/examples/whatsapp_controller.rb +2 -2
- data/flow_chat.gemspec +4 -0
- data/lib/flow_chat/{base_app.rb → app.rb} +28 -9
- data/lib/flow_chat/async_job.rb +176 -0
- data/lib/flow_chat/config.rb +23 -28
- data/lib/flow_chat/{base_executor.rb → executor.rb} +7 -12
- data/lib/flow_chat/factory.rb +94 -0
- data/lib/flow_chat/gateway_async_support.rb +106 -0
- data/lib/flow_chat/generic_async_job.rb +30 -0
- data/lib/flow_chat/http/gateway/simple.rb +125 -0
- data/lib/flow_chat/http/renderer.rb +41 -0
- data/lib/flow_chat/instrumentation/setup.rb +1 -1
- data/lib/flow_chat/instrumentation.rb +25 -0
- data/lib/flow_chat/intercom/client.rb +155 -0
- data/lib/flow_chat/intercom/configuration.rb +149 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +396 -0
- data/lib/flow_chat/intercom/renderer.rb +71 -0
- data/lib/flow_chat/phone_number_util.rb +49 -0
- data/lib/flow_chat/processor.rb +188 -0
- data/lib/flow_chat/renderers/markdown_support.rb +58 -0
- data/lib/flow_chat/session/cache_session_store.rb +1 -17
- data/lib/flow_chat/session/middleware.rb +43 -26
- data/lib/flow_chat/simulator/controller.rb +17 -5
- data/lib/flow_chat/simulator/views/simulator.html.erb +220 -8
- data/lib/flow_chat/telegram/client.rb +240 -0
- data/lib/flow_chat/telegram/configuration.rb +118 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +300 -0
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +35 -0
- data/lib/flow_chat/telegram/renderer.rb +125 -0
- data/lib/flow_chat/telegram.rb +7 -0
- data/lib/flow_chat/ussd/gateway/nalo.rb +27 -11
- data/lib/flow_chat/ussd/middleware/pagination.rb +9 -5
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +144 -13
- data/lib/flow_chat/whatsapp/configuration.rb +1 -1
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +144 -106
- data/lib/flow_chat/whatsapp/id_generator.rb +124 -0
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +147 -0
- data/lib/flow_chat/whatsapp/renderer.rb +145 -11
- data/lib/flow_chat.rb +11 -1
- data/lib/tasks/release.rake +165 -0
- metadata +88 -21
- data/docs/flows.md +0 -320
- data/docs/images/simulator.png +0 -0
- data/docs/media.md +0 -153
- data/docs/sessions.md +0 -433
- data/docs/ussd-setup.md +0 -322
- data/docs/whatsapp-setup.md +0 -162
- data/examples/whatsapp_message_job.rb +0 -113
- data/lib/flow_chat/base_processor.rb +0 -145
- data/lib/flow_chat/session/rails_session_store.rb +0 -68
- data/lib/flow_chat/ussd/app.rb +0 -6
- data/lib/flow_chat/ussd/gateway/nsano.rb +0 -98
- data/lib/flow_chat/ussd/middleware/executor.rb +0 -24
- data/lib/flow_chat/ussd/processor.rb +0 -39
- data/lib/flow_chat/whatsapp/app.rb +0 -29
- data/lib/flow_chat/whatsapp/middleware/executor.rb +0 -24
- data/lib/flow_chat/whatsapp/processor.rb +0 -32
- data/lib/flow_chat/whatsapp/send_job_support.rb +0 -79
|
@@ -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
|
|
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
|
-
|
|
1103
|
-
|
|
1104
|
-
'
|
|
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
|
|
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
|
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
require "net/http"
|
|
2
|
+
require "json"
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module FlowChat
|
|
6
|
+
module Telegram
|
|
7
|
+
class Client
|
|
8
|
+
include FlowChat::Instrumentation
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
FlowChat.logger.info { "Telegram::Client: Initialized Telegram client" }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Main send_message method matching FlowChat pattern
|
|
16
|
+
def send_message(chat_id, prompt, choices: nil, media: nil)
|
|
17
|
+
FlowChat.logger.info { "Telegram::Client: Sending message to chat #{chat_id}" }
|
|
18
|
+
|
|
19
|
+
response = FlowChat::Telegram::Renderer.new(prompt, choices: choices, media: media).render
|
|
20
|
+
type, content, options = response
|
|
21
|
+
|
|
22
|
+
case type
|
|
23
|
+
when :text
|
|
24
|
+
send_text(chat_id, content)
|
|
25
|
+
when :inline_keyboard
|
|
26
|
+
send_text_with_keyboard(chat_id, content, options[:keyboard])
|
|
27
|
+
when :photo
|
|
28
|
+
send_photo(chat_id, options[:url], caption: content)
|
|
29
|
+
when :photo_with_keyboard
|
|
30
|
+
send_photo_with_keyboard(chat_id, options[:url], caption: content, keyboard: options[:keyboard])
|
|
31
|
+
when :document
|
|
32
|
+
send_document(chat_id, options[:url], caption: content)
|
|
33
|
+
when :video
|
|
34
|
+
send_video(chat_id, options[:url], caption: content)
|
|
35
|
+
when :audio
|
|
36
|
+
send_audio(chat_id, options[:url], caption: content)
|
|
37
|
+
when :voice
|
|
38
|
+
send_voice(chat_id, options[:url])
|
|
39
|
+
else
|
|
40
|
+
send_text(chat_id, content.to_s)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def send_text(chat_id, text, parse_mode: "HTML")
|
|
45
|
+
api_request("sendMessage", {
|
|
46
|
+
chat_id: chat_id,
|
|
47
|
+
text: text,
|
|
48
|
+
parse_mode: parse_mode
|
|
49
|
+
}.compact)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def send_text_with_keyboard(chat_id, text, keyboard, parse_mode: "HTML")
|
|
53
|
+
api_request("sendMessage", {
|
|
54
|
+
chat_id: chat_id,
|
|
55
|
+
text: text,
|
|
56
|
+
parse_mode: parse_mode,
|
|
57
|
+
reply_markup: {inline_keyboard: keyboard}
|
|
58
|
+
}.compact)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def send_photo(chat_id, photo_url_or_id, caption: nil)
|
|
62
|
+
api_request("sendPhoto", {
|
|
63
|
+
chat_id: chat_id,
|
|
64
|
+
photo: photo_url_or_id,
|
|
65
|
+
caption: caption
|
|
66
|
+
}.compact)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def send_photo_with_keyboard(chat_id, photo_url_or_id, caption: nil, keyboard: nil)
|
|
70
|
+
api_request("sendPhoto", {
|
|
71
|
+
chat_id: chat_id,
|
|
72
|
+
photo: photo_url_or_id,
|
|
73
|
+
caption: caption,
|
|
74
|
+
reply_markup: keyboard ? {inline_keyboard: keyboard} : nil
|
|
75
|
+
}.compact)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def send_document(chat_id, document_url_or_id, caption: nil)
|
|
79
|
+
api_request("sendDocument", {
|
|
80
|
+
chat_id: chat_id,
|
|
81
|
+
document: document_url_or_id,
|
|
82
|
+
caption: caption
|
|
83
|
+
}.compact)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def send_video(chat_id, video_url_or_id, caption: nil)
|
|
87
|
+
api_request("sendVideo", {
|
|
88
|
+
chat_id: chat_id,
|
|
89
|
+
video: video_url_or_id,
|
|
90
|
+
caption: caption
|
|
91
|
+
}.compact)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def send_audio(chat_id, audio_url_or_id, caption: nil)
|
|
95
|
+
api_request("sendAudio", {
|
|
96
|
+
chat_id: chat_id,
|
|
97
|
+
audio: audio_url_or_id,
|
|
98
|
+
caption: caption
|
|
99
|
+
}.compact)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def send_voice(chat_id, voice_url_or_id)
|
|
103
|
+
api_request("sendVoice", {
|
|
104
|
+
chat_id: chat_id,
|
|
105
|
+
voice: voice_url_or_id
|
|
106
|
+
})
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def answer_callback_query(callback_query_id, text: nil, show_alert: false)
|
|
110
|
+
api_request("answerCallbackQuery", {
|
|
111
|
+
callback_query_id: callback_query_id,
|
|
112
|
+
text: text,
|
|
113
|
+
show_alert: show_alert
|
|
114
|
+
}.compact)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def edit_message_text(chat_id, message_id, text, keyboard: nil, parse_mode: "HTML")
|
|
118
|
+
api_request("editMessageText", {
|
|
119
|
+
chat_id: chat_id,
|
|
120
|
+
message_id: message_id,
|
|
121
|
+
text: text,
|
|
122
|
+
parse_mode: parse_mode,
|
|
123
|
+
reply_markup: keyboard ? {inline_keyboard: keyboard} : nil
|
|
124
|
+
}.compact)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def delete_message(chat_id, message_id)
|
|
128
|
+
api_request("deleteMessage", {
|
|
129
|
+
chat_id: chat_id,
|
|
130
|
+
message_id: message_id
|
|
131
|
+
})
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Send a chat action (e.g. typing indicator) to a Telegram chat.
|
|
135
|
+
#
|
|
136
|
+
# The action lasts ~5 seconds or until the next outbound message.
|
|
137
|
+
# Valid actions per Telegram Bot API: "typing", "upload_photo",
|
|
138
|
+
# "record_video", "upload_video", "record_voice", "upload_voice",
|
|
139
|
+
# "upload_document", "choose_sticker", "find_location",
|
|
140
|
+
# "record_video_note", "upload_video_note".
|
|
141
|
+
#
|
|
142
|
+
# @param chat_id [Integer, String] the target chat id
|
|
143
|
+
# @param action [String] the chat action to broadcast (default: "typing")
|
|
144
|
+
# @return [Hash] parsed Telegram API response
|
|
145
|
+
def send_chat_action(chat_id, action: "typing")
|
|
146
|
+
api_request("sendChatAction", chat_id: chat_id, action: action)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Show a typing indicator in a Telegram chat.
|
|
150
|
+
#
|
|
151
|
+
# Convenience wrapper around `send_chat_action(chat_id, action: "typing")`.
|
|
152
|
+
# The indicator lasts ~5 seconds or until the next outbound message;
|
|
153
|
+
# there is no stop-typing call.
|
|
154
|
+
#
|
|
155
|
+
# @param chat_id [Integer, String] the target chat id
|
|
156
|
+
# @return [Hash] parsed Telegram API response
|
|
157
|
+
def indicate_typing(chat_id)
|
|
158
|
+
send_chat_action(chat_id, action: "typing")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Webhook management
|
|
162
|
+
def set_webhook(url, secret_token: nil, allowed_updates: nil)
|
|
163
|
+
api_request("setWebhook", {
|
|
164
|
+
url: url,
|
|
165
|
+
secret_token: secret_token,
|
|
166
|
+
allowed_updates: allowed_updates || ["message", "callback_query"]
|
|
167
|
+
}.compact)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def delete_webhook
|
|
171
|
+
api_request("deleteWebhook")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def get_webhook_info
|
|
175
|
+
api_request("getWebhookInfo")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def get_me
|
|
179
|
+
api_request("getMe")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
private
|
|
183
|
+
|
|
184
|
+
def api_request(method, params = {})
|
|
185
|
+
uri = URI("#{@config.api_base_url}/#{method}")
|
|
186
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
187
|
+
http.use_ssl = true
|
|
188
|
+
|
|
189
|
+
request = Net::HTTP::Post.new(uri)
|
|
190
|
+
request["Content-Type"] = "application/json"
|
|
191
|
+
request.body = params.to_json
|
|
192
|
+
|
|
193
|
+
FlowChat.logger.debug { "Telegram::Client: API request to #{method}" }
|
|
194
|
+
|
|
195
|
+
response = http.request(request)
|
|
196
|
+
result = JSON.parse(response.body)
|
|
197
|
+
|
|
198
|
+
if result["ok"]
|
|
199
|
+
FlowChat.logger.debug { "Telegram::Client: API request successful" }
|
|
200
|
+
else
|
|
201
|
+
FlowChat.logger.error { "Telegram::Client: API error - #{result["description"]}" }
|
|
202
|
+
report_api_error(
|
|
203
|
+
"Telegram API error: #{result["description"]}",
|
|
204
|
+
api_method: method,
|
|
205
|
+
error_code: result["error_code"],
|
|
206
|
+
error_description: result["description"],
|
|
207
|
+
chat_id: params[:chat_id]
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
result
|
|
212
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => network_error
|
|
213
|
+
FlowChat.logger.error { "Telegram::Client: Network timeout: #{network_error.class.name}: #{network_error.message}" }
|
|
214
|
+
raise network_error
|
|
215
|
+
rescue => error
|
|
216
|
+
FlowChat.logger.error { "Telegram::Client: API request exception: #{error.class.name}: #{error.message}" }
|
|
217
|
+
report_api_error(
|
|
218
|
+
"Telegram API request exception: #{error.class.name}",
|
|
219
|
+
api_method: method,
|
|
220
|
+
error: error,
|
|
221
|
+
chat_id: params[:chat_id]
|
|
222
|
+
)
|
|
223
|
+
{"ok" => false, "description" => error.message}
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def report_api_error(message, api_method: nil, error_code: nil, error_description: nil, error: nil, chat_id: nil)
|
|
227
|
+
FlowChat::Instrumentation.report_api_error(
|
|
228
|
+
message,
|
|
229
|
+
error: error,
|
|
230
|
+
platform: :telegram,
|
|
231
|
+
bot_id: @config.bot_id,
|
|
232
|
+
api_method: api_method,
|
|
233
|
+
error_code: error_code,
|
|
234
|
+
error_description: error_description,
|
|
235
|
+
chat_id: chat_id
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module FlowChat
|
|
2
|
+
module Telegram
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor :bot_token, :secret_token, :name, :skip_signature_validation
|
|
5
|
+
|
|
6
|
+
@@configurations = {}
|
|
7
|
+
|
|
8
|
+
def initialize(name)
|
|
9
|
+
@name = name
|
|
10
|
+
@bot_token = nil
|
|
11
|
+
@secret_token = nil
|
|
12
|
+
@skip_signature_validation = false
|
|
13
|
+
|
|
14
|
+
FlowChat.logger.debug { "Telegram::Configuration: Initialized configuration with name: #{name || "anonymous"}" }
|
|
15
|
+
|
|
16
|
+
register_as(name) if name.present?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.from_credentials
|
|
20
|
+
FlowChat.logger.info { "Telegram::Configuration: Loading configuration from credentials/environment" }
|
|
21
|
+
|
|
22
|
+
config = new(nil)
|
|
23
|
+
|
|
24
|
+
if defined?(Rails) && Rails.respond_to?(:application) && Rails.application.credentials.telegram
|
|
25
|
+
FlowChat.logger.debug { "Telegram::Configuration: Loading from Rails credentials" }
|
|
26
|
+
credentials = Rails.application.credentials.telegram
|
|
27
|
+
config.bot_token = credentials[:bot_token]
|
|
28
|
+
config.secret_token = credentials[:secret_token]
|
|
29
|
+
config.skip_signature_validation = credentials[:skip_signature_validation] || false
|
|
30
|
+
else
|
|
31
|
+
FlowChat.logger.debug { "Telegram::Configuration: Loading from environment variables" }
|
|
32
|
+
config.bot_token = ENV["TELEGRAM_BOT_TOKEN"]
|
|
33
|
+
config.secret_token = ENV["TELEGRAM_SECRET_TOKEN"]
|
|
34
|
+
config.skip_signature_validation = ENV["TELEGRAM_SKIP_SIGNATURE_VALIDATION"] == "true"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if config.valid?
|
|
38
|
+
FlowChat.logger.info { "Telegram::Configuration: Configuration loaded successfully" }
|
|
39
|
+
else
|
|
40
|
+
FlowChat.logger.warn { "Telegram::Configuration: Incomplete configuration loaded - missing required fields" }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
config
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.register(name, config)
|
|
47
|
+
FlowChat.logger.debug { "Telegram::Configuration: Registering configuration '#{name}'" }
|
|
48
|
+
@@configurations[name.to_sym] = config
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.get(name)
|
|
52
|
+
config = @@configurations[name.to_sym]
|
|
53
|
+
if config
|
|
54
|
+
FlowChat.logger.debug { "Telegram::Configuration: Retrieved configuration '#{name}'" }
|
|
55
|
+
config
|
|
56
|
+
else
|
|
57
|
+
FlowChat.logger.error { "Telegram::Configuration: Configuration '#{name}' not found" }
|
|
58
|
+
raise ArgumentError, "Telegram configuration '#{name}' not found"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.exists?(name)
|
|
63
|
+
exists = @@configurations.key?(name.to_sym)
|
|
64
|
+
FlowChat.logger.debug { "Telegram::Configuration: Configuration '#{name}' exists: #{exists}" }
|
|
65
|
+
exists
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.configuration_names
|
|
69
|
+
names = @@configurations.keys
|
|
70
|
+
FlowChat.logger.debug { "Telegram::Configuration: Available configurations: #{names}" }
|
|
71
|
+
names
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.clear_all!
|
|
75
|
+
FlowChat.logger.debug { "Telegram::Configuration: Clearing all registered configurations" }
|
|
76
|
+
@@configurations.clear
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def register_as(name)
|
|
80
|
+
FlowChat.logger.debug { "Telegram::Configuration: Registering configuration as '#{name}'" }
|
|
81
|
+
@name = name.to_sym
|
|
82
|
+
self.class.register(@name, self)
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def valid?
|
|
87
|
+
is_valid = !!(bot_token && !bot_token.to_s.empty?)
|
|
88
|
+
FlowChat.logger.debug { "Telegram::Configuration: Configuration valid: #{is_valid}" }
|
|
89
|
+
is_valid
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def api_base_url
|
|
93
|
+
return nil unless bot_token
|
|
94
|
+
"https://api.telegram.org/bot#{bot_token}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def bot_id
|
|
98
|
+
bot_token&.split(":")&.first
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def send_message_url
|
|
102
|
+
"#{api_base_url}/sendMessage"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def set_webhook_url
|
|
106
|
+
"#{api_base_url}/setWebhook"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def get_webhook_info_url
|
|
110
|
+
"#{api_base_url}/getWebhookInfo"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def delete_webhook_url
|
|
114
|
+
"#{api_base_url}/deleteWebhook"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|