Skip to content

Commit

Permalink
improve: send attachments
Browse files Browse the repository at this point in the history
Add filetype for guessing attachments mime types, eliminating the need of manually specifiy them
Attachments are supported in all flavors: url, path, b64, buffer
Add feat: end audio support
  • Loading branch information
alejamp committed Sep 22, 2024
1 parent 7a8ea35 commit e498f0f
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 110 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ Run your Cel.ai assistant, then a new Chatwoot bot called "Bot Name" will be cre
|---------------------|:-------:|:-----:|
| **Text** |||
| **Image** |||
| **Audio** || |
| **Audio** || |
| **Files** |||
| **Custom Attributes** || |
| **Custom Attributes** || |
| **Video** |||
| **Location** |||
| **Buttons** | - | |
| **Buttons** | - | |
| **Templates** | - ||

## Roadmap
Expand Down
244 changes: 174 additions & 70 deletions celai_chatwoot/connector/msg_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import asyncio
import base64
from dataclasses import dataclass
import json
import aiohttp
from typing import Any, Optional, Dict
from loguru import logger as log
import filetype
import os

ChatwootMessageTypes = ["incoming", "outgoing"]

@dataclass
class ChatwootAttachment:
content: Any
type: str = None
fileName: Optional[str] = None
fileUrl: Optional[str] = None


class ChatwootMessages:

Expand All @@ -23,6 +34,62 @@ def __init__(self,
})


async def __build_content(self, attach: ChatwootAttachment):
if attach.type == "image":
return await self.__build_content_image(attach.content)
elif attach.type.startswith("audio"):
return await self.__build_content_audio(attach.content)

raise NotImplementedError(f"ChatwootClient: Unknown attachment type: {attach.type}")

async def __build_content_image(self, content: Any):

b64_img = None
# if image is a file path, read the file
# -------------------------------------------------------------
if isinstance(content, str) and os.path.exists(content):
with open(content, "rb") as f:
b64_img = base64.b64encode(f.read()).decode()
elif isinstance(content, bytes):
b64_img = base64.b64encode(content).decode()
elif isinstance(content, str):
if content.startswith("data:image"):
b64_img = content.split("base64,")[1]
if content.startswith("http"):
# download the image
async with aiohttp.ClientSession() as session:
async with session.get(content) as resp:
b64_img = base64.b64encode(await resp.read()).decode()
if len(content) > 100:
b64_img = content
else:
raise ValueError("image must be a url/path to a file, a bytes object or a base64 string")

return b64_img

async def __build_content_audio(self, content: str):
if isinstance(content, str) and os.path.exists(content):
with open(content, "rb") as f:
b64_audio = base64.b64encode(f.read()).decode()
elif isinstance(content, bytes):
b64_audio = base64.b64encode(content).decode()
elif isinstance(content, str):
if content.startswith("data:audio"):
b64_audio = content.split("base64,")[1]
if content.startswith("http"):
# download the audio
async with aiohttp.ClientSession() as session:
async with session.get(content) as resp:
b64_audio = base64.b64encode(await resp.read()).decode()

if len(content) > 100:
b64_audio = content
else:
raise ValueError("audio must be a url/path to a file, a bytes object or a base64 string")

return b64_audio


# -------------------------------------------------------------
async def send_text_message(
self,
Expand Down Expand Up @@ -61,15 +128,10 @@ async def send_text_message(
async with session.post(url, json=payload, headers=headers) as response:
response_data = await response.json()
return response_data



# -------------------------------------------------------------
async def send_image_message(self,
conversation_id,
attach=None,
text=None,
is_private=False,
content_attributes=None):
async def send_attachment(self, conversation_id, attach: ChatwootAttachment=None, text=None, is_private=False, content_attributes=None):
assert isinstance(attach, ChatwootAttachment), "attach must be an instance of ChatwootAttachment"

# Construct the URL
url = f"{self.base_url}/api/v1/accounts/{self.account_id}/conversations/{conversation_id}/messages"
Expand All @@ -87,50 +149,21 @@ async def send_image_message(self,
form.add_field("content", text)

# Append the content attributes if provided
if content_attributes and 'items' in content_attributes:
form.add_field("content_attributes", json.dumps(content_attributes or {}))
form.add_field("content_type", "input_select")

# Handle attachment based on type
if attach and attach.get('type') == 'url':
if not attach.get('fileUrl'):
raise ValueError("Missing fileUrl")

async with aiohttp.ClientSession() as session:
async with session.get(attach['fileUrl']) as response:
if response.status != 200:
raise ValueError("Failed to fetch the file from URL")
image_name = os.path.basename(attach['fileUrl'])
form.add_field("attachments[]",
await response.read(),
filename=image_name,
content_type="image")

elif attach and attach.get('type') == 'b64':
if not attach.get('content'):
raise ValueError("Missing content")
if not attach.get('fileName'):
raise ValueError("Missing fileName")

# Decode base64 content
buffer = base64.b64decode(attach['content'])
form.add_field("attachments[]",
buffer,
filename=attach['fileName'],
content_type="image")
# if content_attributes and 'items' in content_attributes:
# form.add_field("content_attributes", json.dumps(content_attributes or {}))
# form.add_field("content_type", "input_select")

else:
raise ValueError("Unknown attachment type")

# Set up headers
headers = {
"api_access_token": f"{self.access_key}"
}
content = await self.__build_content(attach)
buffer = base64.b64decode(content)
file_type = filetype.guess(buffer)
form.add_field("attachments[]", buffer, filename=attach.fileName or "audio.ogg", content_type=file_type.mime)


# Make the HTTP request
async with aiohttp.ClientSession() as session:
try:
async with session.post(url, data=form, headers=headers) as response:
async with session.post(url, data=form, headers=self.headers) as response:
res = await response.json()
print(res)
return res
Expand All @@ -140,40 +173,111 @@ async def send_image_message(self,





if __name__ == "__main__":
import asyncio

import os
# load ogg file
audio_content = None
path = 'tests/data/sample.ogg'
# load file content
with open(path, 'rb') as f:
audio_content = f.read()



b64_img = "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII="
conversation_id = 36

async def main():
client = ChatwootMessages(base_url=os.environ.get("CHATWOOT_URL"),
account_id=os.environ.get("CHATWOOT_ACCOUNT_ID"),
access_key=os.environ.get("CHATWOOT_ACCESS_KEY"))

async def send_image1():
attach = ChatwootAttachment(type="image", content="https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg")
await client.send_attachment(conversation_id=conversation_id, attach=attach, text="Hello", is_private=False)

client = ChatwootMessages(os.environ.get("CHATWOOT_URL"),
os.environ.get("CHATWOOT_ACCOUNT_ID"),
os.environ.get("CHATWOOT_ACCESS_KEY"))

res = await client.send_text_message(conversation_id=34,
content="Hello, world!",
message_type="outgoing",
private=False)


attach = {
"type": "url",
"fileUrl": "https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg"
async def send_image_ba64():
attach = ChatwootAttachment(type="image", content=b64_img, fileName="example.png")
await client.send_attachment(conversation_id=conversation_id, attach=attach, is_private=False)

async def send_audio():
attach = ChatwootAttachment(type="audio",
content=audio_content,
fileName="sample.ogg")
await client.send_attachment(conversation_id=conversation_id, attach=attach, is_private=False)

async def send_input_select():
content_type = "input_select"
content_attributes = {
"items": [
{ "title": "Option1", "value": "Option 1" },
{ "title": "Option2", "value": "Option 2" }
]
}
await client.send_image_message(conversation_id=35, attach=attach, text="Hello", is_private=False)


attach = {
"type": "b64",
"content": b64_img,
"fileName": "example.png"
res = await client.send_text_message(conversation_id=conversation_id,
content="Hello, world!",
message_type="outgoing",
private=False,
content_attributes=content_attributes,
content_type=content_type)

async def send_article():
content_type = "article"

content_attributes = {
"items": [
{ "title": "API start guide", "description": "A random start api guide", "link": "http://google.com" },
{ "title": "Development docs", "description": "Development docs and guidelines", "link": "http://google.com" }
]
}
res = await client.send_text_message(conversation_id=conversation_id,
content="Hello, world!",
message_type="outgoing",
private=False,
content_attributes=content_attributes,
content_type=content_type)

async def send_cards():
content_type = "cards"
content_attributes = {
"items":[
{
"media_url":"https://assets.ajio.com/medias/sys_master/root/hdb/h9a/13582024212510/-1117Wx1400H-460345219-white-MODEL.jpg",
"title":"Nike Shoes 2.0",
"description":"Running with Nike Shoe 2.0",
"actions":[
{
"type":"link",
"text":"View More",
"uri":"google.com"
},
{
"type":"postback",
"text":"Add to cart",
"payload":"ITEM_SELECTED"
}
]
}
]
}
await client.send_image_message(conversation_id=35, attach=attach, is_private=False)


print(res)
res = await client.send_text_message(conversation_id=conversation_id,
content="Hello, world!",
message_type="outgoing",
private=False,
content_attributes=content_attributes,
content_type=content_type)


# Example usage
async def main():
await send_image1()
await send_image_ba64()
await send_audio()
pass

# Run the example
asyncio.run(main())
Loading

0 comments on commit e498f0f

Please sign in to comment.