feat: whatsapp-mcp-go

This commit is contained in:
iamatulsingh
2026-02-21 21:12:14 +01:00
commit dd5e347436
35 changed files with 4159 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
package helpers
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// ConvertToOpusOggTemp creates a temporary .ogg file in Opus format
// Uses reasonable defaults for WhatsApp voice messages
func ConvertToOpusOggTemp(inputFile string) (string, error) {
// WhatsApp voice messages usually use:
// bitrate → 24k32k is very common and good quality/size balance
// sample rate → 48000 Hz (Opus native)
const defaultBitrate = "32k"
const defaultSampleRate = 48000
return ConvertToOpusOggTempWithParams(inputFile, defaultBitrate, defaultSampleRate)
}
// ConvertToOpusOgg converts an audio file to Opus format in an Ogg container.
func ConvertToOpusOgg(inputFile string, outputFile string, bitrate string, sampleRate int) (string, error) {
// Check if input file exists
if _, err := os.Stat(inputFile); os.IsNotExist(err) {
return "", fmt.Errorf("input file not found: %s", inputFile)
}
// If no output file is specified, replace extension with .ogg
if outputFile == "" {
ext := filepath.Ext(inputFile)
outputFile = strings.TrimSuffix(inputFile, ext) + ".ogg"
}
// Ensure output directory exists
outputDir := filepath.Dir(outputFile)
if outputDir != "." {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return "", fmt.Errorf("failed to create output directory: %w", err)
}
}
// Build the ffmpeg command
cmd := exec.Command("ffmpeg",
"-i", inputFile,
"-c:a", "libopus",
"-b:a", bitrate,
"-ar", fmt.Sprintf("%d", sampleRate),
"-application", "voip",
"-vbr", "on",
"-compression_level", "10",
"-frame_duration", "60",
"-y",
outputFile,
)
// Run command and capture output for error reporting
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to convert audio. ffmpeg error: %s (%w)", string(output), err)
}
return outputFile, nil
}
// ConvertToOpusOggTempWithParams converts audio to a temporary .ogg file.
func ConvertToOpusOggTempWithParams(inputFile string, bitrate string, sampleRate int) (string, error) {
// Create a temporary file
tempFile, err := os.CreateTemp("", "audio-*.ogg")
if err != nil {
return "", fmt.Errorf("failed to create temp file: %w", err)
}
tempPath := tempFile.Name()
err = tempFile.Close()
if err != nil {
return "", err
} // Close it so ffmpeg can write to the path
// Convert the audio
result, err := ConvertToOpusOgg(inputFile, tempPath, bitrate, sampleRate)
if err != nil {
// Clean up on failure
err := os.Remove(tempPath)
if err != nil {
return "", err
}
return "", err
}
return result, nil
}
+554
View File
@@ -0,0 +1,554 @@
package helpers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"path/filepath"
"strings"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// InitMcpTool initializes MCP tool for the MCP server
func InitMcpTool() {
server := mcp.NewServer(&mcp.Implementation{
Name: "whatsapp-mcp",
Version: "v1.0.0",
}, nil)
mcp.AddTool[searchContactsInput, any](server, &mcp.Tool{
Name: "search_contacts",
Description: "Search WhatsApp contacts by name or phone number.",
}, searchContactsHandler)
mcp.AddTool[listMessagesInput, any](server, &mcp.Tool{
Name: "list_messages",
Description: "Get WhatsApp messages matching specified criteria with optional context around matches.",
}, listMessagesHandler)
mcp.AddTool[getMessageContextInput, any](server, &mcp.Tool{
Name: "get_message_context",
Description: "Get surrounding messages (context) around a specific WhatsApp message.",
}, getMessageContextHandler)
mcp.AddTool[listChatsInput, any](server, &mcp.Tool{
Name: "list_chats",
Description: "Get list of WhatsApp chats, optionally filtered and sorted.",
}, listChatsHandler)
mcp.AddTool[getChatInput, any](server, &mcp.Tool{
Name: "get_chat",
Description: "Get metadata of a specific WhatsApp chat by JID.",
}, getChatHandler)
mcp.AddTool[getDirectChatByContactInput, any](server, &mcp.Tool{
Name: "get_direct_chat_by_contact",
Description: "Get direct (1:1) chat metadata by contact phone number.",
}, getDirectChatByContactHandler)
mcp.AddTool[getContactChatsInput, any](server, &mcp.Tool{
Name: "get_contact_chats",
Description: "Get all chats involving a specific contact (JID).",
}, getContactChatsHandler)
mcp.AddTool[getLastInteractionInput, any](server, &mcp.Tool{
Name: "get_last_interaction",
Description: "Get most recent WhatsApp message involving the contact.",
}, getLastInteractionHandler)
mcp.AddTool[sendMessageInput, map[string]any](server, &mcp.Tool{
Name: "send_message",
Description: "Send a text message to a person or group on WhatsApp. For groups use the group JID.",
}, sendMessageHandler)
mcp.AddTool[sendFileInput, map[string]any](server, &mcp.Tool{
Name: "send_file",
Description: "Send image, video, document or any file via WhatsApp.",
}, sendFileHandler)
mcp.AddTool[sendAudioMessageInput, map[string]any](server, &mcp.Tool{
Name: "send_audio_message",
Description: "Send audio/voice message (converted to Opus .ogg if needed).",
}, sendAudioMessageHandler)
mcp.AddTool[downloadMediaInput, map[string]any](server, &mcp.Tool{
Name: "download_media",
Description: "Download media from a WhatsApp message and return local file path.",
}, downloadMediaHandler)
isSSE := strings.ToLower(ReadEnv("IS_SSE", "false")) == "true" ||
strings.ToLower(ReadEnv("IS_SSE", "0")) == "1"
ctx := context.Background()
if isSSE {
addr := ReadEnv("SSE_BASE_URL", "0.0.0.0:5777")
slog.Info("Starting WhatsApp MCP HTTP server (SSE)", "addr", addr)
handler := mcp.NewSSEHandler(func(request *http.Request) *mcp.Server {
// A logic can be added here to return different servers based on URL path
// For now, it returns the same WhatsApp server for the root/default path
return server
}, nil)
if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatalf("http server failed: %v", err)
}
} else {
slog.Info("Starting WhatsApp MCP server in stdio mode")
if err := server.Run(ctx, &mcp.StdioTransport{}); err != nil {
log.Fatalf("stdio failed: %v", err)
}
}
}
type searchContactsInput struct {
Query string `json:"query" mcp:"description:Search term to match against contact names or phone numbers"`
}
type listMessagesInput struct {
After *string `mcp:"description:ISO-8601 formatted string"`
Before *string `json:"before,omitempty" jsonschema:"description:ISO-8601 formatted string"`
SenderPhoneNumber *string `json:"sender_phone_number,omitempty"`
ChatJid *string `json:"chat_jid,omitempty"`
Query *string `json:"query,omitempty" jsonschema:"description:Search term in message content"`
Limit int `json:"limit" jsonschema:"default:20"`
Page int `json:"page" jsonschema:"default:0"`
IncludeContext bool `json:"include_context" jsonschema:"default:true"`
ContextBefore int `json:"context_before" jsonschema:"default:1"`
ContextAfter int `json:"context_after" jsonschema:"default:1"`
}
type getMessageContextInput struct {
MessageID string `json:"message_id" jsonschema:"description:The ID of the message"`
Before int `json:"before" jsonschema:"default:5"`
After int `json:"after" jsonschema:"default:5"`
}
type listChatsInput struct {
Query *string `json:"query,omitempty"`
Limit int `json:"limit" jsonschema:"default:20"`
Page int `json:"page" jsonschema:"default:0"`
IncludeLastMessage bool `json:"include_last_message" jsonschema:"default:true"`
SortBy string `json:"sort_by" jsonschema:"default:last_active,enum:last_active|name"`
}
type getChatInput struct {
ChatJid string `json:"chat_jid" jsonschema:"description:The JID of the chat"`
IncludeLastMessage bool `json:"include_last_message" jsonschema:"default:true"`
}
type getDirectChatByContactInput struct {
SenderPhoneNumber string `json:"sender_phone_number" jsonschema:"description:Phone number with country code"`
}
type getContactChatsInput struct {
Jid string `json:"jid"`
Limit int `json:"limit" jsonschema:"default:20"`
Page int `json:"page" jsonschema:"default:0"`
}
type getLastInteractionInput struct {
Jid string `json:"jid" jsonschema:"description:The contact's JID"`
}
type sendMessageInput struct {
Recipient string `json:"recipient" jsonschema:"description:Phone number (no +) or group JID like 123@g.us"`
Message string `json:"message"`
}
type sendFileInput struct {
Recipient string `json:"recipient"`
MediaPath string `json:"media_path" jsonschema:"description:Absolute path to the file"`
}
type sendAudioMessageInput struct {
Recipient string `json:"recipient"`
MediaPath string `json:"media_path" jsonschema:"description:Absolute path to audio file"`
}
type downloadMediaInput struct {
MessageID string `json:"message_id"`
ChatJid string `json:"chat_jid"`
}
func callAPI(method, path string, body any) ([]byte, error) {
var reqBody io.Reader
if body != nil {
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
reqBody = bytes.NewReader(b)
}
req, err := http.NewRequest(method, apiBaseURL+path, reqBody)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: apiTimeout}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("API error %d: %s", resp.StatusCode, string(data))
}
return data, nil
}
func sendMessageHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in sendMessageInput,
) (*mcp.CallToolResult, map[string]any, error) {
if in.Recipient == "" || in.Message == "" {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "recipient and message are required"},
},
}, map[string]any{
"success": false,
"error": "recipient and message are required",
}, nil
}
payload := map[string]any{
"recipient": in.Recipient,
"message": in.Message,
}
data, err := callAPI(http.MethodPost, "/send", payload)
if err != nil {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: err.Error()},
},
}, map[string]any{
"success": false,
"error": err.Error(),
}, nil
}
var resp map[string]any
if err := json.Unmarshal(data, &resp); err != nil {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "failed to parse API response"},
},
}, map[string]any{
"success": false,
"error": "failed to parse API response",
}, nil
}
return &mcp.CallToolResult{}, resp, nil
}
func searchContactsHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in searchContactsInput,
) (*mcp.CallToolResult, any, error) {
if in.Query == "" {
return ErrResult("query is required"), nil, nil
}
data, err := callAPI(http.MethodGet, "/contacts/search?q="+in.Query, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var result struct {
Contacts []map[string]any `json:"contacts"`
}
if err := json.Unmarshal(data, &result); err != nil {
return ErrResult("invalid response format"), nil, nil
}
return OkResult(result.Contacts), nil, nil
}
// list_messages (similar for all read/list tools that return slices/maps)
func listMessagesHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in listMessagesInput,
) (*mcp.CallToolResult, any, error) {
q := ""
if in.After != nil {
q += "&after=" + *in.After
}
if in.Before != nil {
q += "&before=" + *in.Before
}
if in.SenderPhoneNumber != nil {
q += "&sender=" + *in.SenderPhoneNumber
}
if in.ChatJid != nil {
q += "&chat=" + *in.ChatJid
}
if in.Query != nil {
q += "&search=" + *in.Query
}
q += fmt.Sprintf("&limit=%d&page=%d", in.Limit, in.Page)
if in.IncludeContext {
q += "&context=true"
}
data, err := callAPI(http.MethodGet, "/messages?"+strings.TrimPrefix(q, "&"), nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
// The /messages endpoint returns plain text → we return it as string
return OkResult(string(data)), nil, nil
}
// download_media (same idea)
func downloadMediaHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in downloadMediaInput,
) (*mcp.CallToolResult, map[string]any, error) {
path, err := DownloadMedia(in.MessageID, in.ChatJid)
if err != nil || path == "" {
msg := "failed to download media"
if err != nil {
msg += ": " + err.Error()
}
return &mcp.CallToolResult{}, map[string]any{
"success": false,
"message": msg,
}, nil
}
return &mcp.CallToolResult{}, map[string]any{
"success": true,
"message": "Media downloaded successfully",
"file_path": path,
}, nil
}
func getMessageContextHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getMessageContextInput,
) (*mcp.CallToolResult, any, error) {
if in.MessageID == "" {
return ErrResult("message_id is required"), nil, nil
}
path := fmt.Sprintf("/messages/context/%s?before=%d&after=%d",
in.MessageID, in.Before, in.After)
data, err := callAPI(http.MethodGet, path, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var ctxData map[string]any
if err := json.Unmarshal(data, &ctxData); err != nil {
return ErrResult("invalid context response"), nil, nil
}
return OkResult(ctxData), nil, nil
}
func listChatsHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in listChatsInput,
) (*mcp.CallToolResult, any, error) {
q := fmt.Sprintf("?limit=%d&page=%d", in.Limit, in.Page)
if in.Query != nil && *in.Query != "" {
q += "&q=" + *in.Query
}
if in.SortBy != "" {
q += "&sort=" + in.SortBy
}
data, err := callAPI(http.MethodGet, "/chats"+q, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var result struct {
Chats []map[string]any `json:"chats"`
Count int `json:"count"`
}
_ = json.Unmarshal(data, &result) // best effort
return OkResult(result.Chats), nil, nil
}
func getChatHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getChatInput,
) (*mcp.CallToolResult, any, error) {
if in.ChatJid == "" {
return ErrResult("chat_jid is required"), nil, nil
}
data, err := callAPI(http.MethodGet, "/chats/"+in.ChatJid, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var result map[string]any
if err := json.Unmarshal(data, &result); err != nil {
return ErrResult("invalid chat response"), nil, nil
}
return OkResult(result["chat"]), nil, nil
}
func getDirectChatByContactHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getDirectChatByContactInput,
) (*mcp.CallToolResult, any, error) {
if in.SenderPhoneNumber == "" {
return ErrResult("sender_phone_number is required"), nil, nil
}
// GET /api/direct-contacts/{phone}/chat
path := fmt.Sprintf("/direct-contacts/%s/chat", in.SenderPhoneNumber)
data, err := callAPI(http.MethodGet, path, nil)
if err != nil {
if strings.Contains(err.Error(), "404") {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "No direct (1:1) chat found for this phone number"},
},
}, nil, nil
}
return ErrResult(err.Error()), nil, nil
}
var result map[string]any
if err := json.Unmarshal(data, &result); err != nil {
return ErrResult("failed to parse chat response"), nil, nil
}
chat, ok := result["chat"]
if !ok {
return ErrResult("chat object missing in response"), nil, nil
}
return &mcp.CallToolResult{}, chat, nil
}
func getContactChatsHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getContactChatsInput,
) (*mcp.CallToolResult, any, error) {
if in.Jid == "" {
return ErrResult("jid is required"), nil, nil
}
limit := in.Limit
if limit <= 0 {
limit = 20
}
page := in.Page
if page < 0 {
page = 0
}
// GET /api/contacts/{jid}/chats?limit=...&page=...
path := fmt.Sprintf("/contacts/%s/chats?limit=%d&page=%d", in.Jid, limit, page)
data, err := callAPI(http.MethodGet, path, nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
var result struct {
Chats []map[string]any `json:"chats"`
Count int `json:"count"`
}
if err := json.Unmarshal(data, &result); err != nil {
return ErrResult("failed to parse chats response"), nil, nil
}
return &mcp.CallToolResult{}, result.Chats, nil
}
func getLastInteractionHandler(
ctx context.Context,
req *mcp.CallToolRequest,
in getLastInteractionInput,
) (*mcp.CallToolResult, any, error) {
if in.Jid == "" {
return ErrResult("jid is required"), nil, nil
}
// We simulate it by asking for 1 message from that sender
data, err := callAPI(http.MethodGet, "/messages?sender="+in.Jid+"&limit=1", nil)
if err != nil {
return ErrResult(err.Error()), nil, nil
}
return OkResult(string(data)), nil, nil
}
func sendFileHandler(ctx context.Context,
req *mcp.CallToolRequest,
in sendFileInput) (*mcp.CallToolResult, map[string]any, error) {
if in.MediaPath == "" {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{&mcp.TextContent{Text: "media_path is required"}},
}, nil, nil
}
absPath, err := filepath.Abs(in.MediaPath)
if err != nil {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{&mcp.TextContent{Text: fmt.Sprintf("invalid path: %v", err)}},
}, nil, nil
}
success, msg := SendFile(in.Recipient, absPath)
resultData := map[string]any{"success": success, "message": msg}
return &mcp.CallToolResult{IsError: !success}, resultData, nil
}
func sendAudioMessageHandler(ctx context.Context,
req *mcp.CallToolRequest,
in sendAudioMessageInput) (*mcp.CallToolResult, map[string]any, error) {
success, msg := SendAudioVoiceMessage(in.Recipient, in.MediaPath)
resultData := map[string]any{"success": success, "message": msg}
return &mcp.CallToolResult{IsError: !success}, resultData, nil
}
+51
View File
@@ -0,0 +1,51 @@
package helpers
import (
"encoding/json"
"os"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
var apiBaseURL = ReadEnv("API_BASE_URL", "http://192.168.178.119:30015/api")
const apiTimeout = 25 * time.Second
// OkResult return proper ok result for mcp tool
func OkResult(v any) *mcp.CallToolResult {
b, err := json.Marshal(v)
if err != nil {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: "Failed to encode JSON: " + err.Error()},
},
}
}
return &mcp.CallToolResult{
IsError: false,
Content: []mcp.Content{
&mcp.TextContent{Text: string(b)},
},
}
}
// ErrResult return error result for mcp tool
func ErrResult(msg string) *mcp.CallToolResult {
return &mcp.CallToolResult{
IsError: true,
Content: []mcp.Content{
&mcp.TextContent{Text: msg},
},
}
}
// ReadEnv read return value for an env
func ReadEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
+254
View File
@@ -0,0 +1,254 @@
package helpers
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
type Message struct {
Timestamp time.Time `json:"timestamp"`
Sender string `json:"sender"`
Content string `json:"content"`
IsFromMe bool `json:"is_from_me"`
ChatJID string `json:"chat_jid"`
ID string `json:"id"`
ChatName string `json:"chat_name,omitempty"`
MediaType string `json:"media_type,omitempty"`
}
type Chat struct {
JID string `json:"jid"`
Name string `json:"name,omitempty"`
LastMessageTime time.Time `json:"last_message_time,omitempty"`
LastMessage string `json:"last_message,omitempty"`
LastSender string `json:"last_sender,omitempty"`
LastIsFromMe bool `json:"last_is_from_me,omitempty"`
}
func (c *Chat) IsGroup() bool {
return strings.HasSuffix(c.JID, "@g.us")
}
type Contact struct {
PhoneNumber string `json:"phone_number"`
Name string `json:"name,omitempty"`
JID string `json:"jid"`
}
type MessageContext struct {
Message Message `json:"message"`
Before []Message `json:"before"`
After []Message `json:"after"`
}
type ListMessagesParams struct {
After, Before string
SenderPhoneNumber *string
ChatJid *string
Query *string
Limit, Page int
IncludeContext bool
ContextBefore int
ContextAfter int
}
func SendMessage(recipient, message string) (bool, string) {
if recipient == "" {
return false, "Recipient must be provided"
}
payload := map[string]string{
"recipient": recipient,
"message": message,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiBaseURL+"/send", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return false, "Request error: " + err.Error()
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return false, fmt.Sprintf("HTTP %d - %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, "Failed to parse response"
}
success, _ := result["success"].(bool)
msg, _ := result["message"].(string)
if msg == "" {
msg = "Unknown response"
}
return success, msg
}
func SendFile(recipient, mediaPath string) (bool, string) {
if recipient == "" {
return false, "Recipient must be provided"
}
if mediaPath == "" {
return false, "Media path must be provided"
}
if _, err := os.Stat(mediaPath); os.IsNotExist(err) {
return false, "Media file not found: " + mediaPath
}
payload := map[string]string{
"recipient": recipient,
"media_path": mediaPath,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiBaseURL+"/send", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return false, "Request error: " + err.Error()
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return false, fmt.Sprintf("HTTP %d - %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, "Failed to parse response"
}
success, _ := result["success"].(bool)
msg, _ := result["message"].(string)
if msg == "" {
msg = "Unknown response"
}
return success, msg
}
func SendAudioVoiceMessage(recipient, mediaPath string) (bool, string) {
if recipient == "" {
return false, "Recipient must be provided"
}
if mediaPath == "" {
return false, "Media path must be provided"
}
if _, err := os.Stat(mediaPath); os.IsNotExist(err) {
return false, "Media file not found: " + mediaPath
}
finalPath := mediaPath
if !strings.HasSuffix(strings.ToLower(mediaPath), ".ogg") {
converted, err := ConvertToOpusOggTemp(mediaPath)
if err != nil {
return false, "Audio conversion failed (ffmpeg required?): " + err.Error()
}
finalPath = converted
defer func(name string) {
err := os.Remove(name)
if err != nil {
}
}(finalPath)
}
payload := map[string]string{
"recipient": recipient,
"media_path": finalPath,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiBaseURL+"/send", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return false, "Request error: " + err.Error()
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return false, fmt.Sprintf("HTTP %d - %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return false, "Failed to parse response"
}
success, _ := result["success"].(bool)
msg, _ := result["message"].(string)
if msg == "" {
msg = "Unknown response"
}
return success, msg
}
func DownloadMedia(messageID, chatJID string) (string, error) {
payload := map[string]string{
"message_id": messageID,
"chat_jid": chatJID,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", apiBaseURL+"/download", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("HTTP %d - %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
if success, ok := result["success"].(bool); !ok || !success {
msg, _ := result["message"].(string)
if msg == "" {
msg = "Unknown error"
}
return "", errors.New(msg)
}
path, ok := result["path"].(string)
if !ok || path == "" {
return "", errors.New("no path returned")
}
log.Printf("Media downloaded: %s", path)
return path, nil
}