flow_chat 0.4.1 β 0.5.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.
- checksums.yaml +4 -4
- data/README.md +418 -295
- data/SECURITY.md +365 -0
- data/examples/initializer.rb +56 -1
- data/examples/media_prompts_examples.rb +1 -2
- data/examples/multi_tenant_whatsapp_controller.rb +61 -57
- data/examples/simulator_controller.rb +95 -0
- data/examples/ussd_controller.rb +17 -11
- data/examples/whatsapp_controller.rb +103 -14
- data/examples/whatsapp_media_examples.rb +78 -80
- data/examples/whatsapp_message_job.rb +3 -3
- data/lib/flow_chat/base_processor.rb +3 -2
- data/lib/flow_chat/config.rb +6 -3
- data/lib/flow_chat/session/cache_session_store.rb +5 -5
- data/lib/flow_chat/simulator/controller.rb +34 -5
- data/lib/flow_chat/simulator/views/simulator.html.erb +287 -12
- data/lib/flow_chat/ussd/gateway/nsano.rb +1 -1
- data/lib/flow_chat/ussd/processor.rb +1 -1
- data/lib/flow_chat/ussd/prompt.rb +13 -13
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +44 -50
- data/lib/flow_chat/whatsapp/configuration.rb +21 -20
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +129 -19
- data/lib/flow_chat/whatsapp/middleware/executor.rb +1 -1
- data/lib/flow_chat/whatsapp/processor.rb +1 -1
- data/lib/flow_chat/whatsapp/prompt.rb +27 -31
- data/lib/flow_chat/whatsapp/send_job_support.rb +7 -7
- data/lib/flow_chat/whatsapp/template_manager.rb +10 -10
- metadata +4 -2
@@ -1529,6 +1529,7 @@
|
|
1529
1529
|
// Extract message content based on type
|
1530
1530
|
let messageText = ''
|
1531
1531
|
let interactive = null
|
1532
|
+
let mediaContent = null
|
1532
1533
|
|
1533
1534
|
switch (messagePayload.type) {
|
1534
1535
|
case 'text':
|
@@ -1570,25 +1571,48 @@
|
|
1570
1571
|
}
|
1571
1572
|
break
|
1572
1573
|
case 'image':
|
1573
|
-
messageText = messagePayload.image.caption || '
|
1574
|
-
|
1575
|
-
|
1574
|
+
messageText = messagePayload.image.caption || ''
|
1575
|
+
mediaContent = {
|
1576
|
+
type: 'image',
|
1577
|
+
url: messagePayload.image.link || `https://via.placeholder.com/300x200/25d366/white?text=Image+ID:+${messagePayload.image.id || 'media_123'}`,
|
1578
|
+
caption: messagePayload.image.caption
|
1579
|
+
}
|
1576
1580
|
break
|
1577
1581
|
case 'document':
|
1578
|
-
messageText = messagePayload.document.caption ||
|
1579
|
-
|
1582
|
+
messageText = messagePayload.document.caption || ''
|
1583
|
+
mediaContent = {
|
1584
|
+
type: 'document',
|
1585
|
+
url: messagePayload.document.link,
|
1586
|
+
filename: messagePayload.document.filename || 'document.pdf',
|
1587
|
+
caption: messagePayload.document.caption
|
1588
|
+
}
|
1580
1589
|
break
|
1581
1590
|
case 'audio':
|
1582
|
-
messageText = '
|
1591
|
+
messageText = ''
|
1592
|
+
mediaContent = {
|
1593
|
+
type: 'audio',
|
1594
|
+
url: messagePayload.audio.link || `data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmkcBzuU2e+saDUGOWOJ6+2SV0gYKT2L5zJAGyJH4f2GHz+7zJMxtx9R5Dsl`
|
1595
|
+
}
|
1583
1596
|
break
|
1584
1597
|
case 'video':
|
1585
|
-
messageText = messagePayload.video.caption || '
|
1586
|
-
|
1598
|
+
messageText = messagePayload.video.caption || ''
|
1599
|
+
mediaContent = {
|
1600
|
+
type: 'video',
|
1601
|
+
url: messagePayload.video.link || `https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4`,
|
1602
|
+
caption: messagePayload.video.caption
|
1603
|
+
}
|
1587
1604
|
break
|
1588
1605
|
case 'location':
|
1589
1606
|
const loc = messagePayload.location
|
1590
|
-
messageText =
|
1607
|
+
messageText = loc.name || 'Shared location'
|
1591
1608
|
if (loc.address) messageText += `\n${loc.address}`
|
1609
|
+
mediaContent = {
|
1610
|
+
type: 'location',
|
1611
|
+
latitude: loc.latitude || 0,
|
1612
|
+
longitude: loc.longitude || 0,
|
1613
|
+
name: loc.name,
|
1614
|
+
address: loc.address
|
1615
|
+
}
|
1592
1616
|
break
|
1593
1617
|
default:
|
1594
1618
|
messageText = `Unsupported message type: ${messagePayload.type}`
|
@@ -1596,16 +1620,235 @@
|
|
1596
1620
|
}
|
1597
1621
|
|
1598
1622
|
// Add the simulated message to the chat
|
1599
|
-
addMessage(messageText, false, messagePayload.type, interactive)
|
1623
|
+
addMessage(messageText, false, messagePayload.type, interactive, mediaContent)
|
1600
1624
|
}
|
1601
1625
|
|
1602
|
-
function addMessage(content, isOutgoing = false, type = 'text', interactive = null) {
|
1626
|
+
function addMessage(content, isOutgoing = false, type = 'text', interactive = null, mediaContent = null) {
|
1603
1627
|
const messageDiv = document.createElement('div')
|
1604
1628
|
messageDiv.className = `message ${isOutgoing ? 'outgoing' : 'incoming'}`
|
1605
1629
|
|
1606
1630
|
const bubbleDiv = document.createElement('div')
|
1607
1631
|
bubbleDiv.className = 'message-bubble'
|
1608
|
-
|
1632
|
+
|
1633
|
+
// Handle media content first
|
1634
|
+
if (mediaContent) {
|
1635
|
+
const mediaContainer = document.createElement('div')
|
1636
|
+
mediaContainer.className = 'media-container'
|
1637
|
+
mediaContainer.style.marginBottom = content ? '8px' : '0'
|
1638
|
+
|
1639
|
+
switch (mediaContent.type) {
|
1640
|
+
case 'image':
|
1641
|
+
const img = document.createElement('img')
|
1642
|
+
img.src = mediaContent.url
|
1643
|
+
img.style.maxWidth = '100%'
|
1644
|
+
img.style.height = 'auto'
|
1645
|
+
img.style.borderRadius = '8px'
|
1646
|
+
img.style.display = 'block'
|
1647
|
+
img.alt = mediaContent.caption || 'Image'
|
1648
|
+
img.onerror = function() {
|
1649
|
+
this.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMGYwIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPvCfk7cgSW1hZ2U8L3RleHQ+PC9zdmc+'
|
1650
|
+
this.style.border = '1px solid #e0e0e0'
|
1651
|
+
}
|
1652
|
+
mediaContainer.appendChild(img)
|
1653
|
+
break
|
1654
|
+
|
1655
|
+
case 'video':
|
1656
|
+
const video = document.createElement('video')
|
1657
|
+
video.src = mediaContent.url
|
1658
|
+
video.controls = true
|
1659
|
+
video.style.maxWidth = '100%'
|
1660
|
+
video.style.height = 'auto'
|
1661
|
+
video.style.borderRadius = '8px'
|
1662
|
+
video.style.display = 'block'
|
1663
|
+
video.preload = 'metadata'
|
1664
|
+
video.onerror = function() {
|
1665
|
+
const placeholder = document.createElement('div')
|
1666
|
+
placeholder.style.width = '300px'
|
1667
|
+
placeholder.style.height = '200px'
|
1668
|
+
placeholder.style.backgroundColor = '#f0f0f0'
|
1669
|
+
placeholder.style.border = '1px solid #e0e0e0'
|
1670
|
+
placeholder.style.borderRadius = '8px'
|
1671
|
+
placeholder.style.display = 'flex'
|
1672
|
+
placeholder.style.alignItems = 'center'
|
1673
|
+
placeholder.style.justifyContent = 'center'
|
1674
|
+
placeholder.style.color = '#999'
|
1675
|
+
placeholder.style.fontSize = '14px'
|
1676
|
+
placeholder.textContent = 'π₯ Video'
|
1677
|
+
mediaContainer.replaceChild(placeholder, video)
|
1678
|
+
}
|
1679
|
+
mediaContainer.appendChild(video)
|
1680
|
+
break
|
1681
|
+
|
1682
|
+
case 'audio':
|
1683
|
+
const audio = document.createElement('audio')
|
1684
|
+
audio.src = mediaContent.url
|
1685
|
+
audio.controls = true
|
1686
|
+
audio.style.width = '100%'
|
1687
|
+
audio.style.maxWidth = '300px'
|
1688
|
+
audio.preload = 'metadata'
|
1689
|
+
audio.onerror = function() {
|
1690
|
+
const placeholder = document.createElement('div')
|
1691
|
+
placeholder.style.padding = '12px 16px'
|
1692
|
+
placeholder.style.backgroundColor = '#f0f0f0'
|
1693
|
+
placeholder.style.border = '1px solid #e0e0e0'
|
1694
|
+
placeholder.style.borderRadius = '8px'
|
1695
|
+
placeholder.style.display = 'flex'
|
1696
|
+
placeholder.style.alignItems = 'center'
|
1697
|
+
placeholder.style.gap = '8px'
|
1698
|
+
placeholder.style.color = '#666'
|
1699
|
+
placeholder.style.fontSize = '14px'
|
1700
|
+
placeholder.innerHTML = 'π΅ <span>Audio message</span>'
|
1701
|
+
mediaContainer.replaceChild(placeholder, audio)
|
1702
|
+
}
|
1703
|
+
mediaContainer.appendChild(audio)
|
1704
|
+
break
|
1705
|
+
|
1706
|
+
case 'document':
|
1707
|
+
const docContainer = document.createElement('div')
|
1708
|
+
docContainer.style.padding = '12px 16px'
|
1709
|
+
docContainer.style.backgroundColor = '#f8f9fa'
|
1710
|
+
docContainer.style.border = '1px solid #e0e0e0'
|
1711
|
+
docContainer.style.borderRadius = '8px'
|
1712
|
+
docContainer.style.display = 'flex'
|
1713
|
+
docContainer.style.alignItems = 'center'
|
1714
|
+
docContainer.style.gap = '12px'
|
1715
|
+
docContainer.style.cursor = 'pointer'
|
1716
|
+
docContainer.style.transition = 'background-color 0.2s'
|
1717
|
+
|
1718
|
+
const docIcon = document.createElement('div')
|
1719
|
+
docIcon.style.fontSize = '24px'
|
1720
|
+
docIcon.textContent = getDocumentIcon(mediaContent.filename)
|
1721
|
+
|
1722
|
+
const docInfo = document.createElement('div')
|
1723
|
+
docInfo.style.flex = '1'
|
1724
|
+
docInfo.style.minWidth = '0'
|
1725
|
+
|
1726
|
+
const docName = document.createElement('div')
|
1727
|
+
docName.style.fontWeight = '600'
|
1728
|
+
docName.style.fontSize = '14px'
|
1729
|
+
docName.style.color = '#333'
|
1730
|
+
docName.style.overflow = 'hidden'
|
1731
|
+
docName.style.textOverflow = 'ellipsis'
|
1732
|
+
docName.style.whiteSpace = 'nowrap'
|
1733
|
+
docName.textContent = mediaContent.filename
|
1734
|
+
|
1735
|
+
const docType = document.createElement('div')
|
1736
|
+
docType.style.fontSize = '12px'
|
1737
|
+
docType.style.color = '#666'
|
1738
|
+
docType.textContent = 'Document'
|
1739
|
+
|
1740
|
+
docInfo.appendChild(docName)
|
1741
|
+
docInfo.appendChild(docType)
|
1742
|
+
docContainer.appendChild(docIcon)
|
1743
|
+
docContainer.appendChild(docInfo)
|
1744
|
+
|
1745
|
+
// Add click handler if URL is available
|
1746
|
+
if (mediaContent.url) {
|
1747
|
+
const downloadIcon = document.createElement('div')
|
1748
|
+
downloadIcon.style.fontSize = '18px'
|
1749
|
+
downloadIcon.style.color = '#666'
|
1750
|
+
downloadIcon.textContent = 'β¬οΈ'
|
1751
|
+
docContainer.appendChild(downloadIcon)
|
1752
|
+
|
1753
|
+
docContainer.onclick = () => {
|
1754
|
+
if (mediaContent.url.startsWith('http')) {
|
1755
|
+
window.open(mediaContent.url, '_blank')
|
1756
|
+
}
|
1757
|
+
}
|
1758
|
+
|
1759
|
+
docContainer.onmouseover = () => {
|
1760
|
+
docContainer.style.backgroundColor = '#e8f5e8'
|
1761
|
+
}
|
1762
|
+
docContainer.onmouseout = () => {
|
1763
|
+
docContainer.style.backgroundColor = '#f8f9fa'
|
1764
|
+
}
|
1765
|
+
}
|
1766
|
+
|
1767
|
+
mediaContainer.appendChild(docContainer)
|
1768
|
+
break
|
1769
|
+
|
1770
|
+
case 'location':
|
1771
|
+
const locationContainer = document.createElement('div')
|
1772
|
+
locationContainer.style.border = '1px solid #e0e0e0'
|
1773
|
+
locationContainer.style.borderRadius = '8px'
|
1774
|
+
locationContainer.style.overflow = 'hidden'
|
1775
|
+
locationContainer.style.maxWidth = '300px'
|
1776
|
+
|
1777
|
+
// Create a simple map-like visualization
|
1778
|
+
const mapDiv = document.createElement('div')
|
1779
|
+
mapDiv.style.height = '150px'
|
1780
|
+
mapDiv.style.background = 'linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%)'
|
1781
|
+
mapDiv.style.position = 'relative'
|
1782
|
+
mapDiv.style.display = 'flex'
|
1783
|
+
mapDiv.style.alignItems = 'center'
|
1784
|
+
mapDiv.style.justifyContent = 'center'
|
1785
|
+
mapDiv.style.fontSize = '32px'
|
1786
|
+
mapDiv.textContent = 'π'
|
1787
|
+
|
1788
|
+
// Add coordinate text
|
1789
|
+
const coordDiv = document.createElement('div')
|
1790
|
+
coordDiv.style.position = 'absolute'
|
1791
|
+
coordDiv.style.bottom = '8px'
|
1792
|
+
coordDiv.style.right = '8px'
|
1793
|
+
coordDiv.style.background = 'rgba(255,255,255,0.9)'
|
1794
|
+
coordDiv.style.padding = '4px 8px'
|
1795
|
+
coordDiv.style.borderRadius = '4px'
|
1796
|
+
coordDiv.style.fontSize = '10px'
|
1797
|
+
coordDiv.style.color = '#666'
|
1798
|
+
coordDiv.style.fontFamily = 'monospace'
|
1799
|
+
coordDiv.textContent = `${mediaContent.latitude.toFixed(4)}, ${mediaContent.longitude.toFixed(4)}`
|
1800
|
+
mapDiv.appendChild(coordDiv)
|
1801
|
+
|
1802
|
+
const locationInfo = document.createElement('div')
|
1803
|
+
locationInfo.style.padding = '12px'
|
1804
|
+
locationInfo.style.backgroundColor = 'white'
|
1805
|
+
|
1806
|
+
if (mediaContent.name) {
|
1807
|
+
const nameDiv = document.createElement('div')
|
1808
|
+
nameDiv.style.fontWeight = '600'
|
1809
|
+
nameDiv.style.fontSize = '14px'
|
1810
|
+
nameDiv.style.color = '#333'
|
1811
|
+
nameDiv.style.marginBottom = '4px'
|
1812
|
+
nameDiv.textContent = mediaContent.name
|
1813
|
+
locationInfo.appendChild(nameDiv)
|
1814
|
+
}
|
1815
|
+
|
1816
|
+
if (mediaContent.address) {
|
1817
|
+
const addressDiv = document.createElement('div')
|
1818
|
+
addressDiv.style.fontSize = '12px'
|
1819
|
+
addressDiv.style.color = '#666'
|
1820
|
+
addressDiv.textContent = mediaContent.address
|
1821
|
+
locationInfo.appendChild(addressDiv)
|
1822
|
+
}
|
1823
|
+
|
1824
|
+
// Add link to open in maps
|
1825
|
+
const mapsLink = document.createElement('div')
|
1826
|
+
mapsLink.style.fontSize = '12px'
|
1827
|
+
mapsLink.style.color = '#25d366'
|
1828
|
+
mapsLink.style.cursor = 'pointer'
|
1829
|
+
mapsLink.style.marginTop = '8px'
|
1830
|
+
mapsLink.textContent = 'πΊοΈ View in Maps'
|
1831
|
+
mapsLink.onclick = () => {
|
1832
|
+
const url = `https://www.google.com/maps?q=${mediaContent.latitude},${mediaContent.longitude}`
|
1833
|
+
window.open(url, '_blank')
|
1834
|
+
}
|
1835
|
+
locationInfo.appendChild(mapsLink)
|
1836
|
+
|
1837
|
+
locationContainer.appendChild(mapDiv)
|
1838
|
+
locationContainer.appendChild(locationInfo)
|
1839
|
+
mediaContainer.appendChild(locationContainer)
|
1840
|
+
break
|
1841
|
+
}
|
1842
|
+
|
1843
|
+
bubbleDiv.appendChild(mediaContainer)
|
1844
|
+
}
|
1845
|
+
|
1846
|
+
// Add text content if present
|
1847
|
+
if (content) {
|
1848
|
+
const textDiv = document.createElement('div')
|
1849
|
+
textDiv.textContent = content
|
1850
|
+
bubbleDiv.appendChild(textDiv)
|
1851
|
+
}
|
1609
1852
|
|
1610
1853
|
messageDiv.appendChild(bubbleDiv)
|
1611
1854
|
|
@@ -1640,10 +1883,42 @@
|
|
1640
1883
|
bubbleDiv.appendChild(buttonsDiv)
|
1641
1884
|
}
|
1642
1885
|
|
1886
|
+
messageDiv.appendChild(bubbleDiv)
|
1887
|
+
|
1643
1888
|
elements.messagesArea.appendChild(messageDiv)
|
1644
1889
|
elements.messagesArea.scrollTop = elements.messagesArea.scrollHeight
|
1645
1890
|
}
|
1646
1891
|
|
1892
|
+
function getDocumentIcon(filename) {
|
1893
|
+
if (!filename) return 'π'
|
1894
|
+
|
1895
|
+
const ext = filename.split('.').pop().toLowerCase()
|
1896
|
+
|
1897
|
+
const iconMap = {
|
1898
|
+
'pdf': 'π',
|
1899
|
+
'doc': 'π',
|
1900
|
+
'docx': 'π',
|
1901
|
+
'xls': 'π',
|
1902
|
+
'xlsx': 'π',
|
1903
|
+
'ppt': 'π',
|
1904
|
+
'pptx': 'π',
|
1905
|
+
'txt': 'π',
|
1906
|
+
'zip': 'ποΈ',
|
1907
|
+
'rar': 'ποΈ',
|
1908
|
+
'7z': 'ποΈ',
|
1909
|
+
'mp3': 'π΅',
|
1910
|
+
'wav': 'π΅',
|
1911
|
+
'mp4': 'π₯',
|
1912
|
+
'avi': 'π₯',
|
1913
|
+
'jpg': 'πΌοΈ',
|
1914
|
+
'jpeg': 'πΌοΈ',
|
1915
|
+
'png': 'πΌοΈ',
|
1916
|
+
'gif': 'πΌοΈ'
|
1917
|
+
}
|
1918
|
+
|
1919
|
+
return iconMap[ext] || 'π'
|
1920
|
+
}
|
1921
|
+
|
1647
1922
|
function addInfoMessage(content) {
|
1648
1923
|
const messageDiv = document.createElement('div')
|
1649
1924
|
messageDiv.className = 'message incoming'
|
@@ -59,19 +59,19 @@ module FlowChat
|
|
59
59
|
|
60
60
|
# For USSD, we append the media URL to the message
|
61
61
|
media_text = case media_type.to_sym
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
75
|
|
76
76
|
# Combine message with media information
|
77
77
|
"#{message}\n\n#{media_text}"
|
data/lib/flow_chat/version.rb
CHANGED