flow_chat 0.4.1 → 0.4.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.
- checksums.yaml +4 -4
- data/examples/initializer.rb +1 -1
- data/examples/media_prompts_examples.rb +1 -2
- data/examples/multi_tenant_whatsapp_controller.rb +56 -56
- data/examples/ussd_controller.rb +17 -11
- data/examples/whatsapp_controller.rb +10 -10
- data/examples/whatsapp_media_examples.rb +78 -80
- data/examples/whatsapp_message_job.rb +3 -3
- data/lib/flow_chat/base_processor.rb +1 -1
- data/lib/flow_chat/config.rb +4 -3
- data/lib/flow_chat/session/cache_session_store.rb +5 -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 +41 -45
- data/lib/flow_chat/whatsapp/configuration.rb +10 -10
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +8 -10
- 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 +7 -7
- metadata +1 -1
@@ -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
@@ -23,7 +23,7 @@ module FlowChat
|
|
23
23
|
end
|
24
24
|
|
25
25
|
# Send a text message
|
26
|
-
# @param to [String] Phone number in E.164 format
|
26
|
+
# @param to [String] Phone number in E.164 format
|
27
27
|
# @param text [String] Message text
|
28
28
|
# @return [Hash] API response or nil on error
|
29
29
|
def send_text(to, text)
|
@@ -36,7 +36,7 @@ module FlowChat
|
|
36
36
|
# @param buttons [Array] Array of button hashes with :id and :title
|
37
37
|
# @return [Hash] API response or nil on error
|
38
38
|
def send_buttons(to, text, buttons)
|
39
|
-
send_message(to, [:interactive_buttons, text, {
|
39
|
+
send_message(to, [:interactive_buttons, text, {buttons: buttons}])
|
40
40
|
end
|
41
41
|
|
42
42
|
# Send interactive list
|
@@ -46,7 +46,7 @@ module FlowChat
|
|
46
46
|
# @param button_text [String] Button text (default: "Choose")
|
47
47
|
# @return [Hash] API response or nil on error
|
48
48
|
def send_list(to, text, sections, button_text = "Choose")
|
49
|
-
send_message(to, [:interactive_list, text, {
|
49
|
+
send_message(to, [:interactive_list, text, {sections: sections, button_text: button_text}])
|
50
50
|
end
|
51
51
|
|
52
52
|
# Send a template message
|
@@ -56,10 +56,10 @@ module FlowChat
|
|
56
56
|
# @param language [String] Language code (default: "en_US")
|
57
57
|
# @return [Hash] API response or nil on error
|
58
58
|
def send_template(to, template_name, components = [], language = "en_US")
|
59
|
-
send_message(to, [:template, "", {
|
60
|
-
template_name: template_name,
|
61
|
-
components: components,
|
62
|
-
language: language
|
59
|
+
send_message(to, [:template, "", {
|
60
|
+
template_name: template_name,
|
61
|
+
components: components,
|
62
|
+
language: language
|
63
63
|
}])
|
64
64
|
end
|
65
65
|
|
@@ -121,12 +121,12 @@ module FlowChat
|
|
121
121
|
# @raise [StandardError] If upload fails
|
122
122
|
def upload_media(file_path_or_io, mime_type, filename = nil)
|
123
123
|
raise ArgumentError, "mime_type is required" if mime_type.nil? || mime_type.empty?
|
124
|
-
|
124
|
+
|
125
125
|
if file_path_or_io.is_a?(String)
|
126
126
|
# File path
|
127
127
|
raise ArgumentError, "File not found: #{file_path_or_io}" unless File.exist?(file_path_or_io)
|
128
128
|
filename ||= File.basename(file_path_or_io)
|
129
|
-
file = File.open(file_path_or_io,
|
129
|
+
file = File.open(file_path_or_io, "rb")
|
130
130
|
else
|
131
131
|
# IO object
|
132
132
|
file = file_path_or_io
|
@@ -140,24 +140,24 @@ module FlowChat
|
|
140
140
|
|
141
141
|
# Prepare multipart form data
|
142
142
|
boundary = "----WebKitFormBoundary#{SecureRandom.hex(16)}"
|
143
|
-
|
143
|
+
|
144
144
|
form_data = []
|
145
145
|
form_data << "--#{boundary}"
|
146
146
|
form_data << 'Content-Disposition: form-data; name="messaging_product"'
|
147
147
|
form_data << ""
|
148
148
|
form_data << "whatsapp"
|
149
|
-
|
149
|
+
|
150
150
|
form_data << "--#{boundary}"
|
151
151
|
form_data << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\""
|
152
152
|
form_data << "Content-Type: #{mime_type}"
|
153
153
|
form_data << ""
|
154
154
|
form_data << file.read
|
155
|
-
|
155
|
+
|
156
156
|
form_data << "--#{boundary}"
|
157
157
|
form_data << 'Content-Disposition: form-data; name="type"'
|
158
158
|
form_data << ""
|
159
159
|
form_data << mime_type
|
160
|
-
|
160
|
+
|
161
161
|
form_data << "--#{boundary}--"
|
162
162
|
|
163
163
|
body = form_data.join("\r\n")
|
@@ -168,14 +168,10 @@ module FlowChat
|
|
168
168
|
request.body = body
|
169
169
|
|
170
170
|
response = http.request(request)
|
171
|
-
|
171
|
+
|
172
172
|
if response.is_a?(Net::HTTPSuccess)
|
173
173
|
data = JSON.parse(response.body)
|
174
|
-
|
175
|
-
data['id']
|
176
|
-
else
|
177
|
-
raise StandardError, "Failed to upload media: #{data}"
|
178
|
-
end
|
174
|
+
data["id"] || raise(StandardError, "Failed to upload media: #{data}")
|
179
175
|
else
|
180
176
|
Rails.logger.error "WhatsApp Media Upload error: #{response.body}"
|
181
177
|
raise StandardError, "Media upload failed: #{response.body}"
|
@@ -195,7 +191,7 @@ module FlowChat
|
|
195
191
|
messaging_product: "whatsapp",
|
196
192
|
to: to,
|
197
193
|
type: "text",
|
198
|
-
text: {
|
194
|
+
text: {body: content}
|
199
195
|
}
|
200
196
|
when :interactive_buttons
|
201
197
|
{
|
@@ -204,7 +200,7 @@ module FlowChat
|
|
204
200
|
type: "interactive",
|
205
201
|
interactive: {
|
206
202
|
type: "button",
|
207
|
-
body: {
|
203
|
+
body: {text: content},
|
208
204
|
action: {
|
209
205
|
buttons: options[:buttons].map.with_index do |button, index|
|
210
206
|
{
|
@@ -225,7 +221,7 @@ module FlowChat
|
|
225
221
|
type: "interactive",
|
226
222
|
interactive: {
|
227
223
|
type: "list",
|
228
|
-
body: {
|
224
|
+
body: {text: content},
|
229
225
|
action: {
|
230
226
|
button: options[:button_text] || "Choose",
|
231
227
|
sections: options[:sections]
|
@@ -239,7 +235,7 @@ module FlowChat
|
|
239
235
|
type: "template",
|
240
236
|
template: {
|
241
237
|
name: options[:template_name],
|
242
|
-
language: {
|
238
|
+
language: {code: options[:language] || "en_US"},
|
243
239
|
components: options[:components] || []
|
244
240
|
}
|
245
241
|
}
|
@@ -284,7 +280,7 @@ module FlowChat
|
|
284
280
|
messaging_product: "whatsapp",
|
285
281
|
to: to,
|
286
282
|
type: "text",
|
287
|
-
text: {
|
283
|
+
text: {body: content.to_s}
|
288
284
|
}
|
289
285
|
end
|
290
286
|
end
|
@@ -301,7 +297,7 @@ module FlowChat
|
|
301
297
|
request["Authorization"] = "Bearer #{@config.access_token}"
|
302
298
|
|
303
299
|
response = http.request(request)
|
304
|
-
|
300
|
+
|
305
301
|
if response.is_a?(Net::HTTPSuccess)
|
306
302
|
data = JSON.parse(response.body)
|
307
303
|
data["url"]
|
@@ -326,7 +322,7 @@ module FlowChat
|
|
326
322
|
request["Authorization"] = "Bearer #{@config.access_token}"
|
327
323
|
|
328
324
|
response = http.request(request)
|
329
|
-
|
325
|
+
|
330
326
|
if response.is_a?(Net::HTTPSuccess)
|
331
327
|
response.body
|
332
328
|
else
|
@@ -337,15 +333,15 @@ module FlowChat
|
|
337
333
|
|
338
334
|
# Get MIME type from URL without downloading (HEAD request)
|
339
335
|
def get_media_mime_type(url)
|
340
|
-
require
|
341
|
-
|
336
|
+
require "net/http"
|
337
|
+
|
342
338
|
uri = URI(url)
|
343
339
|
http = Net::HTTP.new(uri.host, uri.port)
|
344
|
-
http.use_ssl = (uri.scheme ==
|
345
|
-
|
340
|
+
http.use_ssl = (uri.scheme == "https")
|
341
|
+
|
346
342
|
# Use HEAD request to get headers without downloading content
|
347
343
|
response = http.head(uri.path)
|
348
|
-
response[
|
344
|
+
response["content-type"]
|
349
345
|
rescue => e
|
350
346
|
Rails.logger.warn "Could not detect MIME type for #{url}: #{e.message}"
|
351
347
|
nil
|
@@ -358,7 +354,7 @@ module FlowChat
|
|
358
354
|
# @return [Hash] Media object for WhatsApp API
|
359
355
|
def build_media_object(options)
|
360
356
|
media_obj = {}
|
361
|
-
|
357
|
+
|
362
358
|
# Handle URL or ID
|
363
359
|
if options[:url]
|
364
360
|
# Use URL directly
|
@@ -367,17 +363,17 @@ module FlowChat
|
|
367
363
|
# Use provided media ID directly
|
368
364
|
media_obj[:id] = options[:id]
|
369
365
|
end
|
370
|
-
|
366
|
+
|
371
367
|
# Add optional fields
|
372
368
|
media_obj[:caption] = options[:caption] if options[:caption]
|
373
369
|
media_obj[:filename] = options[:filename] if options[:filename]
|
374
|
-
|
370
|
+
|
375
371
|
media_obj
|
376
372
|
end
|
377
373
|
|
378
374
|
# Check if input is a URL or file path/media ID
|
379
375
|
def url?(input)
|
380
|
-
input.to_s.start_with?(
|
376
|
+
input.to_s.start_with?("http://", "https://")
|
381
377
|
end
|
382
378
|
|
383
379
|
# Extract filename from URL
|
@@ -403,7 +399,7 @@ module FlowChat
|
|
403
399
|
request.body = message_data.to_json
|
404
400
|
|
405
401
|
response = http.request(request)
|
406
|
-
|
402
|
+
|
407
403
|
if response.is_a?(Net::HTTPSuccess)
|
408
404
|
JSON.parse(response.body)
|
409
405
|
else
|
@@ -414,21 +410,21 @@ module FlowChat
|
|
414
410
|
|
415
411
|
def send_media_message(to, media_type, url_or_id, caption: nil, filename: nil, mime_type: nil)
|
416
412
|
media_object = if url?(url_or_id)
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
413
|
+
{link: url_or_id}
|
414
|
+
else
|
415
|
+
{id: url_or_id}
|
416
|
+
end
|
421
417
|
|
422
418
|
# Add caption if provided (stickers don't support captions)
|
423
419
|
media_object[:caption] = caption if caption && media_type != :sticker
|
424
|
-
|
420
|
+
|
425
421
|
# Add filename for documents
|
426
422
|
media_object[:filename] = filename if filename && media_type == :document
|
427
423
|
|
428
424
|
message = {
|
429
|
-
messaging_product
|
430
|
-
to
|
431
|
-
type
|
425
|
+
:messaging_product => "whatsapp",
|
426
|
+
:to => to,
|
427
|
+
:type => media_type.to_s,
|
432
428
|
media_type.to_s => media_object
|
433
429
|
}
|
434
430
|
|
@@ -436,4 +432,4 @@ module FlowChat
|
|
436
432
|
end
|
437
433
|
end
|
438
434
|
end
|
439
|
-
end
|
435
|
+
end
|