-
-
Notifications
You must be signed in to change notification settings - Fork 392
Ultimos cambios #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
WSBOTIA
wants to merge
7
commits into
asternic:main
Choose a base branch
from
WSBOTIA:jules_wip_10972155698215758900
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Ultimos cambios #98
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
221a675
Jules was unable to complete the task in time. Please review the work…
google-labs-jules[bot] 27ba250
Jules was unable to complete the task in time. Please review the work…
google-labs-jules[bot] e1f927f
feat: Implement core incoming message synchronization from Wuzapi to …
google-labs-jules[bot] d5ddf9c
feat: Incoming text & media sync; Note multi-inbox requirement
google-labs-jules[bot] f005710
feat: Incoming sync for text/media; Notes on UI config
google-labs-jules[bot] 30e11ee
Okay, I've completed the incoming message synchronization flow, inclu…
google-labs-jules[bot] af3ea0b
Hey there!
google-labs-jules[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| WUZAPI_BASE_URL= | ||
| WUZAPI_API_KEY= | ||
| WUZAPI_INSTANCE_ID= | ||
| WUZAPI_WEBHOOK_URL_CHATWOOT= | ||
| CHATWOOT_BASE_URL= | ||
| CHATWOOT_ACCESS_TOKEN= | ||
| CHATWOOT_ACCOUNT_ID= | ||
| CHATWOOT_INBOX_ID= | ||
| WEBHOOK_SECRET= | ||
| REDIS_URL= | ||
| DATABASE_URL=./wuzapi_chatwoot.db | ||
| PORT=8080 | ||
| LOG_LEVEL=info | ||
| LOG_FORMAT=console | ||
| WUZAPI_WEBHOOK_PATH=/webhooks/wuzapi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # Placeholder Dockerfile |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Wuzapi-Chatwoot Integration | ||
|
|
||
| This project integrates Wuzapi with Chatwoot. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| // "fmt" // No longer needed | ||
| "os" | ||
|
|
||
| "github.com/joho/godotenv" | ||
| "github.com/rs/zerolog/log" // Use global logger | ||
| ) | ||
|
|
||
| // Config holds all configuration fields for the application. | ||
| type Config struct { | ||
| WuzapiBaseURL string | ||
| WuzapiAPIKey string | ||
| WuzapiInstanceID string | ||
| WuzapiWebhookURLChatwoot string | ||
| ChatwootBaseURL string | ||
| ChatwootAccessToken string | ||
| ChatwootAccountID string | ||
| ChatwootInboxID string | ||
| WebhookSecret string | ||
| RedisURL string | ||
| DatabaseURL string | ||
| Port string | ||
| LogLevel string | ||
| LogFormat string // Added to control log format (e.g., "console" or "json") | ||
| WuzapiWebhookPath string // Path for incoming Wuzapi webhooks | ||
| } | ||
|
|
||
| // LoadConfig loads configuration from environment variables. | ||
| // It attempts to load a .env file if present. | ||
| func LoadConfig() (*Config, error) { | ||
| // Attempt to load .env file, but don't fail if it's not present. | ||
| // Environment variables will take precedence. | ||
| err := godotenv.Load() | ||
| if err != nil { | ||
| log.Info().Err(err).Msg("No .env file found or error loading it, relying on environment variables") | ||
| } else { | ||
| log.Info().Msg("Loaded configuration from .env file (if present)") | ||
| } | ||
|
|
||
| log.Info().Msg("Loading configuration from environment variables...") | ||
|
|
||
| cfg := &Config{ | ||
| WuzapiBaseURL: os.Getenv("WUZAPI_BASE_URL"), | ||
| WuzapiAPIKey: os.Getenv("WUZAPI_API_KEY"), | ||
| WuzapiInstanceID: os.Getenv("WUZAPI_INSTANCE_ID"), | ||
| WuzapiWebhookURLChatwoot: os.Getenv("WUZAPI_WEBHOOK_URL_CHATWOOT"), | ||
| ChatwootBaseURL: os.Getenv("CHATWOOT_BASE_URL"), | ||
| ChatwootAccessToken: os.Getenv("CHATWOOT_ACCESS_TOKEN"), | ||
| ChatwootAccountID: os.Getenv("CHATWOOT_ACCOUNT_ID"), | ||
| ChatwootInboxID: os.Getenv("CHATWOOT_INBOX_ID"), | ||
| WebhookSecret: os.Getenv("WEBHOOK_SECRET"), | ||
| RedisURL: os.Getenv("REDIS_URL"), | ||
| DatabaseURL: os.Getenv("DATABASE_URL"), | ||
| Port: os.Getenv("PORT"), | ||
| LogLevel: os.Getenv("LOG_LEVEL"), | ||
| LogFormat: os.Getenv("LOG_FORMAT"), | ||
| WuzapiWebhookPath: os.Getenv("WUZAPI_WEBHOOK_PATH"), | ||
| } | ||
|
|
||
| if cfg.WuzapiWebhookPath == "" { | ||
| cfg.WuzapiWebhookPath = "/webhooks/wuzapi" // Default path | ||
| log.Info().Str("path", cfg.WuzapiWebhookPath).Msg("WUZAPI_WEBHOOK_PATH not set, using default") | ||
| } | ||
|
|
||
| // In a real application, you would validate these values. | ||
| // For debugging, you might log these, but be careful with sensitive data. | ||
| // Example: log.Debug().Str("wuzapi_base_url", cfg.WuzapiBaseURL).Msg("Config value") | ||
| // Omitting individual value logging here for brevity and security. | ||
|
|
||
| log.Info().Msg("Configuration loading attempt complete.") | ||
| return cfg, nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| module wuzapi-chatwoot-integration | ||
|
|
||
| go 1.23.1 | ||
|
|
||
| require ( | ||
| github.com/go-resty/resty/v2 v2.16.5 | ||
| github.com/joho/godotenv v1.5.1 | ||
| github.com/rs/zerolog v1.34.0 | ||
| gorm.io/driver/sqlite v1.6.0 | ||
| gorm.io/gorm v1.30.0 | ||
| ) | ||
|
|
||
| require ( | ||
| github.com/jinzhu/inflection v1.0.0 // indirect | ||
| github.com/jinzhu/now v1.1.5 // indirect | ||
| github.com/mattn/go-colorable v0.1.13 // indirect | ||
| github.com/mattn/go-isatty v0.0.19 // indirect | ||
| github.com/mattn/go-sqlite3 v1.14.22 // indirect | ||
| golang.org/x/net v0.33.0 // indirect | ||
| golang.org/x/sys v0.28.0 // indirect | ||
| golang.org/x/text v0.21.0 // indirect | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= | ||
| github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= | ||
| github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= | ||
| github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||
| github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||
| github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= | ||
| github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= | ||
| github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||
| github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= | ||
| github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= | ||
| github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= | ||
| github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= | ||
| github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= | ||
| github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= | ||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||
| github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= | ||
| github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= | ||
| github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= | ||
| golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= | ||
| golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= | ||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
| golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= | ||
| golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||
| golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= | ||
| golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= | ||
| golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= | ||
| golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | ||
| gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= | ||
| gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= | ||
| gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= | ||
| gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= |
224 changes: 224 additions & 0 deletions
224
wuzapi-chatwoot-integration/internal/adapters/chatwoot/client.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| package chatwoot | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "time" | ||
|
|
||
| "github.com/go-resty/resty/v2" | ||
| "github.com/rs/zerolog/log" | ||
| ) | ||
|
|
||
| // Client struct holds the configuration for the Chatwoot client. | ||
| type Client struct { | ||
| httpClient *resty.Client | ||
| baseURL string | ||
| accessToken string | ||
| accountID string | ||
| inboxID string // Keep inboxID if it's frequently used in requests, or pass as param | ||
| } | ||
|
|
||
| // NewClient creates a new Chatwoot client. | ||
| // The inboxID is included here for convenience if most operations target a specific inbox. | ||
| func NewClient(baseURL, accessToken, accountID, inboxID string) (*Client, error) { | ||
| if baseURL == "" { | ||
| return nil, fmt.Errorf("Chatwoot baseURL cannot be empty") | ||
| } | ||
| if accessToken == "" { | ||
| return nil, fmt.Errorf("Chatwoot accessToken cannot be empty") | ||
| } | ||
| if accountID == "" { | ||
| return nil, fmt.Errorf("Chatwoot accountID cannot be empty") | ||
| } | ||
| // inboxID might be optional at client level if methods will specify it | ||
| if inboxID == "" { | ||
| return nil, fmt.Errorf("Chatwoot inboxID cannot be empty for this client setup") | ||
| } | ||
|
|
||
| client := resty.New(). | ||
| SetBaseURL(baseURL). | ||
| SetHeader("api_access_token", accessToken). // Common header for Chatwoot | ||
| SetTimeout(10 * time.Second) | ||
|
|
||
| log.Info().Str("baseURL", baseURL).Str("accountID", accountID).Str("inboxID", inboxID).Msg("Chatwoot client configured") | ||
|
|
||
| return &Client{ | ||
| httpClient: client, | ||
| baseURL: baseURL, | ||
| accessToken: accessToken, | ||
| accountID: accountID, | ||
| inboxID: inboxID, | ||
| }, nil | ||
| } | ||
|
|
||
| // CreateContact creates a new contact in Chatwoot. | ||
| func (c *Client) CreateContact(payload ChatwootContactPayload) (*ChatwootContact, error) { | ||
| url := fmt.Sprintf("/api/v1/accounts/%s/contacts", c.accountID) | ||
|
|
||
| resp, err := c.httpClient.R(). | ||
| SetBody(payload). | ||
| SetResult(&ChatwootContact{}). // Expecting direct contact object, not nested like {"payload": {...}} | ||
| Post(url) | ||
|
|
||
| if err != nil { | ||
| log.Error().Err(err).Str("url", url).Interface("payload", payload).Msg("Chatwoot API: CreateContact request failed") | ||
| return nil, fmt.Errorf("Chatwoot API CreateContact request failed: %w", err) | ||
| } | ||
|
|
||
| if resp.IsError() { | ||
| log.Error().Str("url", url).Interface("payload", payload).Int("statusCode", resp.StatusCode()).Str("responseBody", string(resp.Body())).Msg("Chatwoot API: CreateContact returned an error") | ||
| return nil, fmt.Errorf("Chatwoot API CreateContact error: status %s, body: %s", resp.Status(), resp.String()) | ||
| } | ||
|
|
||
| contact := resp.Result().(*ChatwootContact) | ||
| log.Info().Int("contactID", contact.ID).Str("phoneNumber", contact.PhoneNumber).Msg("Successfully created Chatwoot contact") | ||
| return contact, nil | ||
| } | ||
|
|
||
| // GetContactByPhone searches for a contact by phone number. | ||
| // Note: Chatwoot's search is a general query 'q'. If 'phone_number' is not a unique indexed field for search, | ||
| // this might return multiple contacts if other fields match the number. | ||
| // For exact match on phone number, Chatwoot might require a filter if available, or this function needs to iterate. | ||
| func (c *Client) GetContactByPhone(phoneNumber string) (*ChatwootContact, error) { | ||
| url := fmt.Sprintf("/api/v1/accounts/%s/contacts/search", c.accountID) | ||
|
|
||
| var searchResult ChatwootContactSearchPayload // Expects {"payload": [...]} | ||
| resp, err := c.httpClient.R(). | ||
| SetQueryParam("q", phoneNumber). | ||
| SetResult(&searchResult). | ||
| Get(url) | ||
|
|
||
| if err != nil { | ||
| log.Error().Err(err).Str("url", url).Str("phoneNumber", phoneNumber).Msg("Chatwoot API: GetContactByPhone request failed") | ||
| return nil, fmt.Errorf("Chatwoot API GetContactByPhone request failed: %w", err) | ||
| } | ||
|
|
||
| if resp.IsError() { | ||
| log.Error().Str("url", url).Str("phoneNumber", phoneNumber).Int("statusCode", resp.StatusCode()).Str("responseBody", string(resp.Body())).Msg("Chatwoot API: GetContactByPhone returned an error") | ||
| return nil, fmt.Errorf("Chatwoot API GetContactByPhone error: status %s, body: %s", resp.Status(), resp.String()) | ||
| } | ||
|
|
||
| // Iterate through search results to find an exact match for the phone number. | ||
| // Chatwoot search can be broad. | ||
| for _, contact := range searchResult.Payload { | ||
| if contact.PhoneNumber == phoneNumber { | ||
| log.Info().Int("contactID", contact.ID).Str("phoneNumber", phoneNumber).Msg("Found Chatwoot contact by phone number") | ||
| return &contact, nil | ||
| } | ||
| } | ||
|
|
||
| log.Info().Str("phoneNumber", phoneNumber).Msg("No Chatwoot contact found with this exact phone number") | ||
| return nil, nil // Contact not found | ||
| } | ||
|
|
||
| // CreateConversation creates a new conversation in Chatwoot. | ||
| func (c *Client) CreateConversation(payload ChatwootConversationPayload) (*ChatwootConversation, error) { | ||
| url := fmt.Sprintf("/api/v1/accounts/%s/conversations", c.accountID) | ||
|
|
||
| resp, err := c.httpClient.R(). | ||
| SetBody(payload). | ||
| SetResult(&ChatwootConversation{}). // Expecting direct conversation object as response | ||
| Post(url) | ||
|
|
||
| if err != nil { | ||
| log.Error().Err(err).Str("url", url).Interface("payload", payload).Msg("Chatwoot API: CreateConversation request failed") | ||
| return nil, fmt.Errorf("Chatwoot API CreateConversation request failed: %w", err) | ||
| } | ||
|
|
||
| if resp.IsError() { | ||
| log.Error().Str("url", url).Interface("payload", payload).Int("statusCode", resp.StatusCode()).Str("responseBody", string(resp.Body())).Msg("Chatwoot API: CreateConversation returned an error") | ||
| return nil, fmt.Errorf("Chatwoot API CreateConversation error: status %s, body: %s", resp.Status(), resp.String()) | ||
| } | ||
|
|
||
| conversation := resp.Result().(*ChatwootConversation) | ||
| log.Info().Int("conversationID", conversation.ID).Int("contactID", payload.ContactID).Msg("Successfully created Chatwoot conversation") | ||
| return conversation, nil | ||
| } | ||
|
|
||
| // GetConversationsForContact retrieves conversations for a given contact ID. | ||
| func (c *Client) GetConversationsForContact(contactID int) ([]ChatwootConversation, error) { | ||
| url := fmt.Sprintf("/api/v1/accounts/%s/contacts/%d/conversations", c.accountID, contactID) | ||
|
|
||
| var responsePayload ChatwootContactConversationsResponse // Expects {"payload": [...]} | ||
| resp, err := c.httpClient.R(). | ||
| SetResult(&responsePayload). | ||
| Get(url) | ||
|
|
||
| if err != nil { | ||
| log.Error().Err(err).Str("url", url).Int("contactID", contactID).Msg("Chatwoot API: GetConversationsForContact request failed") | ||
| return nil, fmt.Errorf("Chatwoot API GetConversationsForContact request failed: %w", err) | ||
| } | ||
|
|
||
| if resp.IsError() { | ||
| log.Error().Str("url", url).Int("contactID", contactID).Int("statusCode", resp.StatusCode()).Str("responseBody", string(resp.Body())).Msg("Chatwoot API: GetConversationsForContact returned an error") | ||
| return nil, fmt.Errorf("Chatwoot API GetConversationsForContact error: status %s, body: %s", resp.Status(), resp.String()) | ||
| } | ||
|
|
||
| log.Info().Int("contactID", contactID).Int("conversationCount", len(responsePayload.Payload)).Msg("Successfully retrieved conversations for contact") | ||
| return responsePayload.Payload, nil | ||
| } | ||
|
|
||
| // CreateMessage sends a message to a Chatwoot conversation. | ||
| func (c *Client) CreateMessage(conversationID int, payload ChatwootMessagePayload) (*ChatwootMessage, error) { | ||
| url := fmt.Sprintf("/api/v1/accounts/%s/conversations/%d/messages", c.accountID, conversationID) | ||
|
|
||
| resp, err := c.httpClient.R(). | ||
| SetBody(payload). | ||
| SetResult(&ChatwootMessage{}). // Expecting ChatwootMessage as response | ||
| Post(url) | ||
|
|
||
| if err != nil { | ||
| log.Error().Err(err).Str("url", url).Interface("payload", payload).Msg("Chatwoot API: CreateMessage request failed") | ||
| return nil, fmt.Errorf("Chatwoot API CreateMessage request failed: %w", err) | ||
| } | ||
|
|
||
| if resp.IsError() { | ||
| // Log the full body for more context on API errors | ||
| log.Error().Str("url", url).Interface("payload", payload).Int("statusCode", resp.StatusCode()).Str("responseBody", string(resp.Body())).Msg("Chatwoot API: CreateMessage returned an error") | ||
| return nil, fmt.Errorf("Chatwoot API CreateMessage error: status %s, body: %s", resp.Status(), resp.String()) | ||
| } | ||
|
|
||
| message := resp.Result().(*ChatwootMessage) | ||
| log.Info().Int("messageID", message.ID).Int("conversationID", conversationID).Msg("Successfully created Chatwoot message") | ||
| return message, nil | ||
| } | ||
|
|
||
| // UploadFile uploads a file to Chatwoot's generic upload endpoint. | ||
| // Chatwoot typically expects attachments to be uploaded first, and then their IDs are passed when creating a message. | ||
| // The exact endpoint for general file uploads might be /api/v1/accounts/{account_id}/upload | ||
| // The response should contain an ID for the uploaded attachment. | ||
| func (c *Client) UploadFile(fileData []byte, fileName string, contentType string) (*ChatwootAttachment, error) { | ||
| // Note: The 'contentType' parameter might not be explicitly needed by SetFileBytes, | ||
| // as Resty might infer it or Chatwoot might determine it server-side. | ||
| // However, it's good practice to have it if the server requires a specific form field for it. | ||
|
|
||
| // Using a common endpoint pattern, adjust if Chatwoot's specific endpoint is different. | ||
| // The direct upload endpoint might not be tied to a conversation yet. | ||
| url := fmt.Sprintf("/api/v1/accounts/%s/upload", c.accountID) | ||
|
|
||
| // Chatwoot expects the file as 'attachment' or 'attachments[]' in multipart form. | ||
| // Let's assume 'attachment' for a single file upload. | ||
| resp, err := c.httpClient.R(). | ||
| SetFileBytes("attachment", fileName, fileData). // "attachment" is the form field name, fileName is the reported filename | ||
| // SetHeader("Content-Type", "multipart/form-data"). // Resty usually sets this automatically for SetFile/SetFileReader/SetFileBytes | ||
| SetResult(&ChatwootAttachment{}). // Expecting ChatwootAttachment as response | ||
| Post(url) | ||
|
|
||
| if err != nil { | ||
| log.Error().Err(err).Str("url", url).Str("fileName", fileName).Msg("Chatwoot API: UploadFile request failed") | ||
| return nil, fmt.Errorf("Chatwoot API UploadFile request failed for %s: %w", fileName, err) | ||
| } | ||
|
|
||
| if resp.IsError() { | ||
| log.Error().Str("url", url).Str("fileName", fileName).Int("statusCode", resp.StatusCode()).Str("responseBody", string(resp.Body())).Msg("Chatwoot API: UploadFile returned an error") | ||
| return nil, fmt.Errorf("Chatwoot API UploadFile error for %s: status %s, body: %s", fileName, resp.Status(), resp.String()) | ||
| } | ||
|
|
||
| attachment := resp.Result().(*ChatwootAttachment) | ||
| if attachment.ID == 0 { | ||
| log.Error().Str("fileName", fileName).Interface("response", attachment).Msg("Chatwoot API: UploadFile response did not contain a valid attachment ID") | ||
| return nil, fmt.Errorf("Chatwoot API UploadFile for %s returned no ID", fileName) | ||
| } | ||
|
|
||
| log.Info().Int("attachmentID", attachment.ID).Str("fileName", fileName).Str("dataURL", attachment.DataURL).Msg("Successfully uploaded file to Chatwoot") | ||
| return attachment, nil | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding validation for required environment variables and returning an error from
LoadConfigif they are not found. This would make startup failures more explicit.