package main import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "os" "os/signal" "reflect" "strings" "syscall" "time" _ "github.com/mattn/go-sqlite3" "github.com/mdp/qrterminal" "go.mau.fi/whatsmeow" waProto "go.mau.fi/whatsmeow/binary/proto" "go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" "google.golang.org/protobuf/proto" ) // Message represents a chat message for our client type Message struct { Time time.Time Sender string Content string IsFromMe bool } // Database handler for storing message history type MessageStore struct { db *sql.DB } // Initialize message store func NewMessageStore() (*MessageStore, error) { // Create directory for database if it doesn't exist if err := os.MkdirAll("store", 0755); err != nil { return nil, fmt.Errorf("failed to create store directory: %v", err) } // Open SQLite database for messages db, err := sql.Open("sqlite3", "file:store/messages.db?_foreign_keys=on") if err != nil { return nil, fmt.Errorf("failed to open message database: %v", err) } // Create tables if they don't exist _, err = db.Exec(` CREATE TABLE IF NOT EXISTS chats ( jid TEXT PRIMARY KEY, name TEXT, last_message_time TIMESTAMP ); CREATE TABLE IF NOT EXISTS messages ( id TEXT, chat_jid TEXT, sender TEXT, content TEXT, timestamp TIMESTAMP, is_from_me BOOLEAN, PRIMARY KEY (id, chat_jid), FOREIGN KEY (chat_jid) REFERENCES chats(jid) ); `) if err != nil { db.Close() return nil, fmt.Errorf("failed to create tables: %v", err) } return &MessageStore{db: db}, nil } // Close the database connection func (store *MessageStore) Close() error { return store.db.Close() } // Store a chat in the database func (store *MessageStore) StoreChat(jid, name string, lastMessageTime time.Time) error { _, err := store.db.Exec( "INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)", jid, name, lastMessageTime, ) return err } // Store a message in the database func (store *MessageStore) StoreMessage(id, chatJID, sender, content string, timestamp time.Time, isFromMe bool) error { // Only store if there's actual content if content == "" { return nil } _, err := store.db.Exec( "INSERT OR REPLACE INTO messages (id, chat_jid, sender, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?)", id, chatJID, sender, content, timestamp, isFromMe, ) return err } // Get messages from a chat func (store *MessageStore) GetMessages(chatJID string, limit int) ([]Message, error) { rows, err := store.db.Query( "SELECT sender, content, timestamp, is_from_me FROM messages WHERE chat_jid = ? ORDER BY timestamp DESC LIMIT ?", chatJID, limit, ) if err != nil { return nil, err } defer rows.Close() var messages []Message for rows.Next() { var msg Message var timestamp time.Time err := rows.Scan(&msg.Sender, &msg.Content, ×tamp, &msg.IsFromMe) if err != nil { return nil, err } msg.Time = timestamp messages = append(messages, msg) } return messages, nil } // Get all chats func (store *MessageStore) GetChats() (map[string]time.Time, error) { rows, err := store.db.Query("SELECT jid, last_message_time FROM chats ORDER BY last_message_time DESC") if err != nil { return nil, err } defer rows.Close() chats := make(map[string]time.Time) for rows.Next() { var jid string var lastMessageTime time.Time err := rows.Scan(&jid, &lastMessageTime) if err != nil { return nil, err } chats[jid] = lastMessageTime } return chats, nil } // Extract text content from a message func extractTextContent(msg *waProto.Message) string { if msg == nil { return "" } // Try to get text content if text := msg.GetConversation(); text != "" { return text } else if extendedText := msg.GetExtendedTextMessage(); extendedText != nil { return extendedText.GetText() } // For now, we're ignoring non-text messages return "" } // SendMessageResponse represents the response for the send message API type SendMessageResponse struct { Success bool `json:"success"` Message string `json:"message"` } // SendMessageRequest represents the request body for the send message API type SendMessageRequest struct { Recipient string `json:"recipient"` Message string `json:"message"` } // Function to send a WhatsApp message func sendWhatsAppMessage(client *whatsmeow.Client, recipient string, message string) (bool, string) { if !client.IsConnected() { return false, "Not connected to WhatsApp" } // Create JID for recipient var recipientJID types.JID var err error // Check if recipient is a JID isJID := strings.Contains(recipient, "@") if isJID { // Parse the JID string recipientJID, err = types.ParseJID(recipient) if err != nil { return false, fmt.Sprintf("Error parsing JID: %v", err) } } else { // Create JID from phone number recipientJID = types.JID{ User: recipient, Server: "s.whatsapp.net", // For personal chats } } // Send the message _, err = client.SendMessage(context.Background(), recipientJID, &waProto.Message{ Conversation: proto.String(message), }) if err != nil { return false, fmt.Sprintf("Error sending message: %v", err) } return true, fmt.Sprintf("Message sent to %s", recipient) } // Start a REST API server to expose the WhatsApp client functionality func startRESTServer(client *whatsmeow.Client, port int) { // Handler for sending messages http.HandleFunc("/api/send", func(w http.ResponseWriter, r *http.Request) { // Only allow POST requests fmt.Println("Received request to send message") if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse the request body var req SendMessageRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request format", http.StatusBadRequest) return } // Validate request if req.Recipient == "" || req.Message == "" { http.Error(w, "Recipient and message are required", http.StatusBadRequest) return } // Send the message success, message := sendWhatsAppMessage(client, req.Recipient, req.Message) fmt.Println("Message sent", success, message) // Set response headers w.Header().Set("Content-Type", "application/json") // Set appropriate status code if !success { w.WriteHeader(http.StatusInternalServerError) } // Send response json.NewEncoder(w).Encode(SendMessageResponse{ Success: success, Message: message, }) }) // Start the server serverAddr := fmt.Sprintf(":%d", port) fmt.Printf("Starting REST API server on %s...\n", serverAddr) // Run server in a goroutine so it doesn't block go func() { if err := http.ListenAndServe(serverAddr, nil); err != nil { fmt.Printf("REST API server error: %v\n", err) } }() } func main() { // Set up logger logger := waLog.Stdout("Client", "INFO", true) logger.Infof("Starting WhatsApp client...") // Create database connection for storing session data dbLog := waLog.Stdout("Database", "INFO", true) // Create directory for database if it doesn't exist if err := os.MkdirAll("store", 0755); err != nil { logger.Errorf("Failed to create store directory: %v", err) return } container, err := sqlstore.New("sqlite3", "file:store/whatsapp.db?_foreign_keys=on", dbLog) if err != nil { logger.Errorf("Failed to connect to database: %v", err) return } // Get device store - This contains session information deviceStore, err := container.GetFirstDevice() if err != nil { if err == sql.ErrNoRows { // No device exists, create one deviceStore = container.NewDevice() logger.Infof("Created new device") } else { logger.Errorf("Failed to get device: %v", err) return } } // Create client instance client := whatsmeow.NewClient(deviceStore, logger) if client == nil { logger.Errorf("Failed to create WhatsApp client") return } // Initialize message store messageStore, err := NewMessageStore() if err != nil { logger.Errorf("Failed to initialize message store: %v", err) return } defer messageStore.Close() // Setup event handling for messages and history sync client.AddEventHandler(func(evt interface{}) { switch v := evt.(type) { case *events.Message: // Process regular messages handleMessage(client, messageStore, v, logger) case *events.HistorySync: // Process history sync events handleHistorySync(client, messageStore, v, logger) case *events.Connected: logger.Infof("Connected to WhatsApp") case *events.LoggedOut: logger.Warnf("Device logged out, please scan QR code to log in again") } }) // Create channel to track connection success connected := make(chan bool, 1) // Connect to WhatsApp if client.Store.ID == nil { // No ID stored, this is a new client, need to pair with phone qrChan, _ := client.GetQRChannel(context.Background()) err = client.Connect() if err != nil { logger.Errorf("Failed to connect: %v", err) return } // Print QR code for pairing with phone for evt := range qrChan { if evt.Event == "code" { fmt.Println("\nScan this QR code with your WhatsApp app:") qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) } else if evt.Event == "success" { connected <- true break } } // Wait for connection select { case <-connected: fmt.Println("\nSuccessfully connected and authenticated!") case <-time.After(3 * time.Minute): logger.Errorf("Timeout waiting for QR code scan") return } } else { // Already logged in, just connect err = client.Connect() if err != nil { logger.Errorf("Failed to connect: %v", err) return } connected <- true } // Wait a moment for connection to stabilize time.Sleep(2 * time.Second) if !client.IsConnected() { logger.Errorf("Failed to establish stable connection") return } fmt.Println("\nāœ“ Connected to WhatsApp! Type 'help' for commands.") // Start REST API server startRESTServer(client, 8080) // Create a channel to keep the main goroutine alive exitChan := make(chan os.Signal, 1) signal.Notify(exitChan, syscall.SIGINT, syscall.SIGTERM) fmt.Println("REST server is running. Press Ctrl+C to disconnect and exit.") // Wait for termination signal <-exitChan fmt.Println("Disconnecting...") // Disconnect client client.Disconnect() } // GetChatName determines the appropriate name for a chat based on JID and other info func GetChatName(client *whatsmeow.Client, messageStore *MessageStore, jid types.JID, chatJID string, conversation interface{}, sender string, logger waLog.Logger) string { // First, check if chat already exists in database with a name var existingName string err := messageStore.db.QueryRow("SELECT name FROM chats WHERE jid = ?", chatJID).Scan(&existingName) if err == nil && existingName != "" { // Chat exists with a name, use that logger.Infof("Using existing chat name for %s: %s", chatJID, existingName) return existingName } // Need to determine chat name var name string if jid.Server == "g.us" { // This is a group chat logger.Infof("Getting name for group: %s", chatJID) // Use conversation data if provided (from history sync) if conversation != nil { // Extract name from conversation if available // This uses type assertions to handle different possible types var displayName, convName *string // Try to extract the fields we care about regardless of the exact type v := reflect.ValueOf(conversation) if v.Kind() == reflect.Ptr && !v.IsNil() { v = v.Elem() // Try to find DisplayName field if displayNameField := v.FieldByName("DisplayName"); displayNameField.IsValid() && displayNameField.Kind() == reflect.Ptr && !displayNameField.IsNil() { dn := displayNameField.Elem().String() displayName = &dn } // Try to find Name field if nameField := v.FieldByName("Name"); nameField.IsValid() && nameField.Kind() == reflect.Ptr && !nameField.IsNil() { n := nameField.Elem().String() convName = &n } } // Use the name we found if displayName != nil && *displayName != "" { name = *displayName } else if convName != nil && *convName != "" { name = *convName } } // If we didn't get a name, try group info if name == "" { groupInfo, err := client.GetGroupInfo(jid) if err == nil && groupInfo.Name != "" { name = groupInfo.Name } else { // Fallback name for groups name = fmt.Sprintf("Group %s", jid.User) } } logger.Infof("Using group name: %s", name) } else { // This is an individual contact logger.Infof("Getting name for contact: %s", chatJID) // Just use contact info (full name) contact, err := client.Store.Contacts.GetContact(jid) if err == nil && contact.FullName != "" { name = contact.FullName } else if sender != "" { // Fallback to sender name = sender } else { // Last fallback to JID name = jid.User } logger.Infof("Using contact name: %s", name) } return name } // Handle regular incoming messages func handleMessage(client *whatsmeow.Client, messageStore *MessageStore, msg *events.Message, logger waLog.Logger) { // Extract text content content := extractTextContent(msg.Message) if content == "" { return // Skip non-text messages } // Save message to database chatJID := msg.Info.Chat.String() sender := msg.Info.Sender.User // Get appropriate chat name (pass nil for conversation since we don't have one for regular messages) name := GetChatName(client, messageStore, msg.Info.Chat, chatJID, nil, sender, logger) // Update chat in database with the message timestamp (keeps last message time updated) err := messageStore.StoreChat(chatJID, name, msg.Info.Timestamp) if err != nil { logger.Warnf("Failed to store chat: %v", err) } // Store message in database err = messageStore.StoreMessage( msg.Info.ID, chatJID, sender, content, msg.Info.Timestamp, msg.Info.IsFromMe, ) if err != nil { logger.Warnf("Failed to store message: %v", err) } else { // Log message reception timestamp := msg.Info.Timestamp.Format("2006-01-02 15:04:05") direction := "←" if msg.Info.IsFromMe { direction = "→" } fmt.Printf("[%s] %s %s: %s\n", timestamp, direction, sender, content) } } // Handle history sync events func handleHistorySync(client *whatsmeow.Client, messageStore *MessageStore, historySync *events.HistorySync, logger waLog.Logger) { fmt.Printf("Received history sync event with %d conversations\n", len(historySync.Data.Conversations)) syncedCount := 0 for _, conversation := range historySync.Data.Conversations { // Parse JID from the conversation if conversation.ID == nil { continue } chatJID := *conversation.ID // Try to parse the JID jid, err := types.ParseJID(chatJID) if err != nil { logger.Warnf("Failed to parse JID %s: %v", chatJID, err) continue } // Get appropriate chat name by passing the history sync conversation directly name := GetChatName(client, messageStore, jid, chatJID, conversation, "", logger) // Process messages messages := conversation.Messages if len(messages) > 0 { // Update chat with latest message timestamp latestMsg := messages[0] if latestMsg == nil || latestMsg.Message == nil { continue } // Get timestamp from message info timestamp := time.Time{} if ts := latestMsg.Message.GetMessageTimestamp(); ts != 0 { timestamp = time.Unix(int64(ts), 0) } else { continue } messageStore.StoreChat(chatJID, name, timestamp) // Store messages for _, msg := range messages { if msg == nil || msg.Message == nil { continue } // Extract text content var content string if msg.Message.Message != nil { if conv := msg.Message.Message.GetConversation(); conv != "" { content = conv } else if ext := msg.Message.Message.GetExtendedTextMessage(); ext != nil { content = ext.GetText() } } // Log the message content for debugging logger.Infof("Message content: %v", content) // Skip non-text messages if content == "" { continue } // Determine sender var sender string isFromMe := false if msg.Message.Key != nil { if msg.Message.Key.FromMe != nil { isFromMe = *msg.Message.Key.FromMe } if !isFromMe && msg.Message.Key.Participant != nil && *msg.Message.Key.Participant != "" { sender = *msg.Message.Key.Participant } else if isFromMe { sender = client.Store.ID.User } else { sender = jid.User } } else { sender = jid.User } // Store message msgID := "" if msg.Message.Key != nil && msg.Message.Key.ID != nil { msgID = *msg.Message.Key.ID } // Get message timestamp timestamp := time.Time{} if ts := msg.Message.GetMessageTimestamp(); ts != 0 { timestamp = time.Unix(int64(ts), 0) } else { continue } err = messageStore.StoreMessage( msgID, chatJID, sender, content, timestamp, isFromMe, ) if err != nil { logger.Warnf("Failed to store history message: %v", err) } else { syncedCount++ // Log successful message storage logger.Infof("Stored message: [%s] %s -> %s: %s", timestamp.Format("2006-01-02 15:04:05"), sender, chatJID, content) } } } } fmt.Printf("History sync complete. Stored %d text messages.\n", syncedCount) } // Request history sync from the server func requestHistorySync(client *whatsmeow.Client) { if client == nil { fmt.Println("Client is not initialized. Cannot request history sync.") return } if !client.IsConnected() { fmt.Println("Client is not connected. Please ensure you are connected to WhatsApp first.") return } if client.Store.ID == nil { fmt.Println("Client is not logged in. Please scan the QR code first.") return } // Build and send a history sync request historyMsg := client.BuildHistorySyncRequest(nil, 100) if historyMsg == nil { fmt.Println("Failed to build history sync request.") return } _, err := client.SendMessage(context.Background(), types.JID{ Server: "s.whatsapp.net", User: "status", }, historyMsg) if err != nil { fmt.Printf("Failed to request history sync: %v\n", err) } else { fmt.Println("History sync requested. Waiting for server response...") } }