whatsapp-multi-devicewhatsapp-apiwhatsapprestgolanggowhatsapp-web-multi-devicewhatsapp-api-gorest-apigolang-whatsapp-apigolang-whatsappbot
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
308 lines
8.4 KiB
308 lines
8.4 KiB
package whatsapp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/config"
|
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/internal/websocket"
|
|
pkgError "github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/error"
|
|
"github.com/aldinokemal/go-whatsapp-web-multidevice/pkg/utils"
|
|
"github.com/sirupsen/logrus"
|
|
"go.mau.fi/whatsmeow"
|
|
"go.mau.fi/whatsmeow/appstate"
|
|
"go.mau.fi/whatsmeow/proto/waE2E"
|
|
"go.mau.fi/whatsmeow/store"
|
|
"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"
|
|
)
|
|
|
|
// Type definitions
|
|
type ExtractedMedia struct {
|
|
MediaPath string `json:"media_path"`
|
|
MimeType string `json:"mime_type"`
|
|
Caption string `json:"caption"`
|
|
}
|
|
|
|
type evtReaction struct {
|
|
ID string `json:"id,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type evtMessage struct {
|
|
ID string `json:"id,omitempty"`
|
|
Text string `json:"text,omitempty"`
|
|
RepliedId string `json:"replied_id,omitempty"`
|
|
QuotedMessage string `json:"quoted_message,omitempty"`
|
|
}
|
|
|
|
// Global variables
|
|
var (
|
|
cli *whatsmeow.Client
|
|
log waLog.Logger
|
|
historySyncID int32
|
|
startupTime = time.Now().Unix()
|
|
)
|
|
|
|
// InitWaDB initializes the WhatsApp database connection
|
|
func InitWaDB() *sqlstore.Container {
|
|
log = waLog.Stdout("Main", config.WhatsappLogLevel, true)
|
|
dbLog := waLog.Stdout("Database", config.WhatsappLogLevel, true)
|
|
|
|
storeContainer, err := initDatabase(dbLog)
|
|
if err != nil {
|
|
log.Errorf("Database initialization error: %v", err)
|
|
panic(pkgError.InternalServerError(fmt.Sprintf("Database initialization error: %v", err)))
|
|
}
|
|
|
|
return storeContainer
|
|
}
|
|
|
|
// initDatabase creates and returns a database store container based on the configured URI
|
|
func initDatabase(dbLog waLog.Logger) (*sqlstore.Container, error) {
|
|
if strings.HasPrefix(config.DBURI, "file:") {
|
|
return sqlstore.New("sqlite3", config.DBURI, dbLog)
|
|
} else if strings.HasPrefix(config.DBURI, "postgres:") {
|
|
return sqlstore.New("postgres", config.DBURI, dbLog)
|
|
}
|
|
|
|
return nil, fmt.Errorf("unknown database type: %s. Currently only sqlite3(file:) and postgres are supported", config.DBURI)
|
|
}
|
|
|
|
// InitWaCLI initializes the WhatsApp client
|
|
func InitWaCLI(storeContainer *sqlstore.Container) *whatsmeow.Client {
|
|
device, err := storeContainer.GetFirstDevice()
|
|
if err != nil {
|
|
log.Errorf("Failed to get device: %v", err)
|
|
panic(err)
|
|
}
|
|
|
|
if device == nil {
|
|
log.Errorf("No device found")
|
|
panic("No device found")
|
|
}
|
|
|
|
// Configure device properties
|
|
osName := fmt.Sprintf("%s %s", config.AppOs, config.AppVersion)
|
|
store.DeviceProps.PlatformType = &config.AppPlatform
|
|
store.DeviceProps.Os = &osName
|
|
|
|
// Create and configure the client
|
|
cli = whatsmeow.NewClient(device, waLog.Stdout("Client", config.WhatsappLogLevel, true))
|
|
cli.EnableAutoReconnect = true
|
|
cli.AutoTrustIdentity = true
|
|
cli.AddEventHandler(handler)
|
|
|
|
return cli
|
|
}
|
|
|
|
// handler is the main event handler for WhatsApp events
|
|
func handler(rawEvt interface{}) {
|
|
switch evt := rawEvt.(type) {
|
|
case *events.DeleteForMe:
|
|
handleDeleteForMe(evt)
|
|
case *events.AppStateSyncComplete:
|
|
handleAppStateSyncComplete(evt)
|
|
case *events.PairSuccess:
|
|
handlePairSuccess(evt)
|
|
case *events.LoggedOut:
|
|
handleLoggedOut()
|
|
case *events.Connected, *events.PushNameSetting:
|
|
handleConnectionEvents()
|
|
case *events.StreamReplaced:
|
|
handleStreamReplaced()
|
|
case *events.Message:
|
|
handleMessage(evt)
|
|
case *events.Receipt:
|
|
handleReceipt(evt)
|
|
case *events.Presence:
|
|
handlePresence(evt)
|
|
case *events.HistorySync:
|
|
handleHistorySync(evt)
|
|
case *events.AppState:
|
|
handleAppState(evt)
|
|
}
|
|
}
|
|
|
|
// Event handler functions
|
|
|
|
func handleDeleteForMe(evt *events.DeleteForMe) {
|
|
log.Infof("Deleted message %s for %s", evt.MessageID, evt.SenderJID.String())
|
|
}
|
|
|
|
func handleAppStateSyncComplete(evt *events.AppStateSyncComplete) {
|
|
if len(cli.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
|
|
if err := cli.SendPresence(types.PresenceAvailable); err != nil {
|
|
log.Warnf("Failed to send available presence: %v", err)
|
|
} else {
|
|
log.Infof("Marked self as available")
|
|
}
|
|
}
|
|
}
|
|
|
|
func handlePairSuccess(evt *events.PairSuccess) {
|
|
websocket.Broadcast <- websocket.BroadcastMessage{
|
|
Code: "LOGIN_SUCCESS",
|
|
Message: fmt.Sprintf("Successfully pair with %s", evt.ID.String()),
|
|
}
|
|
}
|
|
|
|
func handleLoggedOut() {
|
|
websocket.Broadcast <- websocket.BroadcastMessage{
|
|
Code: "LIST_DEVICES",
|
|
Result: nil,
|
|
}
|
|
}
|
|
|
|
func handleConnectionEvents() {
|
|
if len(cli.Store.PushName) == 0 {
|
|
return
|
|
}
|
|
|
|
// Send presence available when connecting and when the pushname is changed.
|
|
// This makes sure that outgoing messages always have the right pushname.
|
|
if err := cli.SendPresence(types.PresenceAvailable); err != nil {
|
|
log.Warnf("Failed to send available presence: %v", err)
|
|
} else {
|
|
log.Infof("Marked self as available")
|
|
}
|
|
}
|
|
|
|
func handleStreamReplaced() {
|
|
os.Exit(0)
|
|
}
|
|
|
|
func handleMessage(evt *events.Message) {
|
|
// Log message metadata
|
|
metaParts := buildMessageMetaParts(evt)
|
|
log.Infof("Received message %s from %s (%s): %+v",
|
|
evt.Info.ID,
|
|
evt.Info.SourceString(),
|
|
strings.Join(metaParts, ", "),
|
|
evt.Message,
|
|
)
|
|
|
|
// Record the message
|
|
message := ExtractMessageText(evt)
|
|
utils.RecordMessage(evt.Info.ID, evt.Info.Sender.String(), message)
|
|
|
|
// Handle image message if present
|
|
handleImageMessage(evt)
|
|
|
|
// Handle auto-reply if configured
|
|
handleAutoReply(evt)
|
|
|
|
// Forward to webhook if configured
|
|
handleWebhookForward(evt)
|
|
}
|
|
|
|
func buildMessageMetaParts(evt *events.Message) []string {
|
|
metaParts := []string{
|
|
fmt.Sprintf("pushname: %s", evt.Info.PushName),
|
|
fmt.Sprintf("timestamp: %s", evt.Info.Timestamp),
|
|
}
|
|
if evt.Info.Type != "" {
|
|
metaParts = append(metaParts, fmt.Sprintf("type: %s", evt.Info.Type))
|
|
}
|
|
if evt.Info.Category != "" {
|
|
metaParts = append(metaParts, fmt.Sprintf("category: %s", evt.Info.Category))
|
|
}
|
|
if evt.IsViewOnce {
|
|
metaParts = append(metaParts, "view once")
|
|
}
|
|
return metaParts
|
|
}
|
|
|
|
func handleImageMessage(evt *events.Message) {
|
|
if img := evt.Message.GetImageMessage(); img != nil {
|
|
if path, err := ExtractMedia(config.PathStorages, img); err != nil {
|
|
log.Errorf("Failed to download image: %v", err)
|
|
} else {
|
|
log.Infof("Image downloaded to %s", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleAutoReply(evt *events.Message) {
|
|
if config.WhatsappAutoReplyMessage != "" &&
|
|
!isGroupJid(evt.Info.Chat.String()) &&
|
|
!evt.Info.IsIncomingBroadcast() &&
|
|
evt.Message.GetExtendedTextMessage().GetText() != "" {
|
|
_, _ = cli.SendMessage(
|
|
context.Background(),
|
|
FormatJID(evt.Info.Sender.String()),
|
|
&waE2E.Message{Conversation: proto.String(config.WhatsappAutoReplyMessage)},
|
|
)
|
|
}
|
|
}
|
|
|
|
func handleWebhookForward(evt *events.Message) {
|
|
if len(config.WhatsappWebhook) > 0 &&
|
|
!strings.Contains(evt.Info.SourceString(), "broadcast") &&
|
|
!isFromMySelf(evt.Info.SourceString()) {
|
|
go func(evt *events.Message) {
|
|
if err := forwardToWebhook(evt); err != nil {
|
|
logrus.Error("Failed forward to webhook: ", err)
|
|
}
|
|
}(evt)
|
|
}
|
|
}
|
|
|
|
func handleReceipt(evt *events.Receipt) {
|
|
if evt.Type == types.ReceiptTypeRead || evt.Type == types.ReceiptTypeReadSelf {
|
|
log.Infof("%v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp)
|
|
} else if evt.Type == types.ReceiptTypeDelivered {
|
|
log.Infof("%s was delivered to %s at %s", evt.MessageIDs[0], evt.SourceString(), evt.Timestamp)
|
|
}
|
|
}
|
|
|
|
func handlePresence(evt *events.Presence) {
|
|
if evt.Unavailable {
|
|
if evt.LastSeen.IsZero() {
|
|
log.Infof("%s is now offline", evt.From)
|
|
} else {
|
|
log.Infof("%s is now offline (last seen: %s)", evt.From, evt.LastSeen)
|
|
}
|
|
} else {
|
|
log.Infof("%s is now online", evt.From)
|
|
}
|
|
}
|
|
|
|
func handleHistorySync(evt *events.HistorySync) {
|
|
id := atomic.AddInt32(&historySyncID, 1)
|
|
fileName := fmt.Sprintf("%s/history-%d-%s-%d-%s.json",
|
|
config.PathStorages,
|
|
startupTime,
|
|
cli.Store.ID.String(),
|
|
id,
|
|
evt.Data.SyncType.String(),
|
|
)
|
|
|
|
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
log.Errorf("Failed to open file to write history sync: %v", err)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
enc := json.NewEncoder(file)
|
|
enc.SetIndent("", " ")
|
|
if err = enc.Encode(evt.Data); err != nil {
|
|
log.Errorf("Failed to write history sync: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Infof("Wrote history sync to %s", fileName)
|
|
}
|
|
|
|
func handleAppState(evt *events.AppState) {
|
|
log.Debugf("App state event: %+v / %+v", evt.Index, evt.SyncActionValue)
|
|
}
|